#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
mod common;
use std::sync::Arc;
use kaish_kernel::vfs::{DevFs, LocalFs, MemoryFs, VfsRouter};
use kaish_kernel::{Kernel, KernelBackend, KernelConfig, LocalBackend};
use common::{kernel_at, run};
async fn both_spellings(kernel: &Kernel, setup: &str, op: &str, path: &str, expected: bool) {
if !setup.is_empty() {
let (out, code) = run(kernel, setup).await;
assert_eq!(code, 0, "setup failed: {setup}: out={out:?}");
}
let want = if expected { 0 } else { 1 };
let script = format!("if test {op} {path}; then echo YES; else echo NO; fi");
let (out, _) = run(kernel, &script).await;
assert_eq!(
out,
if expected { "YES" } else { "NO" },
"`test {op} {path}` disagrees (want exit {want})",
);
let script = format!("if [[ {op} {path} ]]; then echo YES; else echo NO; fi");
let (out, _) = run(kernel, &script).await;
assert_eq!(
out,
if expected { "YES" } else { "NO" },
"`[[ {op} {path} ]]` disagrees (want exit {want})",
);
}
#[tokio::test]
async fn memoryfs_file_is_writable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
both_spellings(&kernel, "echo hi > /v/probe.txt", "-w", "/v/probe.txt", true).await;
}
#[tokio::test]
async fn memoryfs_file_really_takes_a_second_write() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
let (_, code) = run(&kernel, "echo one > /v/p2.txt; echo two >> /v/p2.txt").await;
assert_eq!(code, 0, "MemoryFs path must accept an append");
let (out, _) = run(&kernel, "cat /v/p2.txt").await;
assert_eq!(out, "one\ntwo");
}
#[tokio::test]
async fn memoryfs_directory_is_writable_and_searchable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
both_spellings(&kernel, "mkdir -p /v/sub", "-w", "/v/sub", true).await;
both_spellings(&kernel, "", "-x", "/v/sub", true).await;
both_spellings(&kernel, "", "-r", "/v/sub", true).await;
both_spellings(&kernel, "", "-x", "/v", true).await;
both_spellings(&kernel, "", "-w", "/v", true).await;
let (_, code) = run(&kernel, "mkdir -p /v/sub/deeper").await;
assert_eq!(code, 0, "a MemoryFs directory must accept mkdir");
}
#[tokio::test]
async fn builtinfs_entry_is_not_writable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
both_spellings(&kernel, "", "-w", "/v/bin/echo", false).await;
}
#[tokio::test]
async fn builtinfs_entry_is_still_readable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
both_spellings(&kernel, "", "-r", "/v/bin/echo", true).await;
}
#[tokio::test]
async fn builtinfs_entry_really_refuses_a_write() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
let (_, code) = run(&kernel, "echo nope > /v/bin/echo").await;
assert_ne!(code, 0, "BuiltinFs must refuse a write to /v/bin/echo");
}
#[tokio::test]
async fn jobfs_node_is_not_writable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
let (out, code) = run(&kernel, "sleep 30 &").await;
assert_eq!(code, 0, "backgrounding a job failed: {out:?}");
both_spellings(&kernel, "", "-w", "/v/jobs/1/status", false).await;
both_spellings(&kernel, "", "-r", "/v/jobs/1/status", true).await;
}
fn devfs_kernel() -> Kernel {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
vfs.mount("/dev", DevFs::new());
let backend: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(vfs)));
Kernel::with_backend(backend, KernelConfig::isolated(), |_| {}, |_| {})
.expect("with_backend kernel")
}
#[tokio::test]
async fn devfs_null_is_writable() {
let kernel = devfs_kernel();
both_spellings(&kernel, "", "-w", "/dev/null", true).await;
let (_, code) = run(&kernel, "echo discard > /dev/null").await;
assert_eq!(code, 0, "/dev/null must accept the write it says it accepts");
}
#[tokio::test]
async fn devfs_null_is_not_executable() {
let kernel = devfs_kernel();
both_spellings(&kernel, "", "-x", "/dev/null", false).await;
both_spellings(&kernel, "", "-r", "/dev/null", true).await;
}
#[tokio::test]
async fn devfs_directory_is_searchable_but_not_writable() {
let kernel = devfs_kernel();
both_spellings(&kernel, "", "-x", "/dev", true).await;
both_spellings(&kernel, "", "-r", "/dev", true).await;
both_spellings(&kernel, "", "-w", "/dev", false).await;
let (_, code) = run(&kernel, "mkdir /dev/newthing").await;
assert_ne!(code, 0, "/dev must refuse mkdir, as `-w /dev` said it would");
}
#[cfg(unix)]
fn dac_is_bypassed(dir: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
let probe = dir.join(".dac-probe");
std::fs::write(&probe, b"x").unwrap();
std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o000)).unwrap();
let bypassed = std::fs::File::open(&probe).is_ok();
std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o644)).unwrap();
bypassed
}
#[cfg(unix)]
#[tokio::test]
async fn localfs_permissions_still_decide() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("rw.txt"), b"hi\n").unwrap();
std::fs::write(tmp.path().join("ro.txt"), b"hi\n").unwrap();
std::fs::set_permissions(
tmp.path().join("ro.txt"),
std::fs::Permissions::from_mode(0o444),
)
.unwrap();
if dac_is_bypassed(tmp.path()) {
eprintln!("skipping: environment bypasses DAC (root?)");
return;
}
let kernel = kernel_at(tmp.path());
let rw = tmp.path().join("rw.txt");
let ro = tmp.path().join("ro.txt");
both_spellings(&kernel, "", "-w", &rw.display().to_string(), true).await;
both_spellings(&kernel, "", "-w", &ro.display().to_string(), false).await;
}
#[cfg(unix)]
#[tokio::test]
async fn permission_bits_for_another_principal_do_not_grant_access() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
if dac_is_bypassed(tmp.path()) {
eprintln!("skipping: environment bypasses DAC (root?)");
return;
}
let cases = [
(0o022u32, "-w", "w.txt"),
(0o044u32, "-r", "r.txt"),
(0o011u32, "-x", "x.txt"),
];
let kernel = kernel_at(tmp.path());
for (mode, op, name) in cases {
let path = tmp.path().join(name);
std::fs::write(&path, b"hi\n").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap();
let mask = match op {
"-w" => 0o222,
"-r" => 0o444,
_ => 0o111,
};
assert_ne!(
mode & mask,
0,
"fixture is pointless unless the raw mask is set for {name}",
);
both_spellings(&kernel, "", op, &path.display().to_string(), false).await;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
}
#[cfg(unix)]
#[tokio::test]
async fn the_other_principal_fixture_really_is_refused() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
if dac_is_bypassed(tmp.path()) {
eprintln!("skipping: environment bypasses DAC (root?)");
return;
}
let path = tmp.path().join("w.txt");
std::fs::write(&path, b"hi\n").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o022)).unwrap();
let opened = std::fs::OpenOptions::new().append(true).open(&path);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
assert!(
opened.is_err(),
"mode 0o022 owned by us must refuse a write despite 0o222 being set",
);
}
#[cfg(unix)]
#[tokio::test]
async fn localfs_executable_bit_still_decides() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("run.sh"), b"#!/bin/sh\n").unwrap();
std::fs::write(tmp.path().join("plain.txt"), b"hi\n").unwrap();
std::fs::set_permissions(
tmp.path().join("run.sh"),
std::fs::Permissions::from_mode(0o755),
)
.unwrap();
let kernel = kernel_at(tmp.path());
let run_sh = tmp.path().join("run.sh");
let plain = tmp.path().join("plain.txt");
both_spellings(&kernel, "", "-x", &run_sh.display().to_string(), true).await;
both_spellings(&kernel, "", "-x", &plain.display().to_string(), false).await;
}
#[tokio::test]
async fn memory_backed_files_are_not_executable() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
both_spellings(&kernel, "echo hi > /v/probe.txt", "-x", "/v/probe.txt", false).await;
both_spellings(&kernel, "", "-x", "/v/bin/echo", false).await;
}
fn read_only_wrapper_kernel(dir: &std::path::Path) -> Kernel {
let mut vfs = VfsRouter::new();
vfs.mount("/", LocalFs::read_only(dir));
vfs.mount("/v", MemoryFs::new());
let backend: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(vfs)));
Kernel::with_backend(backend, KernelConfig::isolated(), |_| {}, |_| {})
.expect("with_backend kernel")
}
#[cfg(unix)]
#[tokio::test]
async fn read_only_wrapper_over_writable_os_dir_is_not_writable() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("file.txt"), b"hi\n").unwrap();
let mode = {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(tmp.path().join("file.txt"))
.unwrap()
.permissions()
.mode()
};
assert_ne!(mode & 0o222, 0, "fixture must be OS-writable to be the hazard");
let kernel = read_only_wrapper_kernel(tmp.path());
both_spellings(&kernel, "", "-w", "/file.txt", false).await;
}
#[tokio::test]
async fn read_only_wrapper_really_refuses_a_write() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("file.txt"), b"hi\n").unwrap();
let kernel = read_only_wrapper_kernel(tmp.path());
let (_, code) = run(&kernel, "echo nope > /file.txt").await;
assert_ne!(code, 0, "a read-only LocalFs must refuse the write");
}
#[cfg(unix)]
#[tokio::test]
async fn read_only_wrapper_keeps_read_and_execute_answers() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("run.sh"), b"#!/bin/sh\n").unwrap();
std::fs::set_permissions(
tmp.path().join("run.sh"),
std::fs::Permissions::from_mode(0o555),
)
.unwrap();
let kernel = read_only_wrapper_kernel(tmp.path());
both_spellings(&kernel, "", "-r", "/run.sh", true).await;
both_spellings(&kernel, "", "-x", "/run.sh", true).await;
both_spellings(&kernel, "", "-w", "/run.sh", false).await;
}
#[tokio::test]
async fn missing_paths_answer_false_everywhere() {
let tmp = tempfile::tempdir().unwrap();
let kernel = kernel_at(tmp.path());
for op in ["-r", "-w", "-x"] {
both_spellings(&kernel, "", op, "/v/nope.txt", false).await;
both_spellings(&kernel, "", op, "/v/bin/definitely-not-a-builtin", false).await;
}
}
fn nested_mount_only_kernel() -> Kernel {
let mut vfs = VfsRouter::new();
vfs.mount("/v/docs", MemoryFs::new());
let backend: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(vfs)));
Kernel::with_backend(backend, KernelConfig::isolated(), |_| {}, |_| {})
.expect("with_backend kernel")
}
#[tokio::test]
async fn synthesized_ancestor_directories_are_readable_and_searchable() {
let kernel = nested_mount_only_kernel();
for path in ["/", "/v"] {
both_spellings(&kernel, "", "-e", path, true).await;
both_spellings(&kernel, "", "-d", path, true).await;
both_spellings(&kernel, "", "-r", path, true).await;
both_spellings(&kernel, "", "-x", path, true).await;
both_spellings(&kernel, "", "-w", path, false).await;
}
}
#[tokio::test]
async fn a_real_mount_under_a_synthesized_ancestor_still_answers() {
let kernel = nested_mount_only_kernel();
both_spellings(&kernel, "", "-w", "/v/docs", true).await;
let (_, code) = run(&kernel, "echo hi > /v/docs/note.txt").await;
assert_eq!(code, 0, "the real mount must accept the write");
both_spellings(&kernel, "", "-w", "/v/docs/note.txt", true).await;
}