mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
fix(vm): keep caller shares out of the reserved guest namespace
A caller share targeting /.ecrou-io (or inside it) shadowed the capture share's files, and one at /.ecrou-exit or '/' shadowed the exit handshake; collect_shares never looked at guest targets. The capture share itself still owns the namespace by design; workspace and --share are validated like any other caller share. The overlap check takes the rootfs path directly.
This commit is contained in:
1 parent
f9250accc0
commit
f3cbf16180
1 file changed
+99
-3
+99
-3
@@ -179,7 +179,7 @@ pub fn run_prepared(rootfs: &PreparedRootfs, options: &RunOptions) -> Result<i32
|
|||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(Error::Other)?;
|
.map_err(Error::Other)?;
|
||||||
let share_pairs = collect_shares(rootfs, options, capture_share).map_err(Error::Other)?;
|
let share_pairs = collect_shares(rootfs.dir(), options, capture_share).map_err(Error::Other)?;
|
||||||
let shares = share_pairs
|
let shares = share_pairs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, gs)| gs.clone())
|
.map(|(_, gs)| gs.clone())
|
||||||
@@ -299,7 +299,7 @@ pub fn run_prepared(rootfs: &PreparedRootfs, options: &RunOptions) -> Result<i32
|
|||||||
/// must exist, and no share may overlap the scratch rootfs, or the
|
/// must exist, and no share may overlap the scratch rootfs, or the
|
||||||
/// guest could rewrite its own init.
|
/// guest could rewrite its own init.
|
||||||
fn collect_shares(
|
fn collect_shares(
|
||||||
rootfs: &PreparedRootfs,
|
rootfs_dir: &std::path::Path,
|
||||||
options: &RunOptions,
|
options: &RunOptions,
|
||||||
capture: Option<Share>,
|
capture: Option<Share>,
|
||||||
) -> anyhow::Result<Vec<(Share, GuestShare)>> {
|
) -> anyhow::Result<Vec<(Share, GuestShare)>> {
|
||||||
@@ -308,6 +308,7 @@ fn collect_shares(
|
|||||||
all.push(ws.clone());
|
all.push(ws.clone());
|
||||||
}
|
}
|
||||||
all.extend(options.shares.iter().cloned());
|
all.extend(options.shares.iter().cloned());
|
||||||
|
let caller_count = all.len();
|
||||||
// The capture share rides last, after every caller share.
|
// The capture share rides last, after every caller share.
|
||||||
if let Some(cap) = capture {
|
if let Some(cap) = capture {
|
||||||
all.push(cap);
|
all.push(cap);
|
||||||
@@ -321,12 +322,18 @@ fn collect_shares(
|
|||||||
share.host.display()
|
share.host.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if rootfs.dir().starts_with(&share.host) || share.host.starts_with(rootfs.dir()) {
|
if rootfs_dir.starts_with(&share.host) || share.host.starts_with(rootfs_dir) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"share {} overlaps the scratch rootfs",
|
"share {} overlaps the scratch rootfs",
|
||||||
share.host.display()
|
share.host.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// The capture share owns the reserved namespace by design;
|
||||||
|
// caller shares may not touch it, or a mount would shadow the
|
||||||
|
// control paths the init and the exit handshake rely on.
|
||||||
|
if i < caller_count {
|
||||||
|
reject_reserved_namespace(&share.guest)?;
|
||||||
|
}
|
||||||
out.push((
|
out.push((
|
||||||
share.clone(),
|
share.clone(),
|
||||||
GuestShare {
|
GuestShare {
|
||||||
@@ -339,6 +346,32 @@ fn collect_shares(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reject a caller share whose guest target would shadow a reserved
|
||||||
|
/// guest path: the capture directory and the exit file. Equal to a
|
||||||
|
/// reserved path, inside one, or containing one (a share at `/`
|
||||||
|
/// shadows everything) are all refused.
|
||||||
|
fn reject_reserved_namespace(target: &str) -> anyhow::Result<()> {
|
||||||
|
let reserved = [crate::guest::CAPTURE_DIR, crate::guest::EXIT_FILE];
|
||||||
|
if target == "/" {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"share target / is reserved by ecrou: it would shadow {} and {}",
|
||||||
|
crate::guest::CAPTURE_DIR,
|
||||||
|
crate::guest::EXIT_FILE
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for path in reserved {
|
||||||
|
if target == path
|
||||||
|
|| target.starts_with(&format!("{path}/"))
|
||||||
|
|| path.starts_with(&format!("{target}/"))
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"share target {target} is reserved by ecrou ({path}); pick another guest path"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// The guest payload: the caller's argv, or the rootfs's default shell.
|
/// The guest payload: the caller's argv, or the rootfs's default shell.
|
||||||
fn payload_command(options: &RunOptions, rootfs_dir: &Path) -> Vec<String> {
|
fn payload_command(options: &RunOptions, rootfs_dir: &Path) -> Vec<String> {
|
||||||
let shell = utils::detect_shell(rootfs_dir).to_string();
|
let shell = utils::detect_shell(rootfs_dir).to_string();
|
||||||
@@ -443,6 +476,69 @@ mod tests {
|
|||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn caller_shares_cannot_take_the_reserved_namespace() {
|
||||||
|
let scratch = tempfile::tempdir().unwrap();
|
||||||
|
let host = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let share = |guest: &str| Share {
|
||||||
|
host: host.path().to_path_buf(),
|
||||||
|
guest: guest.to_string(),
|
||||||
|
read_only: true,
|
||||||
|
};
|
||||||
|
// Equal to a reserved path, inside one, or containing one.
|
||||||
|
for guest in [
|
||||||
|
crate::guest::CAPTURE_DIR,
|
||||||
|
&format!("{}/stdout", crate::guest::CAPTURE_DIR),
|
||||||
|
crate::guest::EXIT_FILE,
|
||||||
|
"/",
|
||||||
|
] {
|
||||||
|
let options = RunOptions {
|
||||||
|
shares: vec![share(guest)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = format!(
|
||||||
|
"{:#}",
|
||||||
|
collect_shares(scratch.path(), &options, None).unwrap_err()
|
||||||
|
);
|
||||||
|
assert!(err.contains("reserved by ecrou"), "{err}");
|
||||||
|
}
|
||||||
|
// The workspace is a caller share like any other.
|
||||||
|
let options = RunOptions {
|
||||||
|
workspace: Some(share(crate::guest::CAPTURE_DIR)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = format!(
|
||||||
|
"{:#}",
|
||||||
|
collect_shares(scratch.path(), &options, None).unwrap_err()
|
||||||
|
);
|
||||||
|
assert!(err.contains("reserved by ecrou"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_capture_share_may_use_the_reserved_namespace() {
|
||||||
|
let scratch = tempfile::tempdir().unwrap();
|
||||||
|
let host = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let options = RunOptions {
|
||||||
|
workspace: Some(Share {
|
||||||
|
host: host.path().to_path_buf(),
|
||||||
|
guest: "/workspace".to_string(),
|
||||||
|
read_only: false,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let capture = Share {
|
||||||
|
host: host.path().join("out"),
|
||||||
|
guest: crate::guest::CAPTURE_DIR.to_string(),
|
||||||
|
read_only: false,
|
||||||
|
};
|
||||||
|
std::fs::create_dir_all(host.path().join("out")).unwrap();
|
||||||
|
let pairs = collect_shares(scratch.path(), &options, Some(capture)).unwrap();
|
||||||
|
assert_eq!(pairs.len(), 2);
|
||||||
|
assert_eq!(pairs[1].1.target, crate::guest::CAPTURE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn payload_defaults_to_the_image_shell() {
|
fn payload_defaults_to_the_image_shell() {
|
||||||
let rootfs = shell_rootfs();
|
let rootfs = shell_rootfs();
|
||||||
|
|||||||
Reference in new issue
Block a user