Restore persistent system path composition
This commit is contained in:
parent
84bb22a7db
commit
7aff8e542e
2 changed files with 338 additions and 30 deletions
|
|
@ -160,6 +160,7 @@ impl StateAnchorLaunchAuthority {
|
|||
pub struct RuntimeStateAnchorAuthority {
|
||||
lower: OwnedFd,
|
||||
persistent_anchor: OwnedFd,
|
||||
upper: OwnedFd,
|
||||
runtime_anchor: OwnedFd,
|
||||
merged_label: PathBuf,
|
||||
merged_descriptor: OwnedFd,
|
||||
|
|
@ -214,6 +215,7 @@ impl RuntimeStateAnchorAuthority {
|
|||
inherited.persistent_anchor_label,
|
||||
(retained_persistent.st_dev, retained_persistent.st_ino),
|
||||
)?;
|
||||
let upper = open_directory(&persistent_anchor, Path::new("upper"))?;
|
||||
let runtime_root = reopen_inherited_directory(inherited.runtime_root_fd)?;
|
||||
let retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?;
|
||||
if !inherited.runtime_anchor_label.is_absolute() {
|
||||
|
|
@ -231,14 +233,13 @@ impl RuntimeStateAnchorAuthority {
|
|||
inherited.runtime_anchor_label,
|
||||
(retained.st_dev, retained.st_ino),
|
||||
)?;
|
||||
for name in ["upper", "work"] {
|
||||
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
||||
}
|
||||
drop(open_directory(&persistent_anchor, Path::new("work"))?);
|
||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||
Ok(Self {
|
||||
lower,
|
||||
persistent_anchor,
|
||||
upper,
|
||||
runtime_anchor,
|
||||
merged_label,
|
||||
merged_descriptor,
|
||||
|
|
@ -421,6 +422,54 @@ impl RuntimeStateAnchorMount {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates exact transient child targets and proves they are absent from the upper layer.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses changed mounts, invalid paths, non-files, links, nested mounts, or upper entries.
|
||||
pub fn require_transient_override_targets(
|
||||
&self,
|
||||
targets: &[PathBuf],
|
||||
) -> Result<(), StateAnchorError> {
|
||||
if mount_identity(&self.merged)? != self.mount {
|
||||
return Err(StateAnchorError::OwnershipMismatch);
|
||||
}
|
||||
for target in targets {
|
||||
if target.as_os_str().is_empty()
|
||||
|| target
|
||||
.components()
|
||||
.any(|component| !matches!(component, std::path::Component::Normal(_)))
|
||||
{
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"transient override target is not normalized and relative",
|
||||
));
|
||||
}
|
||||
let descriptor = openat2(
|
||||
&self.merged,
|
||||
target,
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV,
|
||||
)
|
||||
.map_err(io::Error::from)?;
|
||||
if !FileType::from_raw_mode(
|
||||
rustix::fs::fstat(&descriptor)
|
||||
.map_err(io::Error::from)?
|
||||
.st_mode,
|
||||
)
|
||||
.is_file()
|
||||
{
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"transient override target is not a regular file",
|
||||
));
|
||||
}
|
||||
require_upper_target_absent(&self.authority.upper, target)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmounts and reaps only the retained exact RS-owned State Anchor.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -453,6 +502,38 @@ impl RuntimeStateAnchorMount {
|
|||
}
|
||||
}
|
||||
|
||||
fn require_upper_target_absent(upper: &OwnedFd, target: &Path) -> Result<(), StateAnchorError> {
|
||||
require_upper_path_absent(upper, target)?;
|
||||
let leaf = target.file_name().ok_or(StateAnchorError::InvalidLayout(
|
||||
"transient override target has no final component",
|
||||
))?;
|
||||
let mut whiteout = std::ffi::OsString::from(".wh.");
|
||||
whiteout.push(leaf);
|
||||
require_upper_path_absent(
|
||||
upper,
|
||||
&target.parent().unwrap_or(Path::new("")).join(whiteout),
|
||||
)
|
||||
}
|
||||
|
||||
fn require_upper_path_absent(upper: &OwnedFd, target: &Path) -> Result<(), StateAnchorError> {
|
||||
match openat2(
|
||||
upper,
|
||||
target,
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV,
|
||||
) {
|
||||
Err(rustix::io::Errno::NOENT) => Ok(()),
|
||||
Ok(_) => Err(StateAnchorError::InvalidLayout(
|
||||
"transient override target is present in the upper layer",
|
||||
)),
|
||||
Err(error) => Err(io::Error::from(error).into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifiedStateAnchorReadiness {
|
||||
#[must_use]
|
||||
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,28 @@ fn run() -> io::Result<()> {
|
|||
},
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
let runtime_targets = [
|
||||
PathBuf::from("etc/resolv.conf"),
|
||||
PathBuf::from("etc/hosts"),
|
||||
PathBuf::from("etc/hostname"),
|
||||
];
|
||||
runtime_mount
|
||||
.require_persistent_destinations(&[PathBuf::from("usr"), PathBuf::from("etc")])
|
||||
.map_err(io::Error::other)?;
|
||||
runtime_mount
|
||||
.require_transient_override_targets(&runtime_targets)
|
||||
.map_err(io::Error::other)?;
|
||||
fs::write(layout.merged().join("etc/hosts"), b"stale")?;
|
||||
if runtime_mount
|
||||
.require_transient_override_targets(&runtime_targets)
|
||||
.is_ok()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"upper runtime-file entry survived transient override preflight",
|
||||
));
|
||||
}
|
||||
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
||||
|
||||
let first = acquire(&layout, &artifact)?;
|
||||
fs::write(
|
||||
|
|
|
|||
Loading…
Reference in a new issue