Skip to content

Commit 5d3108e

Browse files
committed
sandlock-core: report read-only mounts as ro in /proc/mounts and mountinfo
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 18ed1b3 commit 5d3108e

1 file changed

Lines changed: 56 additions & 10 deletions

File tree

crates/sandlock-core/src/procfs.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,19 @@ fn detect_fstype(path: &std::path::Path) -> &'static str {
202202
}
203203
}
204204

205+
/// Whether the chroot rootfs should be reported read-only in the synthesized
206+
/// mount tables. Only meaningful under a chroot, where the rootfs is presented
207+
/// as its own mount: it is read-only unless `/` was granted write (a read-write
208+
/// OCI rootfs or `--fs-write /`). Without a chroot, the root is the host's real
209+
/// (read-write) `/`, restricted by Landlock rather than a read-only mount.
210+
fn root_is_read_only(policy: &NotifPolicy) -> bool {
211+
policy.chroot_root.is_some()
212+
&& !policy
213+
.chroot_writable
214+
.iter()
215+
.any(|p| p.as_path() == std::path::Path::new("/"))
216+
}
217+
205218
/// Generate a virtual /proc/mounts showing only the sandbox's own mounts.
206219
///
207220
/// Produces standard `/proc/mounts` format: `device mountpoint type options dump pass`
@@ -210,20 +223,28 @@ fn detect_fstype(path: &std::path::Path) -> &'static str {
210223
pub(crate) fn generate_proc_mounts(
211224
chroot_root: Option<&std::path::Path>,
212225
chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)],
226+
chroot_mount_ro: &[std::path::PathBuf],
227+
root_ro: bool,
213228
) -> Vec<u8> {
214229
let mut buf = String::new();
215230

216231
if let Some(root) = chroot_root {
217232
let fstype = detect_fstype(root);
218-
buf.push_str(&format!("sandlock / {} rw,relatime 0 0\n", fstype));
233+
let opts = if root_ro { "ro,relatime" } else { "rw,relatime" };
234+
buf.push_str(&format!("sandlock / {} {} 0 0\n", fstype, opts));
219235
} else {
220-
buf.push_str("rootfs / rootfs rw 0 0\n");
236+
buf.push_str(&format!("rootfs / rootfs {} 0 0\n", if root_ro { "ro" } else { "rw" }));
221237
}
222238

223239
for (virtual_path, host_path) in chroot_mounts {
224240
let vp = virtual_path.to_string_lossy();
225241
let fstype = detect_fstype(host_path);
226-
buf.push_str(&format!("sandlock {} {} rw,relatime 0 0\n", vp, fstype));
242+
let opts = if chroot_mount_ro.iter().any(|d| d == virtual_path) {
243+
"ro,relatime"
244+
} else {
245+
"rw,relatime"
246+
};
247+
buf.push_str(&format!("sandlock {} {} {} 0 0\n", vp, fstype, opts));
227248
}
228249

229250
buf.into_bytes()
@@ -236,27 +257,35 @@ pub(crate) fn generate_proc_mounts(
236257
pub(crate) fn generate_proc_mountinfo(
237258
chroot_root: Option<&std::path::Path>,
238259
chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)],
260+
chroot_mount_ro: &[std::path::PathBuf],
261+
root_ro: bool,
239262
) -> Vec<u8> {
240263
let mut buf = String::new();
241264
let mut mount_id: u32 = 20;
265+
let (root_opts, root_super) = if root_ro { ("ro,relatime", "ro") } else { ("rw,relatime", "rw") };
242266

243267
if let Some(root) = chroot_root {
244268
let fstype = detect_fstype(root);
245269
buf.push_str(&format!(
246-
"{} 1 8:1 / / rw,relatime - {} sandlock rw\n", mount_id, fstype
270+
"{} 1 8:1 / / {} - {} sandlock {}\n", mount_id, root_opts, fstype, root_super
247271
));
248272
} else {
249273
buf.push_str(&format!(
250-
"{} 1 0:1 / / rw - rootfs rootfs rw\n", mount_id
274+
"{} 1 0:1 / / {} - rootfs rootfs {}\n", mount_id, root_super, root_super
251275
));
252276
}
253277
mount_id += 1;
254278

255279
for (virtual_path, host_path) in chroot_mounts {
256280
let vp = virtual_path.to_string_lossy();
257281
let fstype = detect_fstype(host_path);
282+
let (opts, sup) = if chroot_mount_ro.iter().any(|d| d == virtual_path) {
283+
("ro,relatime", "ro")
284+
} else {
285+
("rw,relatime", "rw")
286+
};
258287
buf.push_str(&format!(
259-
"{} 20 8:1 / {} rw,relatime - {} sandlock rw\n", mount_id, vp, fstype
288+
"{} 20 8:1 / {} {} - {} sandlock {}\n", mount_id, vp, opts, fstype, sup
260289
));
261290
mount_id += 1;
262291
}
@@ -461,6 +490,8 @@ pub(crate) async fn handle_proc_open(
461490
let content = generate_proc_mounts(
462491
policy.chroot_root.as_deref(),
463492
&policy.chroot_mounts,
493+
&policy.chroot_mount_ro,
494+
root_is_read_only(policy),
464495
);
465496
return inject_memfd(&content);
466497
}
@@ -470,6 +501,8 @@ pub(crate) async fn handle_proc_open(
470501
let content = generate_proc_mountinfo(
471502
policy.chroot_root.as_deref(),
472503
&policy.chroot_mounts,
504+
&policy.chroot_mount_ro,
505+
root_is_read_only(policy),
473506
);
474507
return inject_memfd(&content);
475508
}
@@ -1156,7 +1189,8 @@ mod tests {
11561189
(std::path::PathBuf::from("/work"), tmp.clone()),
11571190
(std::path::PathBuf::from("/data"), tmp.clone()),
11581191
];
1159-
let content = generate_proc_mounts(Some(tmp.as_path()), &mounts);
1192+
let ro = vec![std::path::PathBuf::from("/data")];
1193+
let content = generate_proc_mounts(Some(tmp.as_path()), &mounts, &ro, false);
11601194
let text = String::from_utf8(content).unwrap();
11611195
// Root entry with detected fstype (not hardcoded ext4)
11621196
assert!(text.starts_with("sandlock / "), "Should start with root entry, got: {}", text);
@@ -1167,12 +1201,24 @@ mod tests {
11671201
// Fstype should be detected, not "unknown" (tmp is on a real fs)
11681202
let root_line = text.lines().next().unwrap();
11691203
assert!(!root_line.contains("unknown"), "root fstype should be detected, got: {}", root_line);
1204+
// Options reflect read-only: /data is ro, /work and root are rw.
1205+
assert!(text.lines().any(|l| l.starts_with("sandlock / ") && l.contains(" rw,relatime ")));
1206+
assert!(text.lines().any(|l| l.starts_with("sandlock /work ") && l.contains(" rw,relatime ")));
1207+
assert!(text.lines().any(|l| l.starts_with("sandlock /data ") && l.contains(" ro,relatime ")));
1208+
}
1209+
1210+
#[test]
1211+
fn test_generate_proc_mounts_read_only_root() {
1212+
let tmp = std::env::temp_dir();
1213+
let content = generate_proc_mounts(Some(tmp.as_path()), &[], &[], true);
1214+
let text = String::from_utf8(content).unwrap();
1215+
assert!(text.lines().next().unwrap().contains(" ro,relatime "), "got: {}", text);
11701216
}
11711217

11721218
#[test]
11731219
fn test_generate_proc_mounts_no_chroot() {
11741220
let mounts: Vec<(std::path::PathBuf, std::path::PathBuf)> = vec![];
1175-
let content = generate_proc_mounts(None, &mounts);
1221+
let content = generate_proc_mounts(None, &mounts, &[], false);
11761222
let text = String::from_utf8(content).unwrap();
11771223
assert!(text.contains("rootfs / rootfs rw 0 0"));
11781224
assert_eq!(text.lines().count(), 1);
@@ -1184,7 +1230,7 @@ mod tests {
11841230
let mounts = vec![
11851231
(std::path::PathBuf::from("/work"), tmp.clone()),
11861232
];
1187-
let content = generate_proc_mountinfo(Some(tmp.as_path()), &mounts);
1233+
let content = generate_proc_mountinfo(Some(tmp.as_path()), &mounts, &[], false);
11881234
let text = String::from_utf8(content).unwrap();
11891235
assert!(text.contains("/ / rw,relatime -"));
11901236
assert!(text.contains("/ /work rw,relatime -"));
@@ -1195,7 +1241,7 @@ mod tests {
11951241
#[test]
11961242
fn test_generate_proc_mountinfo_no_chroot() {
11971243
let mounts: Vec<(std::path::PathBuf, std::path::PathBuf)> = vec![];
1198-
let content = generate_proc_mountinfo(None, &mounts);
1244+
let content = generate_proc_mountinfo(None, &mounts, &[], false);
11991245
let text = String::from_utf8(content).unwrap();
12001246
assert!(text.contains("/ / rw - rootfs rootfs rw"));
12011247
assert_eq!(text.lines().count(), 1);

0 commit comments

Comments
 (0)