bunpen: namespace: bind all requested user paths, and create requisite directories

This commit is contained in:
2024-08-25 19:06:28 +00:00
parent 1c50ff8fe4
commit 64697a2cb8
2 changed files with 132 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ use fs;
use io;
use log;
use os;
use path;
use rt;
use rtext;
use strings;
@@ -61,22 +62,138 @@ export fn namespace_restrict(what: *resources) void = {
rtext::check_error("mkdir new", rt::mkdir("new", 0o755));
rtext::check_error("mount -t tmpfs tmpfs new", rtext::mount("tmpfs", "new", "tmpfs", 0, null));
// bind old/$dir to new/$dir for every `dir` we want available in the sandbox:
// TODO: mount the paths the user asks for, but until then hardcode stuff.
rtext::check_error("mkdir new/bin", rt::mkdir("new/bin", 0o755));
rtext::check_error("mount old/bin new/bin", rtext::mount("old/bin", "new/bin", "", rtext::MS_BIND | rtext::MS_REC, null));
rtext::check_error("mkdir new/nix", rt::mkdir("new/nix", 0o755));
rtext::check_error("mount old/nix new/nix", rtext::mount("old/nix", "new/nix", "", rtext::MS_BIND | rtext::MS_REC, null));
{
// bind all the user-requested paths from `old/$p` into `new/$p`.
// use the `dirfd` abstraction so that paths meant for `old` can't crawl out
// of that virtual fs.
let old_fd = rt::open("old", rt::O_RDONLY | rt::O_CLOEXEC, rt::RESOLVE_NO_SYMLINKS: uint)!;
let old_fs = os::dirfdopen(old_fd);
defer(free(old_fs));
let new_fd = rt::open("new", rt::O_RDONLY | rt::O_CLOEXEC, rt::RESOLVE_NO_SYMLINKS: uint)!;
let new_fs = os::dirfdopen(new_fd);
defer(free(new_fs));
for (let path .. what.paths) {
bind_leaf(old_fs, new_fs, path);
};
};
// pivot into the new rootfs
pivot_into("new");
// TODO: `chdir` back to the original dir we were executed from
// TODO: CLONE_NEWPID (might not work without forking to also become reaper)
};
// walk from root to `p`, creating any ancestors necessary and then binding the
// leaf from the old fs into the new fs.
//
// cases handled:
// - [x] `p` is already present in the new fs. no-op.
// - [x] ancestors of `p` are all ordinary directories in the old fs:
// corresponding directories will be created in the new fs.
// mountpoints are treated as directories for this case.
// - [ ] ancestors of `p` are symlinks, such that `p != realpath(p)`.
// corresponding symlinks will be created in the new fs, as well as
// exactly as many underlying directories necessary to bind `p`.
// - [ ] `p` itself is a symlink in the old fs, rather than a file/directory.
// an equivalent symlink will be created, and then its target will be
// bound as per the logic described above.
// - [x] `p` is not canonicalized. it will be canonicalized *before* any of the
// above processing.
//
// failure modes handled:
// - [x] path is too long => does not create the leaf *nor any ancestors*.
// - [x] canonical path points outside the fs (e.g. `..`, or `../new/proc`).
// does not create the leaf *nor any of its ancestors* at/after the `..`.
fn bind_leaf(old_fs: *fs::fs, new_fs: *fs::fs, user_path: str) void = {
log::printfln("namespace: permit path: {}", user_path);
// parse the path, which has the effect also of canonicalizing it.
// returns error if the path is too long.
// may point to `.` or `../`
let p = match (path::init(user_path)) {
case let e: path::error =>
// should happen only if the path is absurdly long. swallow it and return.
log::printfln("unable to bind {}: {}", user_path, path::strerror(e));
return;
case let other: path::buffer =>
yield other;
};
let it = path::iter(&p);
let cur_path = path::init()!;
let cur_strpath = "";
for (let comp => path::nextiter(&it)) {
if (comp == "..") {
log::printfln("not binding external path {} (of {})", cur_strpath, user_path);
return;
};
if (len(comp) >= 1 && strings::sub(comp, 0, 1) == "/") {
// dirfd doesn't do well will absolute paths.
comp = strings::sub(comp, 1, strings::end);
};
cur_strpath = match (path::push(&cur_path, comp)) {
case let e: path::error =>
log::printfln("unable to construct intermediate path for binding {} / {}: {}", cur_strpath, comp, path::strerror(e));
return;
case let other: str =>
yield other;
};
match (bind_component(old_fs, new_fs, cur_strpath)) {
case let e: fs::error =>
log::printfln("unable to copy intermediate path {} of {}: {}", cur_strpath, user_path, fs::strerror(e));
return;
case void => void;
};
};
// and now, perform the actual bind:
let old_path = p;
let new_path = p;
let old_strpath = match (path::prepend(&old_path, "old")) {
case let e: path::error =>
log::printfln("unable to construct old path for binding {}: {}", user_path, path::strerror(e));
return;
case let other: str =>
yield other;
};
let new_strpath = match (path::prepend(&new_path, "new")) {
case let e: path::error =>
log::printfln("unable to construct new path for binding {}: {}", user_path, path::strerror(e));
return;
case let other: str =>
yield other;
};
rtext::swallow_error("bind_leaf: mount", rtext::mount(old_strpath, new_strpath, "", rtext::MS_BIND | rtext::MS_REC, null));
};
fn bind_component(old_fs: *fs::fs, new_fs: *fs::fs, strpath: str) (void | fs::error) = {
match (fs::stat(new_fs, strpath)) {
case let e: fs::error => void;
case let other: fs::filestat => return; // already created
};
let st = fs::stat(old_fs, strpath)?;
// if (st.mode == mode_new) {
// return; // already created
// };
if (fs::islink(st.mode)) {
log::printfln("TODO: support symlinks ({})", strpath);
return;
} else if (fs::isdir(st.mode)) {
fs::mkdir(new_fs, strpath, st.mode)?;
} else {
// TODO: tune options (optional parameter; default is fs::flag::TRUNC)
fs::create(new_fs, strpath, st.mode)?;
};
};
// make `new_root` the new `/`, and optionally make the old root accessible
// at some directory (to be created) underneath it.
fn pivot_into(new_root: str, stash_old_root: (str|void) = void) void = {
log::printfln("namespace: pivot_root {}", new_root);
rtext::check_error("cd <new_root>", os::chdir(new_root));
match (stash_old_root) {
case let old: str =>

View File

@@ -13,3 +13,12 @@ export fn check_error(context: str, what: (void | fs::error | os::exec::error |
};
};
export fn swallow_error(context: str, what: (void | fs::error | os::exec::error | rt::errno)) void = {
match (what) {
case let e: fs::error => log::printfln("{}: {}", context, fs::strerror(e));
case let e: os::exec::error => log::printfln("{}: {}", context, os::exec::strerror(e));
case let e: rt::errno => log::printfln("{}: {}: {}", context, rt::errname(e), rt::strerror(e));
case => void;
};
};