From 27f1a9083c5a77ff265724424c0db89e4a6fcc71 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 16:53:24 -0400 Subject: [PATCH 1/4] feat(sandbox): compose native Linux plugin mounts and Landlock policy --- .github/workflows/plugin-native-launcher.yml | 61 ++++ crates/cyber-sandbox/src/plugin_policy.rs | 48 +++ .../cyber-sandbox/src/plugin_policy/linux.rs | 291 ++++++++++++++++ .../src/plugin_policy/linux/tests.rs | 160 +++++++++ .../cyber-sandbox/src/plugin_policy/tests.rs | 23 ++ .../tests/fixtures/plugin_path_mutation.c | 20 ++ .../tests/fixtures/plugin_proc_probe.c | 48 +++ .../tests/support/native_exec_linux.rs | 11 +- .../tests/support/native_policy_linux.rs | 320 ++++++++++++++++++ .../linux-policy-composition.md | 105 ++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- scripts/probes/plugin-native-entry/report.mts | 3 + 12 files changed, 1090 insertions(+), 2 deletions(-) create mode 100644 crates/cyber-sandbox/src/plugin_policy/linux.rs create mode 100644 crates/cyber-sandbox/src/plugin_policy/linux/tests.rs create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c create mode 100644 crates/cyber-sandbox/tests/support/native_policy_linux.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml index 519f3b51..ee207230 100644 --- a/.github/workflows/plugin-native-launcher.yml +++ b/.github/workflows/plugin-native-launcher.yml @@ -131,6 +131,66 @@ jobs: cp "$RUNNER_TEMP/native-policy-positive.rs" "$source" cargo test --locked -p cyber-sandbox --lib plugin_policy cargo test --locked -p cyber-sandbox --test native_exec + - name: Verify Linux composed policy controls + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/src/plugin_policy/linux.rs + cp "$source" "$RUNNER_TEMP/linux-policy-positive.rs" + trap 'cp "$RUNNER_TEMP/linux-policy-positive.rs" "$source"' EXIT + for control in ancestor immutable private read shell proc; do + cp "$RUNNER_TEMP/linux-policy-positive.rs" "$source" + python3 - "$source" "$control" <<'PY' + from pathlib import Path + import sys + path = Path(sys.argv[1]) + text = path.read_text() + control = sys.argv[2] + if control == 'ancestor': + before, after = 'for parent in anchors(p) {', 'for parent in anchors(p).into_iter().take(0) {' + elif control == 'immutable': + start = text.index(' let readonly: BTreeSet<_> = immutable(p).collect();') + end = text.index(' for mask in masks(p) {', start) + before, after = text[start:end], '' + elif control == 'private': + before, after = 'for mask in masks(p) {', 'for mask in masks(p).into_iter().take(0) {' + elif control == 'read': + before, after = 'if p.fs_read {', 'if true {' + elif control == 'shell': + before, after = 'if p.shell {', 'if true {' + else: + before = ' args.bind("--bind", p.scratch)?;' + after = ' args.add(["--ro-bind", "/proc", "/proc"])?;\n' + before + assert text.count(before) == 1 + path.write_text(text.replace(before, after)) + PY + if [[ "$control" == ancestor || "$control" == immutable ]]; then + name=rust_linux_policy_protects_ancestors_and_preserves_public_renames + elif [[ "$control" == proc ]]; then + name=rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads + else + name=rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + fi + if cargo test --locked -p cyber-sandbox --test native_exec "linux::policy::$name" -- --exact > "$RUNNER_TEMP/linux-policy-$control-control.log" 2>&1; then + cat "$RUNNER_TEMP/linux-policy-$control-control.log" + echo "Missing Linux $control safeguard unexpectedly passed" >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/linux-policy-$control-control.log" "$name" "$control" <<'PY' + from pathlib import Path + import re, sys + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert f'{sys.argv[2]} ... FAILED' in text + expected = {'ancestor': 'failed: protected ancestor rename:', 'immutable': 'failed: immutable package write', + 'private': 'failed: private_write:', 'read': 'failed: project_read:', 'shell': 'failed: shell:', + 'proc': 'failed: private proc visibility:'} + assert expected[sys.argv[3]] in text, text + PY + done + cp "$RUNNER_TEMP/linux-policy-positive.rs" "$source" + cargo test --locked -p cyber-sandbox --lib plugin_policy + cargo test --locked -p cyber-sandbox --test native_exec - name: Verify packaged entry across supported runtimes if: runner.os != 'Windows' shell: bash @@ -213,6 +273,7 @@ jobs: ${{ runner.temp }}/native-entry-provenance.json ${{ runner.temp }}/packaged-runtime/ ${{ runner.temp }}/native-fd-control.log + ${{ runner.temp }}/linux-policy-*-control.log ${{ runner.temp }}/native-policy-ancestor-control.log ${{ runner.temp }}/native-policy-immutable-control.log ${{ runner.temp }}/native-policy-private-control.log diff --git a/crates/cyber-sandbox/src/plugin_policy.rs b/crates/cyber-sandbox/src/plugin_policy.rs index 5882f6ea..7a717422 100644 --- a/crates/cyber-sandbox/src/plugin_policy.rs +++ b/crates/cyber-sandbox/src/plugin_policy.rs @@ -59,6 +59,54 @@ impl MacProfile<'_> { } } +/// Paths hidden in the Linux mount namespace. The owner attests their kinds. +#[derive(Debug, Clone, Copy)] +pub enum MaskPath<'a> { + File(&'a Path), + Directory(&'a Path), +} + +/// Explicit, owner-bound Linux inputs. Composition performs no filesystem IO. +/// The mask is a new owned mode-000 regular file, never a credential or device. +/// All referenced resources must remain protected until the child tree is reaped. +pub struct LinuxPolicy<'a> { + pub helper: &'a Path, + pub target: &'a Path, + pub profile: &'a Path, + pub package: &'a Path, + pub scratch: &'a Path, + pub location: &'a Path, + pub mask_file: &'a Path, + pub bootstrap: &'a [&'a Path], + pub private: &'a [MaskPath<'a>], + pub read_only: &'a [&'a Path], + pub target_args: &'a [String], + pub fs_read: bool, + pub fs_write: bool, + pub shell: bool, + /// Bun needs private procfs reads; never bind host procfs back over it. + pub namespace_proc: bool, +} + +/// Inputs to a reviewed bubblewrap executable, plus the bounded profile bytes the +/// owner must write at LinuxPolicy::profile before spawning. Not a launch permit. +#[derive(Debug, PartialEq, Eq)] +pub struct LinuxPlan { + pub arguments: Vec, + pub landlock: String, +} +impl LinuxPolicy<'_> { + pub fn compose(&self) -> Result { + #[cfg(target_os = "linux")] + return linux::compose(self); + #[cfg(not(target_os = "linux"))] + Err(Error::Unavailable) + } +} + +#[cfg(any(target_os = "linux", all(test, unix)))] +mod linux; + #[cfg(target_os = "macos")] mod macos { use super::*; diff --git a/crates/cyber-sandbox/src/plugin_policy/linux.rs b/crates/cyber-sandbox/src/plugin_policy/linux.rs new file mode 100644 index 00000000..a81f9993 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_policy/linux.rs @@ -0,0 +1,291 @@ +//! Linux-specific mount ordering and Landlock input; no native IO or execution. +use super::{Error, LinuxPlan, LinuxPolicy, MaskPath}; +use std::collections::BTreeSet; +use std::fmt::Write; +use std::path::{Component, Path}; + +const MAX_ROOTS: usize = 64; +const MAX_PROFILE: usize = 64 * 1024; +const MAX_ARGUMENTS: usize = 1024; +const MAX_ARGUMENT_BYTES: usize = 128 * 1024; + +fn mask_path<'a>(mask: &MaskPath<'a>) -> &'a Path { + match mask { + MaskPath::File(path) | MaskPath::Directory(path) => path, + } +} +fn path(path: &Path) -> Result<&str, Error> { + let text = path.to_str().ok_or(Error::Input)?; + if !path.is_absolute() + || path.parent().is_none() + || text.len() > 4096 + || text.chars().any(char::is_control) + || path.components().count() > 64 + || path + .components() + .any(|part| !matches!(part, Component::RootDir | Component::Normal(_))) + { + return Err(Error::Input); + } + Ok(text) +} +fn overlaps(a: &Path, b: &Path) -> bool { + a.starts_with(b) || b.starts_with(a) +} +fn pseudo(path: &Path) -> bool { + path.starts_with("/proc") || path.starts_with("/dev") +} +fn startup_resources<'a>(p: &'a LinuxPolicy<'a>) -> impl Iterator { + [p.helper, p.target, p.profile, p.package, p.mask_file] + .into_iter() + .chain(p.bootstrap.iter().copied()) +} +fn immutable<'a>(p: &'a LinuxPolicy<'a>) -> impl Iterator { + startup_resources(p).chain(p.read_only.iter().copied()) +} +fn validate_masks(masks: &[MaskPath<'_>]) -> Result<(), Error> { + for (index, a) in masks.iter().enumerate() { + for b in masks.iter().skip(index + 1) { + let (pa, pb) = (mask_path(a), mask_path(b)); + if pa == pb && std::mem::discriminant(a) != std::mem::discriminant(b) { + return Err(Error::Input); + } + if (matches!(a, MaskPath::File(_)) && pa != pb && pb.starts_with(pa)) + || (matches!(b, MaskPath::File(_)) && pa != pb && pa.starts_with(pb)) + { + return Err(Error::Overlap); + } + } + } + Ok(()) +} +fn validate(p: &LinuxPolicy<'_>) -> Result<(), Error> { + if p.bootstrap + .len() + .saturating_add(p.private.len()) + .saturating_add(p.read_only.len()) + .saturating_add(7) + > MAX_ROOTS + { + return Err(Error::Limit); + } + let roots = immutable(p) + .chain([p.location, p.scratch]) + .chain(p.private.iter().map(mask_path)); + let mut bytes = 0; + for root in roots { + bytes += path(root)?.len(); + if bytes > MAX_PROFILE { + return Err(Error::Limit); + } + if pseudo(root) { + return Err(Error::Input); + } + } + validate_masks(p.private)?; + if p.location.starts_with(p.scratch) { + return Err(Error::Overlap); + } + for root in immutable(p) { + if overlaps(root, p.scratch) || p.location.starts_with(root) { + return Err(Error::Overlap); + } + } + if startup_resources(p).any(|root| p.private.iter().any(|mask| overlaps(root, mask_path(mask)))) + { + return Err(Error::Overlap); + } + if p.private + .iter() + .any(|mask| overlaps(mask_path(mask), p.scratch) || p.location.starts_with(mask_path(mask))) + { + return Err(Error::Overlap); + } + let prefix = [ + "--plugin-native-exec", + path(p.profile)?, + "--initial-shell=0", + path(p.target)?, + ]; + let mut count = prefix.len(); + let mut bytes: usize = prefix.iter().map(|arg| arg.len() + 1).sum(); + if p.target_args.len() > 252 { + return Err(Error::Limit); + } + for arg in p.target_args { + if arg.len() > MAX_PROFILE { + return Err(Error::Limit); + } + if arg.contains('\0') { + return Err(Error::Input); + } + count += 1; + bytes = bytes.saturating_add(arg.len() + 1); + if count > 256 || bytes > MAX_PROFILE { + return Err(Error::Limit); + } + } + Ok(()) +} +#[derive(Default)] +struct Arguments { + values: Vec, + bytes: usize, +} +impl Arguments { + fn add<'a>(&mut self, parts: impl IntoIterator) -> Result<(), Error> { + for part in parts { + if self.values.len() >= MAX_ARGUMENTS + || self.bytes + part.len() + 1 > MAX_ARGUMENT_BYTES + { + return Err(Error::Limit); + } + self.bytes += part.len() + 1; + self.values.push(part.into()); + } + Ok(()) + } + fn bind(&mut self, mode: &str, root: &Path) -> Result<(), Error> { + self.add([mode, path(root)?, path(root)?]) + } + fn hide_directory(&mut self, root: &Path) -> Result<(), Error> { + self.add([ + "--perms", + "0000", + "--tmpfs", + path(root)?, + "--remount-ro", + path(root)?, + ]) + } +} +fn anchors<'a>(p: &'a LinuxPolicy<'a>) -> Vec<&'a Path> { + let mut set = BTreeSet::new(); + for root in immutable(p).chain(p.private.iter().map(mask_path)) { + for parent in root.ancestors().skip(1) { + if parent != p.location && parent.starts_with(p.location) { + set.insert(parent); + } + } + } + let mut paths: Vec<_> = set.into_iter().collect(); + paths.sort_by(|a, b| { + a.components() + .count() + .cmp(&b.components().count()) + .then_with(|| a.cmp(b)) + }); + paths +} +fn masks<'a>(p: &'a LinuxPolicy<'a>) -> Vec> { + let mut selected = Vec::new(); + for mask in p.private.iter().copied() { + let root = mask_path(&mask); + if p.private.iter().any(|other| matches!(other, MaskPath::Directory(parent) if root != *parent && root.starts_with(parent))) { + continue; + } + if !selected.iter().any(|other| mask_path(other) == root) { + selected.push(mask); + } + } + selected +} +fn mounts(p: &LinuxPolicy<'_>) -> Result, Error> { + let mut args = Arguments::default(); + args.add([ + "--die-with-parent", + "--unshare-user", + "--unshare-pid", + "--unshare-net", + "--unshare-ipc", + "--unshare-uts", + "--disable-userns", + "--cap-drop", + "ALL", + "--ro-bind", + "/", + "/", + "--proc", + "/proc", + "--dev", + "/dev", + ])?; + let mode = if p.fs_write { "--bind" } else { "--ro-bind" }; + args.bind(mode, p.location)?; + for parent in anchors(p) { + args.bind(mode, parent)?; + } + let readonly: BTreeSet<_> = immutable(p).collect(); + for root in readonly { + args.bind("--ro-bind", root)?; + } + for mask in masks(p) { + match mask { + MaskPath::Directory(root) => args.hide_directory(root)?, + MaskPath::File(root) => args.add(["--ro-bind", path(p.mask_file)?, path(root)?])?, + } + } + if p.namespace_proc { + for root in [Path::new("/proc/1"), Path::new("/proc/sys")] { + args.hide_directory(root)?; + } + } + args.bind("--bind", p.scratch)?; + args.add([ + "--chdir", + path(p.package)?, + "--", + path(p.helper)?, + "--plugin-native-exec", + path(p.profile)?, + if p.shell { + "--initial-shell=1" + } else { + "--initial-shell=0" + }, + path(p.target)?, + ])?; + args.add(p.target_args.iter().map(String::as_str))?; + Ok(args.values) +} +fn grant(text: &mut String, right: char, root: &Path) -> Result<(), Error> { + let root = path(root)?; + if text.len() + root.len() + 3 > MAX_PROFILE { + return Err(Error::Limit); + } + writeln!(text, "{right}\t{root}").map_err(|_| Error::Limit) +} +fn landlock(p: &LinuxPolicy<'_>) -> Result { + let mut text = String::new(); + for root in [p.target, p.package, p.scratch] + .into_iter() + .chain(p.bootstrap.iter().copied()) + { + grant(&mut text, 'R', root)?; + } + for root in ["/dev/null", "/dev/urandom", "/dev/random"] { + grant(&mut text, 'R', Path::new(root))?; + } + grant(&mut text, 'W', Path::new("/dev/null"))?; + grant(&mut text, 'W', p.scratch)?; + if p.fs_read { + grant(&mut text, 'R', p.location)?; + } + if p.fs_write { + grant(&mut text, 'W', p.location)?; + } + if p.namespace_proc { + grant(&mut text, 'R', Path::new("/proc"))?; + } + Ok(text) +} +pub(super) fn compose(p: &LinuxPolicy<'_>) -> Result { + validate(p)?; + Ok(LinuxPlan { + arguments: mounts(p)?, + landlock: landlock(p)?, + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs b/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs new file mode 100644 index 00000000..744c86f6 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs @@ -0,0 +1,160 @@ +use super::*; +fn policy<'a>(private: &'a [MaskPath<'a>]) -> LinuxPolicy<'a> { + LinuxPolicy { + helper: Path::new("/owned/helper"), + target: Path::new("/project/plugins/level/package/main"), + profile: Path::new("/owned/profile"), + package: Path::new("/project/plugins/level/package"), + scratch: Path::new("/owned/scratch"), + location: Path::new("/project"), + mask_file: Path::new("/owned/empty"), + bootstrap: &[], + private, + read_only: &[], + target_args: &[], + fs_read: false, + fs_write: true, + shell: false, + namespace_proc: false, + } +} +fn position(args: &[String], group: &[&str]) -> usize { + args.windows(group.len()) + .position(|parts| parts.iter().map(String::as_str).eq(group.iter().copied())) + .unwrap() +} +#[test] +fn linux_layers_order_anchors_immutable_mounts_masks_and_scratch() { + let private = [ + MaskPath::Directory(Path::new("/project/private/level/state")), + MaskPath::File(Path::new("/project/keys/level/key")), + ]; + let plan = compose(&policy(&private)).unwrap(); + let anchor = position( + &plan.arguments, + &["--bind", "/project/private", "/project/private"], + ); + let deep = position( + &plan.arguments, + &["--bind", "/project/private/level", "/project/private/level"], + ); + let immutable = position( + &plan.arguments, + &[ + "--ro-bind", + "/project/plugins/level/package", + "/project/plugins/level/package", + ], + ); + let mask = position( + &plan.arguments, + &["--tmpfs", "/project/private/level/state"], + ); + let scratch = position( + &plan.arguments, + &["--bind", "/owned/scratch", "/owned/scratch"], + ); + assert!(anchor < deep && deep < immutable && immutable < mask && mask < scratch); + assert!(!plan.landlock.contains("R\t/project\n")); + assert!(plan.landlock.contains("W\t/project\n")); + assert!(plan.landlock.contains("R\t/owned/scratch\n")); + assert!(plan.arguments.contains(&"--initial-shell=0".into())); +} +#[test] +fn linux_proc_is_fresh_and_never_replaced_by_a_host_bind() { + let mut p = policy(&[]); + p.namespace_proc = true; + let plan = compose(&p).unwrap(); + position(&plan.arguments, &["--proc", "/proc"]); + position(&plan.arguments, &["--tmpfs", "/proc/1"]); + position(&plan.arguments, &["--tmpfs", "/proc/sys"]); + assert!( + !plan + .arguments + .windows(3) + .any(|p| p == ["--ro-bind", "/proc", "/proc"]) + ); + assert!(plan.landlock.contains("R\t/proc\n")); + let bootstrap = [Path::new("/proc")]; + p.bootstrap = &bootstrap; + assert_eq!(compose(&p), Err(Error::Input)); +} +#[test] +fn linux_inputs_refuse_unsafe_overlap_injection_and_argument_overflow() { + let mut p = policy(&[]); + for target in [ + "/", + "relative", + "/owned/../main", + "/owned/main\nW\t/project", + ] { + p.target = Path::new(target); + assert_eq!(compose(&p), Err(Error::Input)); + } + p.target = Path::new("/project/plugins/level/package/main"); + p.scratch = p.package; + assert_eq!(compose(&p), Err(Error::Overlap)); + p.scratch = Path::new("/owned/scratch"); + let args = vec!["argument".to_owned(); 252]; + p.target_args = &args; + assert!(compose(&p).is_ok()); + let args = vec!["argument".to_owned(); 253]; + p.target_args = &args; + assert_eq!(compose(&p), Err(Error::Limit)); +} + +#[test] +fn linux_public_composition_is_unavailable_on_this_host() { + assert_eq!(policy(&[]).compose(), Err(Error::Unavailable)); +} + +#[test] +fn linux_private_parents_cover_children_without_inner_mount_creation() { + let masks = [ + MaskPath::File(Path::new("/project/private/child/secret")), + MaskPath::Directory(Path::new("/project/private")), + MaskPath::Directory(Path::new("/project/private/child")), + ]; + let plan = compose(&policy(&masks)).unwrap(); + assert_eq!( + plan.arguments + .iter() + .filter(|arg| arg.as_str() == "--tmpfs") + .count(), + 1 + ); + assert!( + !plan + .arguments + .contains(&"/project/private/child/secret".into()) + ); +} + +#[test] +fn linux_conflicting_private_kinds_and_file_ancestors_are_refused() { + let masks = [ + MaskPath::File(Path::new("/project/private")), + MaskPath::Directory(Path::new("/project/private")), + ]; + assert_eq!(compose(&policy(&masks)), Err(Error::Input)); + let masks = [ + MaskPath::File(Path::new("/project/private")), + MaskPath::File(Path::new("/project/private/child")), + ]; + assert_eq!(compose(&policy(&masks)), Err(Error::Overlap)); +} + +#[test] +fn linux_private_masks_can_cover_readonly_configuration_descendants() { + let masks = [MaskPath::Directory(Path::new("/project/config/private"))]; + let mut p = policy(&masks); + let readonly = [Path::new("/project/config")]; + p.read_only = &readonly; + let plan = compose(&p).unwrap(); + assert!( + position( + &plan.arguments, + &["--ro-bind", "/project/config", "/project/config"] + ) < position(&plan.arguments, &["--tmpfs", "/project/config/private"]) + ); +} diff --git a/crates/cyber-sandbox/src/plugin_policy/tests.rs b/crates/cyber-sandbox/src/plugin_policy/tests.rs index 5170423e..d543a319 100644 --- a/crates/cyber-sandbox/src/plugin_policy/tests.rs +++ b/crates/cyber-sandbox/src/plugin_policy/tests.rs @@ -83,3 +83,26 @@ fn native_profile_accepts_root_limit_and_refuses_rendered_policy_overflow() { plan.bootstrap = &bootstrap; assert_eq!(plan.render(), Err(Error::Limit)); } + +#[cfg(not(target_os = "linux"))] +#[test] +fn linux_public_composition_refuses_unsupported_hosts() { + let plan = LinuxPolicy { + helper: Path::new("/owned/helper"), + target: Path::new("/owned/main"), + profile: Path::new("/owned/profile"), + package: Path::new("/owned/package"), + scratch: Path::new("/owned/scratch"), + location: Path::new("/project"), + mask_file: Path::new("/owned/mask"), + bootstrap: &[], + private: &[], + read_only: &[], + target_args: &[], + fs_read: false, + fs_write: false, + shell: false, + namespace_proc: false, + }; + assert_eq!(plan.compose(), Err(Error::Unavailable)); +} diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c b/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c new file mode 100644 index 00000000..d9cdce87 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c @@ -0,0 +1,20 @@ +/* Fixed native syscall fixture. All paths refer to newly owned test data. */ +#include +#include +#include +#include +#include +int main(int argc, char **argv) { + if (argc != 4) return 2; + int result; + if (strcmp(argv[1], "rename") == 0) result = rename(argv[2], argv[3]); + else if (strcmp(argv[1], "write") == 0) { + int fd = open(argv[2], O_WRONLY | O_TRUNC); + result = fd < 0 ? -1 : (write(fd, "changed-fixture", 15) == 15 ? 0 : -1); + int saved = errno; + if (fd >= 0) close(fd); + errno = saved; + } else return 2; + printf("{\"ok\":%s,\"errno\":%d}\n", result == 0 ? "true" : "false", result == 0 ? 0 : errno); + return 0; +} diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c b/crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c new file mode 100644 index 00000000..6c2fdc93 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c @@ -0,0 +1,48 @@ +/* Native private-procfs and proc-root alias receipts. No policy cooperation. */ +#include +#include +#include +#include +#include +#include +static const char *truth(int value) { return value ? "true" : "false"; } +static int readable(const char *path) { + int fd = open(path, O_RDONLY); + char byte; + int result = fd >= 0 && read(fd, &byte, 1) == 1; + if (fd >= 0) close(fd); + return result; +} +static int alias(const char *path) { + char buffer[PATH_MAX]; + int size = snprintf(buffer, sizeof(buffer), "/proc/self/root%s", path); + return size > 0 && (size_t)size < sizeof(buffer) && readable(buffer); +} +static int private_pid_view(void) { + FILE *file = fopen("/proc/self/status", "r"); + if (file == NULL) return 0; + char line[4096]; + int count = 0; + while (fgets(line, sizeof(line), file) != NULL) { + if (strncmp(line, "NSpid:", 6) != 0) continue; + char *current = line + 6; + while (*current != '\0' && *current != '\n') { + char *end; + (void)strtoul(current, &end, 10); + if (end == current) break; + count++; + current = end; + } + break; + } + fclose(file); + return count == 1; +} +int main(int argc, char **argv) { + if (argc != 4) return 2; + printf("{\"self\":%s,\"init\":%s,\"sys\":%s,\"private_pid_view\":%s,\"project_alias\":%s,\"private_alias\":%s,\"scratch_alias\":%s}\n", + truth(readable("/proc/self/status")), truth(readable("/proc/1/status")), + truth(readable("/proc/sys/kernel/pid_max")), truth(private_pid_view()), + truth(alias(argv[1])), truth(alias(argv[2])), truth(alias(argv[3]))); + return 0; +} diff --git a/crates/cyber-sandbox/tests/support/native_exec_linux.rs b/crates/cyber-sandbox/tests/support/native_exec_linux.rs index 891f2ac1..e3975fa6 100644 --- a/crates/cyber-sandbox/tests/support/native_exec_linux.rs +++ b/crates/cyber-sandbox/tests/support/native_exec_linux.rs @@ -15,7 +15,13 @@ fn output(command: &mut Command, input: Option<&[u8]>) -> Output { .spawn() .unwrap(); if let Some(input) = input { - child.stdin.take().unwrap().write_all(input).unwrap(); + if let Err(error) = child.stdin.take().unwrap().write_all(input) + && error.kind() != std::io::ErrorKind::BrokenPipe + { + let _ = child.kill(); + let _ = child.wait(); + panic!("native fixture input failed: {error}"); + } } drop(child.stdin.take()); let deadline = Instant::now() + Duration::from_secs(15); @@ -301,3 +307,6 @@ fn invalid_profiles_and_missing_shell_policy_refuse_before_target() { assert_eq!(result.status.code(), Some(64)); assert!(result.stdout.is_empty()); } + +#[path = "native_policy_linux.rs"] +mod policy; diff --git a/crates/cyber-sandbox/tests/support/native_policy_linux.rs b/crates/cyber-sandbox/tests/support/native_policy_linux.rs new file mode 100644 index 00000000..07d06135 --- /dev/null +++ b/crates/cyber-sandbox/tests/support/native_policy_linux.rs @@ -0,0 +1,320 @@ +//! Actual packaged helper with the production mount and Landlock composer. +use super::*; +use cyber_sandbox::plugin_policy::{LinuxPolicy, MaskPath}; +use std::os::unix::fs::PermissionsExt; + +struct Composed { + fixture: Fixture, + project: PathBuf, + package: PathBuf, + target: PathBuf, + private_dir: PathBuf, + private_file: PathBuf, + mask: PathBuf, + bootstrap: Vec, + namespace_proc: bool, +} +impl Composed { + fn new() -> Self { + let fixture = Fixture::new(); + let project = fixture.root.join("project \"quote\" \\ workspace"); + let package = project.join("extensions/level/package"); + let private_dir = project.join("owner/level/state"); + let private_file = project.join("keys/level/secret"); + for directory in [ + package.as_path(), + private_dir.as_path(), + private_file.parent().unwrap(), + ] { + std::fs::create_dir_all(directory).unwrap(); + } + for file in [ + package.join("data"), + project.join("data"), + private_dir.join("secret"), + private_file.clone(), + ] { + std::fs::write(file, b"owned-fixture").unwrap(); + } + symlink(&private_file, package.join("alias")).unwrap(); + let target = package.join("main"); + std::fs::copy(&fixture.payload, &target).unwrap(); + let mask = fixture.root.join("empty-mask"); + std::fs::write(&mask, b"").unwrap(); + std::fs::set_permissions(&mask, std::fs::Permissions::from_mode(0o000)).unwrap(); + let bootstrap = ["/usr", "/lib", "/lib64", "/bin", "/etc/ld.so.cache"] + .into_iter() + .filter(|path| Path::new(path).exists()) + .map(|path| std::fs::canonicalize(path).unwrap()) + .collect(); + Self { + fixture, + project, + package, + target, + private_dir, + private_file, + mask, + bootstrap, + namespace_proc: false, + } + } + fn invoke( + &self, + target: &Path, + args: &[String], + read: bool, + write: bool, + shell: bool, + ) -> Output { + let helper = Path::new(env!("CARGO_BIN_EXE_cyber-sandbox-exec")); + let profile = self.fixture.root.join("composed-profile"); + let scratch = self.fixture.root.join("scratch"); + let bootstrap: Vec<_> = self.bootstrap.iter().map(PathBuf::as_path).collect(); + let masks = [ + MaskPath::Directory(&self.private_dir), + MaskPath::File(&self.private_file), + ]; + let policy = LinuxPolicy { + helper, + target, + profile: &profile, + package: &self.package, + scratch: &scratch, + location: &self.project, + mask_file: &self.mask, + bootstrap: &bootstrap, + private: &masks, + read_only: &[], + target_args: args, + fs_read: read, + fs_write: write, + shell, + namespace_proc: self.namespace_proc, + }; + let plan = policy.compose().unwrap(); + std::fs::write(&profile, &plan.landlock).unwrap(); + let mut command = Command::new("/usr/bin/bwrap"); + command + .args(&plan.arguments) + .env_clear() + .env("PATH", "/usr/bin:/bin") + .env("CYBER_PROOF_PRIVATE", &self.private_file); + success(&mut command, Some(b"{\"jsonrpc\":\"2.0\",\"id\":\"native-proof\",\"method\":\"tool/execute\",\"params\":{\"name\":\"native-probe\",\"input\":{}}}\n")) + } + fn inspect(&self, read: bool, write: bool, shell: bool) -> Value { + let scratch = self.fixture.root.join("scratch"); + let mut args = vec!["inspect".into()]; + for path in [ + self.package.join("data"), + self.project.join("data"), + self.private_file.clone(), + scratch.join("roundtrip"), + self.project.join("written"), + self.package.join("alias"), + self.private_dir.join("secret"), + scratch, + self.project.clone(), + ] { + args.push(path.to_str().unwrap().into()); + } + args.extend(["0".into(), "0".into(), "1".into()]); + let result = self.invoke(&self.target, &args, read, write, shell); + let packet: Value = serde_json::from_slice(&result.stdout).unwrap(); + assert_eq!(packet["jsonrpc"], "2.0"); + assert_eq!(packet["id"], "native-proof"); + assert!(packet.get("error").is_none()); + packet["result"].clone() + } +} +fn capabilities(value: Value, read: bool, write: bool, shell: bool) { + for key in [ + "stdio_tool", + "package_read", + "thread", + "scratch_write", + "scratch_read", + "cannot_relax", + "filter_replacement_denied", + ] { + assert_eq!(value[key], true, "{key}: {value}"); + } + for key in [ + "private_read", + "private_list", + "private_write", + "protected_read", + "protected_write", + "symlink_private_read", + "hardlink_private", + "package_hardlink", + "package_alias_write", + "inherited_descriptor", + "constructor_private_read", + "project_rename", + ] { + assert_eq!(value[key], false, "{key}: {value}"); + } + assert_eq!(value["project_read"], read, "project_read: {value}"); + assert_eq!(value["project_list"], read, "{value}"); + assert_eq!(value["project_write"], write, "{value}"); + assert_eq!(value["written_project_read"], read && write, "{value}"); + assert_eq!( + value["thread_reads"], + serde_json::json!([read, false]), + "{value}" + ); + for key in [ + "shell", + "native_fork", + "native_spawn", + "constructor_fork", + "thread_shell", + ] { + assert_eq!(value[key], shell, "{key}: {value}"); + } + let children = if shell { + serde_json::json!([read, false]) + } else { + Value::Null + }; + assert_eq!(value["child_reads"], children, "{value}"); + assert_eq!(value["native_child_reads"], children, "{value}"); +} +#[test] +fn rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry() { + let f = Composed::new(); + for (read, write, shell) in [ + (false, false, false), + (false, false, true), + (false, true, false), + (false, true, true), + (true, false, false), + (true, false, true), + (true, true, false), + (true, true, true), + ] { + capabilities(f.inspect(read, write, shell), read, write, shell); + assert_eq!(std::fs::read(&f.private_file).unwrap(), b"owned-fixture"); + assert_eq!( + std::fs::read(f.private_dir.join("secret")).unwrap(), + b"owned-fixture" + ); + assert_eq!( + std::fs::read(f.package.join("data")).unwrap(), + b"owned-fixture" + ); + let written = f.project.join("written"); + if written.exists() { + std::fs::remove_file(written).unwrap(); + } + } +} + +fn mutation( + f: &Composed, + target: &Path, + operation: &str, + source: &Path, + destination: &Path, +) -> Value { + let args = [ + operation.into(), + source.to_str().unwrap().into(), + destination.to_str().unwrap().into(), + ]; + let result = f.invoke(target, &args, true, true, false); + serde_json::from_slice(&result.stdout).unwrap() +} +#[test] +fn rust_linux_policy_protects_ancestors_and_preserves_public_renames() { + let f = Composed::new(); + let target = f.package.join("mutation"); + success( + Command::new("cc") + .args(["-Wall", "-Wextra", "-Werror"]) + .arg( + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/plugin_path_mutation.c"), + ) + .arg("-o") + .arg(&target), + None, + ); + for root in [&f.package, &f.private_dir, &f.private_file] { + let parent = root.parent().unwrap(); + for source in [parent, parent.parent().unwrap()] { + let observed = mutation( + &f, + &target, + "rename", + source, + &source.with_extension("moved"), + ); + assert_eq!( + observed, + serde_json::json!({"ok":false,"errno":16}), + "protected ancestor rename: {source:?}" + ); + let sibling = source.join("public-sibling"); + std::fs::write(&sibling, b"public-fixture").unwrap(); + assert_eq!( + mutation(&f, &target, "write", &sibling, &sibling), + serde_json::json!({"ok":true,"errno":0}) + ); + } + } + let data = f.package.join("data"); + assert_eq!( + mutation(&f, &target, "write", &data, &data), + serde_json::json!({"ok":false,"errno":30}), + "immutable package write" + ); + let public = f.project.join("public"); + std::fs::create_dir(&public).unwrap(); + assert_eq!( + mutation( + &f, + &target, + "rename", + &public, + &public.with_extension("moved") + ), + serde_json::json!({"ok":true,"errno":0}) + ); + assert_eq!(std::fs::read(&f.private_file).unwrap(), b"owned-fixture"); + assert_eq!( + std::fs::read(f.private_dir.join("secret")).unwrap(), + b"owned-fixture" + ); +} + +#[test] +fn rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads() { + let mut f = Composed::new(); + f.namespace_proc = true; + let target = f.package.join("proc-probe"); + success( + Command::new("cc") + .args(["-Wall", "-Wextra", "-Werror"]) + .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/plugin_proc_probe.c")) + .arg("-o") + .arg(&target), + None, + ); + let scratch = f.fixture.root.join("scratch/proc-data"); + std::fs::write(&scratch, b"scratch-fixture").unwrap(); + let args: Vec = [f.project.join("data"), f.private_file.clone(), scratch] + .iter() + .map(|path| path.to_str().unwrap().into()) + .collect(); + for read in [false, true] { + let output = f.invoke(&target, &args, read, false, false); + let observed: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!( + observed, + serde_json::json!({"self":true,"init":false,"sys":false, + "private_pid_view":true,"project_alias":read,"private_alias":false,"scratch_alias":true}), + "private proc visibility: {observed}" + ); + } +} diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md new file mode 100644 index 00000000..ac9739f6 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md @@ -0,0 +1,105 @@ +# Composed Linux plugin policy + +`cyber-sandbox::plugin_policy::LinuxPolicy` now constructs the native helper's +Landlock profile and its bubblewrap arguments together. This promotes the +[verified mount primitives](linux-mount-protection.md) into a production library +component. It does not activate the production host or supply an execution permit. + +The owner supplies canonical, bound helper/target/profile/package/scratch paths, +explicit reviewed bootstrap roots, private file/directory masks, read-only roots, +Location, target arguments and independent read/write/shell choices. The mask +source must be a new owned, singly linked, mode-000 regular file protected until +process settlement. Composition does no filesystem IO and cannot attest any of +those native facts. It never discovers executables through PATH. + +The plan requires private user/PID/network/IPC/UTS namespaces, drops capabilities, +disables nested user namespaces and installs fresh procfs and minimal devices. +Location and every protected ancestor inside it are mounted first. Immutable +resources are rebound read-only next, followed by private masks and writable +scratch. Nested private roots collapse beneath an opaque private directory so no +later mount tries to create a child inside a sealed parent. Conflicting declared +kinds and private file ancestors refuse composition. + +Landlock grants package, target, explicit bootstrap and scratch reads. Only +scratch and /dev/null get unconditional writes; Location reads and writes are +independent. Shell reaches the packaged entry as its mandatory initial-shell flag, +not as an unfiltered fallback. The existing helper installs Landlock and its +one-shot process boundary before the runtime's first instruction. Host CWD is +the package directory, consistent with the startup contract. + +Optional procfs reads refer only to the fresh private procfs. /proc/1 and /proc/sys +are masked. Host bootstrap paths beneath /proc or /dev refuse input rather than +being rebound over the new namespace objects. Only the explicit private-procfs +option and fixed null/entropy devices add those reads. + +## Bounds and refusals + +At most 64 supplied paths and 64 KiB of path text are accepted, with 4,096 bytes +and 64 components per path. Relative/root/parent-traversing/control-character or +non-UTF-8 paths refuse. Scratch cannot overlap immutable/private roots or contain +Location. The target's internal helper arguments fit its 256-argument/64-KiB +frame; generated bubblewrap arguments are bounded to 1,024 entries and 128 KiB. +The profile is capped at 64 KiB while rendering. Errors are static and do not echo +private paths or argument values. + +Public composition returns unavailable on non-Linux hosts. The pure backend is +also compiled in Unix unit tests so local macOS can exercise serialization and +bounds without claiming native Linux execution. This changes no ordinary command +sandbox or App host behavior. + +## Verification status + +On local macOS arm64, all 11 policy unit tests pass, including the pure Linux +backend and explicit public unavailability on this host. All eight existing +packaged macOS entry regressions pass. Sandbox all-target Clippy passes with Rust +warnings denied; the C compiler reports two deprecated Seatbelt SDK calls, without +assigning their origin. Both new C fixtures pass syntax-only compilation. Rust +formatting, provenance-script TypeScript syntax, workflow YAML/all seven Bash +blocks, diff checks, all 107 strict OpenSpec items and spec lint (zero errors, +25 warnings) pass. Linux-specific test compilation and kernel execution cannot +run on this macOS host and remain required in CI. +Linux CI must execute the three new actual-helper regressions: + +- Eight read/write/shell combinations, including constructor-time policy, + threads, child inheritance, real JSON-RPC receipt, private file/directory masks, + symlink/hard-link attempts and unchanged owner fixture bytes. +- Private/package parent and grandparent renames, immutable package writes, + allowed public sibling writes and public directory rename. +- Private procfs visibility and /proc/self/root aliases with Location read both + disabled and enabled, without exposing init/sys or restoring private reads. + +Six source-mutation controls must fail their named native assertions, followed by +restored unit/native suites. Their execution is configured, not locally passed: +ancestor binds, immutable binds, private masks, read capability, shell capability +and host procfs re-exposure. Source provenance includes the builder and fixtures. + +## Remaining native admission and intersections + +All source/runtime/helper/mask/ancestor ownership, all-alias coverage, current +plugin/digest/Location generation binding, serialized start, revocation and child +settlement remain caller obligations. The owner must supply every configuration, +protected-root and trust ceiling; these are not derived by the composer. + +A private IP namespace is not acceptance of the complete network capability. +Pathname UNIX-socket/host-IPC isolation, proxy transport and secret intersections +still require dedicated native verification and implementation before activation. +The kernel documents distinct [network-namespace scopes](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) +and [Landlock rights/scopes](https://docs.kernel.org/userspace-api/landlock.html); +this increment neither changes the helper's ABI floor nor certifies those missing +intersections. No weaker support decision is substituted for them. + +The App still uses its legacy sandbox. Reviewed runtime resolution, immutable +resource admission, complete native runtime/platform acceptance and explicit +support/design review remain open. No parent checkbox is closed, and protocol +features or the later Daily coding/binary workstreams are not declared complete. + + +The scoped implementation review checked every changed composer, unit/native +fixture, workflow/control and provenance input. Paths and target argument lengths +are bounded before copying or pairwise analysis; output is bounded incrementally. +The builder performs no owner filesystem IO. Mount ordering preserves writable +siblings while sealing immutable/private subtrees, and no host procfs/devices are +rebound over the new namespace objects. Positive native assertions, exact failing +control reasons and restored checks are configured rather than waived. Review +found no remaining local source issue within this component; native results are +still pending and production activation remains blocked by the obligations above. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 58cc8213..10a1fe2c 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -8,7 +8,7 @@ ## 2. Native capability enforcement - [ ] 2.1 Prove a native enforcement mechanism and document runtime bootstrap, read roots, writes without implied broad reads, threads, exec/fork/descendant behavior and OS/runtime support. Present any required design revision before integration. Native CI at `59cae487` passed ten jobs (90 cases) covering Node/Python on macOS/Linux arm64/x86_64 and native Linux executables, including pre-main reads/forks and immutable one-shot exec filtering. Evidence and remaining runtime/platform/integration gates: [native-proof.md](native-proof.md); parent task stays open pending the complete support decisions and acceptance evidence. -- [ ] 2.2 Implement plugin-specific confinement, reviewed runtime resolution and descriptor/environment isolation without weakening ordinary tools or protected roots. The content-bound helper copy foundation is documented in [image-preparation.md](image-preparation.md), with packaged macOS entry progress in [packaged-native-entry.md](packaged-native-entry.md) and Linux entry progress in [packaged-linux-entry.md](packaged-linux-entry.md); [Generation-bound final commit](launch-authority.md) adds synchronous revocation/start ordering, and [host bootstrap environment](bootstrap-environment.md) replaces implicit environment inheritance; [Runtime startup isolation](runtime-startup.md) adds package CWD/config selection and prepared workspace resources; [Private-root inventory](private-root-inventory.md) adds bounded metadata-only alias detection; [Native policy composition](native-policy-composition.md) adds the bounded Rust macOS filesystem/process builder and packaged-helper receipts; [Linux protected mount ancestry](linux-mount-protection.md) records five passing primitive cases per Linux architecture, including actual ancestor-move counterexamples and regular-file masks, before production Linux composition; complete owner admission and production native launch integration remain open. +- [ ] 2.2 Implement plugin-specific confinement, reviewed runtime resolution and descriptor/environment isolation without weakening ordinary tools or protected roots. The content-bound helper copy foundation is documented in [image-preparation.md](image-preparation.md), with packaged macOS entry progress in [packaged-native-entry.md](packaged-native-entry.md) and Linux entry progress in [packaged-linux-entry.md](packaged-linux-entry.md); [Generation-bound final commit](launch-authority.md) adds synchronous revocation/start ordering, and [host bootstrap environment](bootstrap-environment.md) replaces implicit environment inheritance; [Runtime startup isolation](runtime-startup.md) adds package CWD/config selection and prepared workspace resources; [Private-root inventory](private-root-inventory.md) adds bounded metadata-only alias detection; [Native policy composition](native-policy-composition.md) adds the bounded Rust macOS filesystem/process builder and packaged-helper receipts; [Linux protected mount ancestry](linux-mount-protection.md) records five passing primitive cases per Linux architecture, including actual ancestor-move counterexamples and regular-file masks, before [Rust Linux composition](linux-policy-composition.md), which now supplies bounded arguments/profile and actual-helper regressions pending hosted execution; complete owner admission and production native launch integration remain open. - [ ] 2.3 Enforce `fs.read`, `fs.write`, `shell`, network and secret intersections; refuse unsupported confinement with actionable plugin diagnostics. - [ ] 2.4 Add real-process positive/negative tests for native reads, home/Location/symlink bypass, spawning a shell/interpreter/native child, inherited restrictions, protected state, revocation and unsupported hosts. Obtain native macOS/Linux evidence and explicit Windows results/limits. diff --git a/scripts/probes/plugin-native-entry/report.mts b/scripts/probes/plugin-native-entry/report.mts index 3542aebb..7dc555cb 100644 --- a/scripts/probes/plugin-native-entry/report.mts +++ b/scripts/probes/plugin-native-entry/report.mts @@ -15,6 +15,9 @@ const sources = [ "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", "crates/cyber-sandbox/tests/native_exec.rs", "crates/cyber-sandbox/src/lib.rs", "crates/cyber-sandbox/src/plugin_policy.rs", "crates/cyber-sandbox/src/plugin_policy/tests.rs", "crates/cyber-sandbox/tests/support/native_policy_macos.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", "crates/cyber-sandbox/src/plugin_policy/linux/tests.rs", + "crates/cyber-sandbox/tests/support/native_policy_linux.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c", "crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c", "crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c", "scripts/probes/plugin-confinement/fixture_exec.c", "scripts/probes/plugin-confinement/fixture_native.c", "scripts/probes/plugin-confinement/process_policy.h", "scripts/probes/plugin-native-entry/report.mts", From b4822f2e9ce2c178fb123bb8c3cdfc0c6f873b04 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 16:59:30 -0400 Subject: [PATCH 2/4] test(sandbox): scope unavailable policy assertions to non-Linux hosts --- .../cyber-sandbox/src/plugin_policy/linux/tests.rs | 5 ----- .../linux-policy-composition.md | 13 ++++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs b/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs index 744c86f6..09ea6338 100644 --- a/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs +++ b/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs @@ -103,11 +103,6 @@ fn linux_inputs_refuse_unsafe_overlap_injection_and_argument_overflow() { assert_eq!(compose(&p), Err(Error::Limit)); } -#[test] -fn linux_public_composition_is_unavailable_on_this_host() { - assert_eq!(policy(&[]).compose(), Err(Error::Unavailable)); -} - #[test] fn linux_private_parents_cover_children_without_inner_mount_creation() { let masks = [ diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md index ac9739f6..7bae093f 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md @@ -49,7 +49,7 @@ sandbox or App host behavior. ## Verification status -On local macOS arm64, all 11 policy unit tests pass, including the pure Linux +On local macOS arm64, all 10 policy unit tests pass, including the pure Linux backend and explicit public unavailability on this host. All eight existing packaged macOS entry regressions pass. Sandbox all-target Clippy passes with Rust warnings denied; the C compiler reports two deprecated Seatbelt SDK calls, without @@ -103,3 +103,14 @@ rebound over the new namespace objects. Positive native assertions, exact failin control reasons and restored checks are configured rather than waived. Review found no remaining local source issue within this component; native results are still pending and production activation remains blocked by the obligations above. + + +## Initial hosted test-scope correction + +At `27f1a908`, both Linux packaged jobs failed a duplicate unsupported-host unit +assertion. The intended non-Linux check already lives in the common test module +behind `cfg(not(target_os = "linux"))`; an old unguarded copy remained in the +backend tests and incorrectly expected unavailable on Linux itself. That duplicate +is removed. The jobs stopped before the new native integration/controls, so none +of those native checks is counted as passing for that revision. Fresh hosted +execution remains required after this test-only correction. From 546641f97c0ef4dc539b017ad613397bbc250452 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:03:58 -0400 Subject: [PATCH 3/4] test(sandbox): distinguish private file masks from directory masks --- .../tests/fixtures/plugin_path_mutation.c | 8 ++++++++ .../tests/support/native_policy_linux.rs | 12 +++++++++++- .../linux-policy-composition.md | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c b/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c index d9cdce87..25a7feb1 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c @@ -8,6 +8,14 @@ int main(int argc, char **argv) { if (argc != 4) return 2; int result; if (strcmp(argv[1], "rename") == 0) result = rename(argv[2], argv[3]); + else if (strcmp(argv[1], "read") == 0) { + char byte; + int fd = open(argv[2], O_RDONLY); + result = fd < 0 ? -1 : (read(fd, &byte, 1) == 1 ? 0 : -1); + int saved = errno; + if (fd >= 0) close(fd); + errno = saved; + } else if (strcmp(argv[1], "write") == 0) { int fd = open(argv[2], O_WRONLY | O_TRUNC); result = fd < 0 ? -1 : (write(fd, "changed-fixture", 15) == 15 ? 0 : -1); diff --git a/crates/cyber-sandbox/tests/support/native_policy_linux.rs b/crates/cyber-sandbox/tests/support/native_policy_linux.rs index 07d06135..2050ec5b 100644 --- a/crates/cyber-sandbox/tests/support/native_policy_linux.rs +++ b/crates/cyber-sandbox/tests/support/native_policy_linux.rs @@ -108,7 +108,7 @@ impl Composed { for path in [ self.package.join("data"), self.project.join("data"), - self.private_file.clone(), + self.private_dir.join("secret"), scratch.join("roundtrip"), self.project.join("written"), self.package.join("alias"), @@ -263,6 +263,16 @@ fn rust_linux_policy_protects_ancestors_and_preserves_public_renames() { ); } } + assert_eq!( + mutation(&f, &target, "read", &f.private_file, &f.private_file), + serde_json::json!({"ok":false,"errno":13}), + "private file read" + ); + assert_eq!( + mutation(&f, &target, "write", &f.private_file, &f.private_file), + serde_json::json!({"ok":false,"errno":30}), + "private file write" + ); let data = f.package.join("data"); assert_eq!( mutation(&f, &target, "write", &data, &data), diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md index 7bae093f..20524927 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md @@ -114,3 +114,14 @@ backend tests and incorrectly expected unavailable on Linux itself. That duplica is removed. The jobs stopped before the new native integration/controls, so none of those native checks is counted as passing for that revision. Fresh hosted execution remains required after this test-only correction. + + +At `b4822f2e`, both Linux jobs compile the new tests and pass private-procfs and +ancestor/positive-rename regressions. The matrix then fails `private_list`: its +primary private-file argument was under a public parent, while the shared native +fixture deliberately measures the parent directory's listing. A file mask does +not make its public parent an opaque directory. The matrix now supplies the +actual private-directory file for that directory assertion, while constructor and +symlink probes retain the private-file target. Separate direct file-read/write +assertions are added to the mutation regression. No directory or file denial is +removed; full fresh matrix and control execution remains required. From 856a70f05aea8f43390f8823122402a9c4b16a32 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:10:51 -0400 Subject: [PATCH 4/4] docs(plugins): retain Linux policy native evidence --- .../linux-policy-composition.md | 33 +++ .../linux-policy-546641f9/arm64/job.json | 197 ++++++++++++++++++ .../arm64/linux-policy-ancestor-control.log | 24 +++ .../arm64/linux-policy-immutable-control.log | 24 +++ .../arm64/linux-policy-private-control.log | 24 +++ .../arm64/linux-policy-proc-control.log | 24 +++ .../arm64/linux-policy-read-control.log | 24 +++ .../arm64/linux-policy-shell-control.log | 24 +++ .../arm64/native-entry-provenance.json | 42 ++++ .../linux-policy-546641f9/x64/job.json | 197 ++++++++++++++++++ .../x64/linux-policy-ancestor-control.log | 24 +++ .../x64/linux-policy-immutable-control.log | 24 +++ .../x64/linux-policy-private-control.log | 24 +++ .../x64/linux-policy-proc-control.log | 24 +++ .../x64/linux-policy-read-control.log | 24 +++ .../x64/linux-policy-shell-control.log | 24 +++ .../x64/native-entry-provenance.json | 42 ++++ 17 files changed, 799 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/job.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-ancestor-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-immutable-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-private-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-proc-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-read-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-shell-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/native-entry-provenance.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/job.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-ancestor-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-immutable-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-private-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-proc-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-read-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-shell-control.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/native-entry-provenance.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md index 20524927..737f9e8e 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md @@ -125,3 +125,36 @@ actual private-directory file for that directory assertion, while constructor an symlink probes retain the private-file target. Separate direct file-read/write assertions are added to the mutation regression. No directory or file denial is removed; full fresh matrix and control execution remains required. + +## Hosted component evidence at `546641f9` + +Both Linux jobs in [run 40954544764](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40954544764) +completed successfully: [x64 job 123731111912](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40954544764/job/123731111912) +and [arm64 job 123731111847](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40954544764/job/123731111847). +This supersedes the pending Linux component results above, without closing the +remaining admission or runtime acceptance requirements. + +Each job passes seven Linux policy unit tests and all eight native entry tests, +including the three new composed-policy regressions. The six individual source +mutations fail their named assertions, verified directly in the retained logs: +protected ancestor rename, immutable package write, private write, project read, +shell execution and private proc visibility. Both restored suites pass afterward; +the later descriptor-cleanup control also fails as intended and its restored +eight-test native suite passes. A failing mutation is evidence that its specific +assertion detects the removed safeguard, not a passing production test. + +The [retained reports and control logs](../../../scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/) +bind these results to merge checkout `d273221de42084d0c0f45d64d7779a42b6ace819`, +whose parents are base `2548fdeaf68fdfe6e13f8ecfa87e77ae41e5bcd7` and PR head +`546641f97c0ef4dc539b017ad613397bbc250452`. Independently hashing every manifest +path plus NUL plus its exact checkout bytes reproduces both reports' source hash +`21e1e4adca3b9deaefa1a392c1da62c83386bff16c970012b5f6dbd6a4cb9545`. +The jobs report helper SHA-256 values +`22ad1c8ddc2828a2ad618dbe1289d30b91b996272cb4b1e279b38f86fb55cbff` (x64) +and `effbfd2d592bd9bdadbce618f9821a040b80949d0dc8069c8128e577dcd49d28` +(arm64). The artifacts retain provenance and logs, not helper binaries, so those +binary hashes are job-reported rather than independently rehashed downloads. + +These are native fixture/component results. They do not establish production host +activation, full runtime/configuration acceptance, UNIX-socket isolation or the +owner/generation/settlement obligations listed above. Those gates remain open. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/job.json b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/job.json new file mode 100644 index 00000000..1cb7cc37 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/job.json @@ -0,0 +1,197 @@ +{ + "id": 123731111847, + "run_id": 40954544764, + "head_sha": "546641f97c0ef4dc539b017ad613397bbc250452", + "name": "Packaged entry (ubuntu-24.04-arm)", + "html_url": "https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40954544764/job/123731111847", + "status": "completed", + "conclusion": "success", + "started_at": "2026-10-29T21:04:12Z", + "completed_at": "2026-10-29T21:06:45Z", + "steps": [ + { + "name": "Set up job", + "status": "completed", + "conclusion": "success", + "number": 1, + "started_at": "2026-10-29T21:04:13Z", + "completed_at": "2026-10-29T21:04:15Z" + }, + { + "name": "Run actions/checkout@v4", + "status": "completed", + "conclusion": "success", + "number": 2, + "started_at": "2026-10-29T21:04:15Z", + "completed_at": "2026-10-29T21:04:17Z" + }, + { + "name": "Run actions/setup-node@v4", + "status": "completed", + "conclusion": "success", + "number": 3, + "started_at": "2026-10-29T21:04:17Z", + "completed_at": "2026-10-29T21:04:18Z" + }, + { + "name": "Run actions/setup-python@v5", + "status": "completed", + "conclusion": "success", + "number": 4, + "started_at": "2026-10-29T21:04:18Z", + "completed_at": "2026-10-29T21:04:18Z" + }, + { + "name": "Run oven-sh/setup-bun@v2", + "status": "completed", + "conclusion": "success", + "number": 5, + "started_at": "2026-10-29T21:04:18Z", + "completed_at": "2026-10-29T21:04:19Z" + }, + { + "name": "Run denoland/setup-deno@v2", + "status": "completed", + "conclusion": "success", + "number": 6, + "started_at": "2026-10-29T21:04:19Z", + "completed_at": "2026-10-29T21:04:20Z" + }, + { + "name": "Run dtolnay/rust-toolchain@stable", + "status": "completed", + "conclusion": "success", + "number": 7, + "started_at": "2026-10-29T21:04:20Z", + "completed_at": "2026-10-29T21:04:21Z" + }, + { + "name": "Run Swatinem/rust-cache@v2", + "status": "completed", + "conclusion": "success", + "number": 8, + "started_at": "2026-10-29T21:04:21Z", + "completed_at": "2026-10-29T21:04:22Z" + }, + { + "name": "Verify native musl C compilation", + "status": "completed", + "conclusion": "success", + "number": 9, + "started_at": "2026-10-29T21:04:22Z", + "completed_at": "2026-10-29T21:04:36Z" + }, + { + "name": "Verify packaged entry and explicit unsupported-mode behavior", + "status": "completed", + "conclusion": "success", + "number": 10, + "started_at": "2026-10-29T21:04:36Z", + "completed_at": "2026-10-29T21:05:40Z" + }, + { + "name": "Verify native policy composition and failure controls", + "status": "completed", + "conclusion": "skipped", + "number": 11, + "started_at": "2026-10-29T21:05:40Z", + "completed_at": "2026-10-29T21:05:40Z" + }, + { + "name": "Verify Linux composed policy controls", + "status": "completed", + "conclusion": "success", + "number": 12, + "started_at": "2026-10-29T21:05:40Z", + "completed_at": "2026-10-29T21:06:14Z" + }, + { + "name": "Verify packaged entry across supported runtimes", + "status": "completed", + "conclusion": "success", + "number": 13, + "started_at": "2026-10-29T21:06:14Z", + "completed_at": "2026-10-29T21:06:23Z" + }, + { + "name": "Verify high-descriptor regression detects missing cleanup", + "status": "completed", + "conclusion": "success", + "number": 14, + "started_at": "2026-10-29T21:06:23Z", + "completed_at": "2026-10-29T21:06:32Z" + }, + { + "name": "Record exact source and built helper provenance", + "status": "completed", + "conclusion": "success", + "number": 15, + "started_at": "2026-10-29T21:06:32Z", + "completed_at": "2026-10-29T21:06:32Z" + }, + { + "name": "Retain build provenance", + "status": "completed", + "conclusion": "success", + "number": 16, + "started_at": "2026-10-29T21:06:32Z", + "completed_at": "2026-10-29T21:06:33Z" + }, + { + "name": "Post Run Swatinem/rust-cache@v2", + "status": "completed", + "conclusion": "success", + "number": 27, + "started_at": "2026-10-29T21:06:33Z", + "completed_at": "2026-10-29T21:06:41Z" + }, + { + "name": "Post Run denoland/setup-deno@v2", + "status": "completed", + "conclusion": "success", + "number": 28, + "started_at": "2026-10-29T21:06:41Z", + "completed_at": "2026-10-29T21:06:41Z" + }, + { + "name": "Post Run oven-sh/setup-bun@v2", + "status": "completed", + "conclusion": "success", + "number": 29, + "started_at": "2026-10-29T21:06:41Z", + "completed_at": "2026-10-29T21:06:42Z" + }, + { + "name": "Post Run actions/setup-python@v5", + "status": "completed", + "conclusion": "success", + "number": 30, + "started_at": "2026-10-29T21:06:42Z", + "completed_at": "2026-10-29T21:06:42Z" + }, + { + "name": "Post Run actions/setup-node@v4", + "status": "completed", + "conclusion": "success", + "number": 31, + "started_at": "2026-10-29T21:06:42Z", + "completed_at": "2026-10-29T21:06:43Z" + }, + { + "name": "Post Run actions/checkout@v4", + "status": "completed", + "conclusion": "success", + "number": 32, + "started_at": "2026-10-29T21:06:43Z", + "completed_at": "2026-10-29T21:06:43Z" + }, + { + "name": "Complete job", + "status": "completed", + "conclusion": "success", + "number": 33, + "started_at": "2026-10-29T21:06:43Z", + "completed_at": "2026-10-29T21:06:43Z" + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-ancestor-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-ancestor-control.log new file mode 100644 index 00000000..452496d9 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-ancestor-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 4.06s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames stdout ---- + +thread 'linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames' (6346) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:253:13: +assertion `left == right` failed: protected ancestor rename: "/tmp/.tmpA1kE3F/project \"quote\" \\ workspace/extensions/level" + left: Object {"ok": Bool(true), "errno": Number(0)} + right: Object {"ok": Bool(false), "errno": Number(16)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.12s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-immutable-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-immutable-control.log new file mode 100644 index 00000000..547eec3c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-immutable-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 4.00s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames stdout ---- + +thread 'linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames' (6419) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:277:5: +assertion `left == right` failed: immutable package write + left: Object {"ok": Bool(true), "errno": Number(0)} + right: Object {"ok": Bool(false), "errno": Number(30)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.28s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-private-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-private-control.log new file mode 100644 index 00000000..99fc03a5 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-private-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 3.94s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (6547) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:156:9: +assertion `left == right` failed: private_write: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":false,"project_list":false,"project_write":true,"written_project_read":false,"private_read":false,"private_list":false,"private_write":true,"protected_read":false,"protected_write":true,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[false,false],"thread_shell":false,"shell":false,"native_fork":false,"child_reads":null,"native_child_reads":null,"cannot_relax":true,"constructor_private_read":false,"constructor_fork":false,"native_spawn":false,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.12s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-proc-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-proc-control.log new file mode 100644 index 00000000..0646d205 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-proc-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 3.96s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads stdout ---- + +thread 'linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads' (6781) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:323:9: +assertion `left == right` failed: private proc visibility: {"self":true,"init":true,"sys":true,"private_pid_view":false,"project_alias":false,"private_alias":false,"scratch_alias":true} + left: Object {"self": Bool(true), "init": Bool(true), "sys": Bool(true), "private_pid_view": Bool(false), "project_alias": Bool(false), "private_alias": Bool(false), "scratch_alias": Bool(true)} + right: Object {"self": Bool(true), "init": Bool(false), "sys": Bool(false), "private_pid_view": Bool(true), "project_alias": Bool(false), "private_alias": Bool(false), "scratch_alias": Bool(true)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.12s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-read-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-read-control.log new file mode 100644 index 00000000..b2746094 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-read-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 3.97s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (6635) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:158:5: +assertion `left == right` failed: project_read: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":true,"project_list":true,"project_write":false,"written_project_read":false,"private_read":false,"private_list":false,"private_write":false,"protected_read":false,"protected_write":false,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[true,false],"thread_shell":false,"shell":false,"native_fork":false,"child_reads":null,"native_child_reads":null,"cannot_relax":true,"constructor_private_read":false,"constructor_fork":false,"native_spawn":false,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.08s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-shell-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-shell-control.log new file mode 100644 index 00000000..0696cfcb --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/linux-policy-shell-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 3.94s + Running tests/native_exec.rs (target/debug/deps/native_exec-5605358acd3324ea) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (6704) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:174:9: +assertion `left == right` failed: shell: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":false,"project_list":false,"project_write":false,"written_project_read":false,"private_read":false,"private_list":false,"private_write":false,"protected_read":false,"protected_write":false,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[false,false],"thread_shell":true,"shell":true,"native_fork":true,"child_reads":[false,false],"native_child_reads":[false,false],"cannot_relax":true,"constructor_private_read":false,"constructor_fork":true,"native_spawn":true,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.09s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/native-entry-provenance.json b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/native-entry-provenance.json new file mode 100644 index 00000000..77637ad3 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/arm64/native-entry-provenance.json @@ -0,0 +1,42 @@ +{ + "recorded_at": "2026-10-29T21:06:32.466Z", + "platform": "linux", + "architecture": "arm64", + "checkout": "d273221de42084d0c0f45d64d7779a42b6ace819", + "run_id": "40954544764", + "sources": [ + ".github/workflows/plugin-native-launcher.yml", + "scripts/probes/plugin-confinement/run.mts", + "crates/cyber-sandbox/native/linux_plugin_exec.c", + "crates/cyber-sandbox/native/linux_filter.h", + "crates/cyber-sandbox/native/linux_initial_exec.h", + "crates/cyber-sandbox/tests/support/native_exec_linux.rs", + "Cargo.lock", + "crates/cyber-sandbox/Cargo.toml", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/native/macos_plugin_exec.c", + "crates/cyber-sandbox/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/tests/native_exec.rs", + "crates/cyber-sandbox/src/lib.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/tests.rs", + "crates/cyber-sandbox/tests/support/native_policy_macos.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "crates/cyber-sandbox/src/plugin_policy/linux/tests.rs", + "crates/cyber-sandbox/tests/support/native_policy_linux.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c", + "crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c", + "crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c", + "scripts/probes/plugin-confinement/fixture_exec.c", + "scripts/probes/plugin-confinement/fixture_native.c", + "scripts/probes/plugin-confinement/process_policy.h", + "scripts/probes/plugin-native-entry/report.mts" + ], + "source_sha256": "21e1e4adca3b9deaefa1a392c1da62c83386bff16c970012b5f6dbd6a4cb9545", + "packaged_binary": { + "sha256": "effbfd2d592bd9bdadbce618f9821a040b80949d0dc8069c8128e577dcd49d28", + "bytes": 124230528 + }, + "scope": "Build provenance. macOS tests execute private copies signed with fresh identifiers; read the job for outcomes. This does not establish plugin launch admission or runtime compatibility beyond the tested fixtures." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/job.json b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/job.json new file mode 100644 index 00000000..164690b9 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/job.json @@ -0,0 +1,197 @@ +{ + "id": 123731111912, + "run_id": 40954544764, + "head_sha": "546641f97c0ef4dc539b017ad613397bbc250452", + "name": "Packaged entry (ubuntu-24.04)", + "html_url": "https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40954544764/job/123731111912", + "status": "completed", + "conclusion": "success", + "started_at": "2026-10-29T21:04:10Z", + "completed_at": "2026-10-29T21:06:36Z", + "steps": [ + { + "name": "Set up job", + "status": "completed", + "conclusion": "success", + "number": 1, + "started_at": "2026-10-29T21:04:11Z", + "completed_at": "2026-10-29T21:04:14Z" + }, + { + "name": "Run actions/checkout@v4", + "status": "completed", + "conclusion": "success", + "number": 2, + "started_at": "2026-10-29T21:04:14Z", + "completed_at": "2026-10-29T21:04:17Z" + }, + { + "name": "Run actions/setup-node@v4", + "status": "completed", + "conclusion": "success", + "number": 3, + "started_at": "2026-10-29T21:04:17Z", + "completed_at": "2026-10-29T21:04:17Z" + }, + { + "name": "Run actions/setup-python@v5", + "status": "completed", + "conclusion": "success", + "number": 4, + "started_at": "2026-10-29T21:04:17Z", + "completed_at": "2026-10-29T21:04:18Z" + }, + { + "name": "Run oven-sh/setup-bun@v2", + "status": "completed", + "conclusion": "success", + "number": 5, + "started_at": "2026-10-29T21:04:18Z", + "completed_at": "2026-10-29T21:04:19Z" + }, + { + "name": "Run denoland/setup-deno@v2", + "status": "completed", + "conclusion": "success", + "number": 6, + "started_at": "2026-10-29T21:04:19Z", + "completed_at": "2026-10-29T21:04:21Z" + }, + { + "name": "Run dtolnay/rust-toolchain@stable", + "status": "completed", + "conclusion": "success", + "number": 7, + "started_at": "2026-10-29T21:04:21Z", + "completed_at": "2026-10-29T21:04:21Z" + }, + { + "name": "Run Swatinem/rust-cache@v2", + "status": "completed", + "conclusion": "success", + "number": 8, + "started_at": "2026-10-29T21:04:21Z", + "completed_at": "2026-10-29T21:04:23Z" + }, + { + "name": "Verify native musl C compilation", + "status": "completed", + "conclusion": "success", + "number": 9, + "started_at": "2026-10-29T21:04:23Z", + "completed_at": "2026-10-29T21:04:31Z" + }, + { + "name": "Verify packaged entry and explicit unsupported-mode behavior", + "status": "completed", + "conclusion": "success", + "number": 10, + "started_at": "2026-10-29T21:04:31Z", + "completed_at": "2026-10-29T21:05:47Z" + }, + { + "name": "Verify native policy composition and failure controls", + "status": "completed", + "conclusion": "skipped", + "number": 11, + "started_at": "2026-10-29T21:05:47Z", + "completed_at": "2026-10-29T21:05:47Z" + }, + { + "name": "Verify Linux composed policy controls", + "status": "completed", + "conclusion": "success", + "number": 12, + "started_at": "2026-10-29T21:05:47Z", + "completed_at": "2026-10-29T21:06:06Z" + }, + { + "name": "Verify packaged entry across supported runtimes", + "status": "completed", + "conclusion": "success", + "number": 13, + "started_at": "2026-10-29T21:06:06Z", + "completed_at": "2026-10-29T21:06:15Z" + }, + { + "name": "Verify high-descriptor regression detects missing cleanup", + "status": "completed", + "conclusion": "success", + "number": 14, + "started_at": "2026-10-29T21:06:15Z", + "completed_at": "2026-10-29T21:06:20Z" + }, + { + "name": "Record exact source and built helper provenance", + "status": "completed", + "conclusion": "success", + "number": 15, + "started_at": "2026-10-29T21:06:20Z", + "completed_at": "2026-10-29T21:06:20Z" + }, + { + "name": "Retain build provenance", + "status": "completed", + "conclusion": "success", + "number": 16, + "started_at": "2026-10-29T21:06:20Z", + "completed_at": "2026-10-29T21:06:22Z" + }, + { + "name": "Post Run Swatinem/rust-cache@v2", + "status": "completed", + "conclusion": "success", + "number": 27, + "started_at": "2026-10-29T21:06:22Z", + "completed_at": "2026-10-29T21:06:31Z" + }, + { + "name": "Post Run denoland/setup-deno@v2", + "status": "completed", + "conclusion": "success", + "number": 28, + "started_at": "2026-10-29T21:06:31Z", + "completed_at": "2026-10-29T21:06:31Z" + }, + { + "name": "Post Run oven-sh/setup-bun@v2", + "status": "completed", + "conclusion": "success", + "number": 29, + "started_at": "2026-10-29T21:06:31Z", + "completed_at": "2026-10-29T21:06:34Z" + }, + { + "name": "Post Run actions/setup-python@v5", + "status": "completed", + "conclusion": "success", + "number": 30, + "started_at": "2026-10-29T21:06:34Z", + "completed_at": "2026-10-29T21:06:34Z" + }, + { + "name": "Post Run actions/setup-node@v4", + "status": "completed", + "conclusion": "success", + "number": 31, + "started_at": "2026-10-29T21:06:34Z", + "completed_at": "2026-10-29T21:06:34Z" + }, + { + "name": "Post Run actions/checkout@v4", + "status": "completed", + "conclusion": "success", + "number": 32, + "started_at": "2026-10-29T21:06:34Z", + "completed_at": "2026-10-29T21:06:34Z" + }, + { + "name": "Complete job", + "status": "completed", + "conclusion": "success", + "number": 33, + "started_at": "2026-10-29T21:06:34Z", + "completed_at": "2026-10-29T21:06:34Z" + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-ancestor-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-ancestor-control.log new file mode 100644 index 00000000..f52958b4 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-ancestor-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.05s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames stdout ---- + +thread 'linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames' (6625) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:253:13: +assertion `left == right` failed: protected ancestor rename: "/tmp/.tmpqG1xi8/project \"quote\" \\ workspace/extensions/level" + left: Object {"ok": Bool(true), "errno": Number(0)} + right: Object {"ok": Bool(false), "errno": Number(16)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.12s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-immutable-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-immutable-control.log new file mode 100644 index 00000000..eb6b6592 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-immutable-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.03s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames stdout ---- + +thread 'linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames' (6706) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:277:5: +assertion `left == right` failed: immutable package write + left: Object {"ok": Bool(true), "errno": Number(0)} + right: Object {"ok": Bool(false), "errno": Number(30)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_protects_ancestors_and_preserves_public_renames + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.32s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-private-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-private-control.log new file mode 100644 index 00000000..6fb4cb6f --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-private-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.16s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (6843) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:156:9: +assertion `left == right` failed: private_write: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":false,"project_list":false,"project_write":true,"written_project_read":false,"private_read":false,"private_list":false,"private_write":true,"protected_read":false,"protected_write":true,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[false,false],"thread_shell":false,"shell":false,"native_fork":false,"child_reads":null,"native_child_reads":null,"cannot_relax":true,"constructor_private_read":false,"constructor_fork":false,"native_spawn":false,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.12s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-proc-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-proc-control.log new file mode 100644 index 00000000..d764da26 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-proc-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.14s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads stdout ---- + +thread 'linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads' (7103) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:323:9: +assertion `left == right` failed: private proc visibility: {"self":true,"init":true,"sys":true,"private_pid_view":false,"project_alias":false,"private_alias":false,"scratch_alias":true} + left: Object {"self": Bool(true), "init": Bool(true), "sys": Bool(true), "private_pid_view": Bool(false), "project_alias": Bool(false), "private_alias": Bool(false), "scratch_alias": Bool(true)} + right: Object {"self": Bool(true), "init": Bool(false), "sys": Bool(false), "private_pid_view": Bool(true), "project_alias": Bool(false), "private_alias": Bool(false), "scratch_alias": Bool(true)} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_private_proc_cannot_restore_host_or_private_file_reads + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.13s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-read-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-read-control.log new file mode 100644 index 00000000..08cec910 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-read-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.24s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (6940) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:158:5: +assertion `left == right` failed: project_read: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":true,"project_list":true,"project_write":false,"written_project_read":false,"private_read":false,"private_list":false,"private_write":false,"protected_read":false,"protected_write":false,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[true,false],"thread_shell":false,"shell":false,"native_fork":false,"child_reads":null,"native_child_reads":null,"cannot_relax":true,"constructor_private_read":false,"constructor_fork":false,"native_spawn":false,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.08s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-shell-control.log b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-shell-control.log new file mode 100644 index 00000000..09aca8a1 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/linux-policy-shell-control.log @@ -0,0 +1,24 @@ + Compiling cyber-sandbox v0.1.0 (/home/runner/work/cybercode-cli/cybercode-cli/crates/cyber-sandbox) + Finished `test` profile [unoptimized + debuginfo] target(s) in 2.13s + Running tests/native_exec.rs (target/debug/deps/native_exec-867fac6aeb87ccec) + +running 1 test +test linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry ... FAILED + +failures: + +---- linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry stdout ---- + +thread 'linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry' (7017) panicked at crates/cyber-sandbox/tests/support/native_policy_linux.rs:174:9: +assertion `left == right` failed: shell: {"stdio_tool":true,"package_read":true,"scratch_write":true,"scratch_read":true,"project_read":false,"project_list":false,"project_write":false,"written_project_read":false,"private_read":false,"private_list":false,"private_write":false,"protected_read":false,"protected_write":false,"symlink_private_read":false,"hardlink_private":false,"project_rename":false,"inherited_descriptor":false,"thread":true,"package_hardlink":false,"package_alias_write":false,"thread_reads":[false,false],"thread_shell":true,"shell":true,"native_fork":true,"child_reads":[false,false],"native_child_reads":[false,false],"cannot_relax":true,"constructor_private_read":false,"constructor_fork":true,"native_spawn":true,"filter_replacement_denied":true} + left: Bool(true) + right: false +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + +failures: + linux::policy::rust_linux_policy_composes_mounts_with_landlock_and_the_packaged_entry + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.09s + +error: test failed, to rerun pass `-p cyber-sandbox --test native_exec` diff --git a/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/native-entry-provenance.json b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/native-entry-provenance.json new file mode 100644 index 00000000..e6c2bb97 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-policy-546641f9/x64/native-entry-provenance.json @@ -0,0 +1,42 @@ +{ + "recorded_at": "2026-10-29T21:06:20.867Z", + "platform": "linux", + "architecture": "x64", + "checkout": "d273221de42084d0c0f45d64d7779a42b6ace819", + "run_id": "40954544764", + "sources": [ + ".github/workflows/plugin-native-launcher.yml", + "scripts/probes/plugin-confinement/run.mts", + "crates/cyber-sandbox/native/linux_plugin_exec.c", + "crates/cyber-sandbox/native/linux_filter.h", + "crates/cyber-sandbox/native/linux_initial_exec.h", + "crates/cyber-sandbox/tests/support/native_exec_linux.rs", + "Cargo.lock", + "crates/cyber-sandbox/Cargo.toml", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/native/macos_plugin_exec.c", + "crates/cyber-sandbox/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/tests/native_exec.rs", + "crates/cyber-sandbox/src/lib.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/tests.rs", + "crates/cyber-sandbox/tests/support/native_policy_macos.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "crates/cyber-sandbox/src/plugin_policy/linux/tests.rs", + "crates/cyber-sandbox/tests/support/native_policy_linux.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c", + "crates/cyber-sandbox/tests/fixtures/plugin_proc_probe.c", + "crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c", + "scripts/probes/plugin-confinement/fixture_exec.c", + "scripts/probes/plugin-confinement/fixture_native.c", + "scripts/probes/plugin-confinement/process_policy.h", + "scripts/probes/plugin-native-entry/report.mts" + ], + "source_sha256": "21e1e4adca3b9deaefa1a392c1da62c83386bff16c970012b5f6dbd6a4cb9545", + "packaged_binary": { + "sha256": "22ad1c8ddc2828a2ad618dbe1289d30b91b996272cb4b1e279b38f86fb55cbff", + "bytes": 128989736 + }, + "scope": "Build provenance. macOS tests execute private copies signed with fresh identifiers; read the job for outcomes. This does not establish plugin launch admission or runtime compatibility beyond the tested fixtures." +}