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..09ea6338 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_policy/linux/tests.rs @@ -0,0 +1,155 @@ +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_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..25a7feb1 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_path_mutation.c @@ -0,0 +1,28 @@ +/* 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], "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); + 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..2050ec5b --- /dev/null +++ b/crates/cyber-sandbox/tests/support/native_policy_linux.rs @@ -0,0 +1,330 @@ +//! 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_dir.join("secret"), + 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}) + ); + } + } + 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), + 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..737f9e8e --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-policy-composition.md @@ -0,0 +1,160 @@ +# 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 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 +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. + + +## 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. + + +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. + +## 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/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/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." +} 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",