Refuse unrecorded publication artifacts during recovery

This commit is contained in:
ms2k 2026-07-17 07:03:29 +09:00
commit d597b05484

View file

@ -785,16 +785,17 @@ fn recover_publication(
if participant.state() == ParticipantState::Clean {
return Ok(());
}
let Some(ParticipantOwnership::Publication(publication)) = participant.ownership() else {
if participant.ownership().is_none() {
return clean_role_after_absence(store, generation_id, ParticipantRole::Publication);
}
return Err("publication ownership shape is invalid".into());
};
let root = configuration
.runtime_domain_root()
.open_authority()
.map_err(|error| error.to_string())?;
let Some(ParticipantOwnership::Publication(publication)) = participant.ownership() else {
if participant.ownership().is_none() {
require_bare_publication_absent(&root)?;
return clean_role_after_absence(store, generation_id, ParticipantRole::Publication);
}
return Err("publication ownership shape is invalid".into());
};
require_publication_labels(publication)?;
remove_exact_publication_resource(
&root,
@ -815,6 +816,27 @@ fn recover_publication(
clean_role_after_absence(store, generation_id, ParticipantRole::Publication)
}
fn require_bare_publication_absent(root: &std::os::fd::OwnedFd) -> Result<(), String> {
for label in [
STAGED_CONTROL_SOCKET_NAME,
STAGED_PUBLICATION_IDENTITY_NAME,
CONTROL_SOCKET_NAME,
PUBLICATION_IDENTITY_NAME,
] {
match openat(
root,
label,
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
) {
Err(rustix::io::Errno::NOENT) => {}
Err(error) => return Err(error.to_string()),
Ok(_) => return Err("bare publication intent has a fixed-label artifact".into()),
}
}
Ok(())
}
fn require_publication_labels(
ownership: &lazaret_core::records::PublicationOwnership,
) -> Result<(), String> {
@ -1280,9 +1302,10 @@ fn hex_lower(bytes: &[u8]) -> String {
mod tests {
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::symlink;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{
@ -1517,6 +1540,61 @@ mod tests {
.unwrap();
}
#[derive(Clone, Copy)]
enum PublicationFixtureBoundary {
TrulyAbsent,
AfterStagedIdentity,
AfterStagedSocket,
}
#[test]
fn bare_publication_intent_requires_every_fixed_label_to_be_absent() {
let absent = ProductionRecoveryFixture::new();
absent.prepare_bare_publication(PublicationFixtureBoundary::TrulyAbsent);
recover_publication(&absent.configuration, &absent.store, GenerationId::new(2)).unwrap();
assert_eq!(absent.publication(2).state(), ParticipantState::Clean);
let identity = ProductionRecoveryFixture::new();
identity.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedIdentity);
assert!(
recover_publication(
&identity.configuration,
&identity.store,
GenerationId::new(2)
)
.is_err()
);
assert!(
identity
.runtime_domain()
.join(STAGED_PUBLICATION_IDENTITY_NAME)
.is_file()
);
assert_eq!(identity.publication(2).state(), ParticipantState::Intent);
assert!(identity.publication(2).ownership().is_none());
let socket = ProductionRecoveryFixture::new();
socket.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedSocket);
assert!(
recover_publication(&socket.configuration, &socket.store, GenerationId::new(2))
.is_err()
);
assert!(
socket
.runtime_domain()
.join(STAGED_PUBLICATION_IDENTITY_NAME)
.is_file()
);
assert!(
fs::symlink_metadata(socket.runtime_domain().join(STAGED_CONTROL_SOCKET_NAME))
.unwrap()
.file_type()
.is_socket()
);
assert_eq!(socket.publication(2).state(), ParticipantState::Intent);
assert!(socket.publication(2).ownership().is_none());
}
#[test]
fn production_preflight_retries_every_recovery_boundary() {
for failed in [
@ -1993,6 +2071,74 @@ mod tests {
prepared,
}
}
fn runtime_domain(&self) -> PathBuf {
Path::new(std::ffi::OsStr::from_bytes(
self.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
))
.to_path_buf()
}
fn prepare_bare_publication(&self, boundary: PublicationFixtureBoundary) {
let runtime_domain = self.runtime_domain();
fs::remove_file(runtime_domain.join(CONTROL_SOCKET_NAME)).unwrap();
fs::remove_file(runtime_domain.join(PUBLICATION_IDENTITY_NAME)).unwrap();
let generation = Generation {
domain_id: self.configuration.domain_id(),
generation_id: GenerationId::new(2),
configuration_fingerprint: self.configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([13; 32]),
network_sidecar_id: NetworkSidecarId::new([14; 32]),
sandbox_id: SandboxId::new([15; 32]),
durable_decision: DurableDecision::None,
};
self.store
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
.unwrap();
self.store
.apply(&[
DomainStoreMutation::PutGeneration(Generation {
durable_decision: DurableDecision::Preparing,
..generation
}),
DomainStoreMutation::PutParticipant(Participant::bare_intent(
GenerationId::new(2),
ParticipantRole::Publication,
)),
])
.unwrap();
if !matches!(boundary, PublicationFixtureBoundary::TrulyAbsent) {
fs::write(
runtime_domain.join(STAGED_PUBLICATION_IDENTITY_NAME),
b"identity",
)
.unwrap();
}
if matches!(boundary, PublicationFixtureBoundary::AfterStagedSocket) {
let root = File::open(runtime_domain).unwrap();
drop(
UnixListener::bind(format!(
"/proc/self/fd/{}/{}",
root.as_raw_fd(),
STAGED_CONTROL_SOCKET_NAME
))
.unwrap(),
);
}
}
fn publication(&self, generation: u64) -> Participant {
role(
&self.store,
GenerationId::new(generation),
ParticipantRole::Publication,
)
.unwrap()
.unwrap()
}
}
fn path_identity(root: &Path, path: &Path) -> MountIdentity {