From d90f4c162bab22d4dd16a56d63fee1f02583d811 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 07:28:14 -0400 Subject: [PATCH 01/50] feat(sandbox): package the macOS native plugin exec entry --- .config/nextest.toml | 1 + .github/workflows/plugin-native-launcher.yml | 73 ++++ Cargo.lock | 1 + crates/cyber-sandbox/Cargo.toml | 3 + crates/cyber-sandbox/build.rs | 13 + .../cyber-sandbox/native/macos_plugin_exec.c | 64 ++++ .../src/bin/cyber-sandbox-exec.rs | 18 + crates/cyber-sandbox/src/native_exec.rs | 96 ++++++ .../tests/fixtures/plugin_native_fds.c | 25 ++ crates/cyber-sandbox/tests/native_exec.rs | 314 ++++++++++++++++++ .../packaged-native-entry.md | 106 ++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- scripts/probes/plugin-native-entry/report.mts | 33 ++ 13 files changed, 748 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/plugin-native-launcher.yml create mode 100644 crates/cyber-sandbox/build.rs create mode 100644 crates/cyber-sandbox/native/macos_plugin_exec.c create mode 100644 crates/cyber-sandbox/src/native_exec.rs create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c create mode 100644 crates/cyber-sandbox/tests/native_exec.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md create mode 100644 scripts/probes/plugin-native-entry/report.mts diff --git a/.config/nextest.toml b/.config/nextest.toml index d60c1c6c..6d3034f9 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -107,6 +107,7 @@ binary_id(cyber-account) | binary_id(cyber-sandbox::bridge) | binary_id(cyber-sandbox::git_inspection) | binary_id(cyber-sandbox::plugin_copy) +| binary_id(cyber-sandbox::native_exec) | binary_id(cyber-sandbox::proxy_shutdown) | binary_id(cyber-sandbox::windows_confine) | binary_id(cyber-sandbox::windows_job) diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml new file mode 100644 index 00000000..3bdea36e --- /dev/null +++ b/.github/workflows/plugin-native-launcher.yml @@ -0,0 +1,73 @@ +name: Packaged plugin native entry + +on: + workflow_dispatch: + pull_request: + paths: + - crates/cyber-sandbox/** + - scripts/probes/plugin-confinement/fixture_exec.c + - scripts/probes/plugin-confinement/fixture_native.c + - scripts/probes/plugin-confinement/process_policy.h + - Cargo.lock + - .github/workflows/plugin-native-launcher.yml + - scripts/probes/plugin-native-entry/** + push: + branches: [main] + paths: + - crates/cyber-sandbox/** + - scripts/probes/plugin-confinement/fixture_exec.c + - scripts/probes/plugin-confinement/fixture_native.c + - scripts/probes/plugin-confinement/process_policy.h + - Cargo.lock + - .github/workflows/plugin-native-launcher.yml + - scripts/probes/plugin-native-entry/** + +permissions: + contents: read + +concurrency: + group: plugin-native-entry-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + entry: + name: Packaged entry (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [macos-15, macos-15-intel, ubuntu-24.04, ubuntu-24.04-arm, windows-latest] + env: + CARGO_INCREMENTAL: 0 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Verify packaged entry and explicit unsupported-mode behavior + shell: bash + run: | + set -euo pipefail + cargo test --locked -p cyber-sandbox --bin cyber-sandbox-exec native_exec + cargo test --locked -p cyber-sandbox --test native_exec + - name: Record exact source and built helper provenance + if: always() + shell: bash + run: | + set -euo pipefail + binary=target/debug/cyber-sandbox-exec + if [[ "$RUNNER_OS" == Windows ]]; then + binary+=.exe + fi + node --experimental-strip-types scripts/probes/plugin-native-entry/report.mts --binary "$binary" --out "$RUNNER_TEMP/native-entry-provenance.json" + - name: Retain build provenance + if: always() + uses: actions/upload-artifact@v4 + with: + name: native-entry-provenance-${{ matrix.os }} + path: ${{ runner.temp }}/native-entry-provenance.json + if-no-files-found: error + retention-days: 14 diff --git a/Cargo.lock b/Cargo.lock index 62d4c284..6c1e99ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1209,6 +1209,7 @@ dependencies = [ "base64 0.22.1", "cap-fs-ext", "cap-std", + "cc", "cyber-core", "futures", "git2", diff --git a/crates/cyber-sandbox/Cargo.toml b/crates/cyber-sandbox/Cargo.toml index cf3536ce..79d56472 100644 --- a/crates/cyber-sandbox/Cargo.toml +++ b/crates/cyber-sandbox/Cargo.toml @@ -31,6 +31,9 @@ libc.workspace = true [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = ["Wdk_Storage_FileSystem", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Globalization", "Win32_Security", "Win32_Security_Isolation", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_JobObjects", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } +[build-dependencies] +cc = "1" + [dev-dependencies] rusqlite.workspace = true tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "sync", "time", "macros", "process"] } diff --git a/crates/cyber-sandbox/build.rs b/crates/cyber-sandbox/build.rs new file mode 100644 index 00000000..164c60c8 --- /dev/null +++ b/crates/cyber-sandbox/build.rs @@ -0,0 +1,13 @@ +fn main() { + println!("cargo:rerun-if-changed=native/macos_plugin_exec.c"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") { + return; + } + cc::Build::new() + .file("native/macos_plugin_exec.c") + .flag("-Wall") + .flag("-Wextra") + .flag("-Werror") + .flag("-Wno-error=deprecated-declarations") + .compile("cyber_plugin_exec"); +} diff --git a/crates/cyber-sandbox/native/macos_plugin_exec.c b/crates/cyber-sandbox/native/macos_plugin_exec.c new file mode 100644 index 00000000..06d30ed6 --- /dev/null +++ b/crates/cyber-sandbox/native/macos_plugin_exec.c @@ -0,0 +1,64 @@ +/* Trusted pre-target Seatbelt entry. Profile and image ownership are caller + * admission requirements; this function never authorizes a package or generation. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +int cyber_plugin_exec(int argc, const char *const argv[]) { + if (argc < 3 || argv[1][0] != '/' || argv[2][0] != '/') return 2; + { /* Production entry always installs a policy; no probe bypass. */ + int fd = open(argv[1], O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | O_NOCTTY); + struct stat info; + if (fd < 0 || fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || + info.st_nlink != 1 || info.st_size <= 0 || info.st_size > 65536) { + if (fd >= 0) close(fd); + return 2; + } + size_t size = (size_t)info.st_size, offset = 0; + char *profile = calloc(size + 1, 1); + if (profile == NULL) { close(fd); return 2; } + while (offset < size) { + ssize_t count = read(fd, profile + offset, size - offset); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) { free(profile); close(fd); return 2; } + offset += (size_t)count; + } + close(fd); + if (strlen(profile) != size) { free(profile); return 2; } + char *error = NULL; + int result = sandbox_init(profile, 0, &error); + free(profile); + sandbox_free_error(error); + if (result != 0) return 2; + } + /* Kernel exec-time closure covers descriptors above a lowered soft limit. + * SETEXEC replaces this process without needing a permitted fork. */ + posix_spawnattr_t attributes; + posix_spawn_file_actions_t actions; + if (posix_spawnattr_init(&attributes) != 0) return 2; + if (posix_spawn_file_actions_init(&actions) != 0) { + posix_spawnattr_destroy(&attributes); + return 2; + } + int result = posix_spawnattr_setflags(&attributes, + POSIX_SPAWN_SETEXEC | POSIX_SPAWN_CLOEXEC_DEFAULT); + for (int fd = STDIN_FILENO; result == 0 && fd <= STDERR_FILENO; fd++) { + if (fcntl(fd, F_GETFD) >= 0) result = posix_spawn_file_actions_addinherit_np(&actions, fd); + else if (errno != EBADF) result = errno; + } + if (result == 0) result = posix_spawn(NULL, argv[2], &actions, &attributes, (char *const *)(argv + 2), environ); + posix_spawn_file_actions_destroy(&actions); + posix_spawnattr_destroy(&attributes); + errno = result; + int denied = result == EPERM || result == EACCES; + perror("initial native exec"); + return denied ? 13 : 2; +} diff --git a/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs b/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs index dfd9370d..10ea8840 100644 --- a/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs +++ b/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs @@ -3,6 +3,10 @@ use std::process::ExitCode; +#[cfg(target_os = "macos")] +#[path = "../native_exec.rs"] +mod native_exec; + #[cfg(unix)] #[path = "../proxy_bridge.rs"] mod proxy_bridge; @@ -12,6 +16,20 @@ mod proxy_bridge; mod windows_job; fn main() -> ExitCode { + if std::env::args_os() + .nth(1) + .is_some_and(|arg| arg == "--plugin-native-exec") + { + #[cfg(target_os = "macos")] + return native_exec::run(std::env::args_os().skip(1)); + #[cfg(not(target_os = "macos"))] + { + eprintln!( + "cyber-sandbox-exec: native plugin exec entry is unavailable on this platform" + ); + return ExitCode::from(69); + } + } if std::env::args_os() .nth(1) .is_some_and(|arg| arg == "--plugin-copy") diff --git a/crates/cyber-sandbox/src/native_exec.rs b/crates/cyber-sandbox/src/native_exec.rs new file mode 100644 index 00000000..2babe1b2 --- /dev/null +++ b/crates/cyber-sandbox/src/native_exec.rs @@ -0,0 +1,96 @@ +//! Dedicated helper-only macOS policy-before-target entry, not execution admission. +//! This private binary module must never be called in the owner process. +//! Callers must first prove profile/image ownership, signature-identity separation, +//! generation authority, environment/namespace isolation and runtime compatibility. +use std::ffi::{CString, OsString}; +use std::os::unix::ffi::OsStrExt; +use std::process::ExitCode; + +const MAX_ARGS: usize = 256; +const MAX_BYTES: usize = 64 * 1024; + +/// Arguments include the native mode flag as argv[0], then absolute profile and +/// target paths and the target's arguments. A successful call replaces this PID. +pub fn run(arguments: impl IntoIterator) -> ExitCode { + let Some(arguments) = bounded(arguments) else { + eprintln!("cyber-sandbox-exec: invalid native plugin exec arguments"); + return ExitCode::from(64); + }; + ExitCode::from(match execute(&arguments) { + 13 => 13, + _ => 2, + }) +} + +fn bounded(arguments: impl IntoIterator) -> Option> { + let mut strings = Vec::new(); + let mut bytes = 0usize; + for argument in arguments { + let value = argument.as_os_str().as_bytes(); + bytes = bytes.checked_add(value.len().checked_add(1)?)?; + if strings.len() >= MAX_ARGS || bytes > MAX_BYTES { + return None; + } + strings.push(CString::new(value).ok()?); + } + if strings.len() < 3 || strings[0].as_bytes() != b"--plugin-native-exec" { + return None; + } + for argument in &strings[1..3] { + if argument.as_bytes().len() > 4096 || !argument.as_bytes().starts_with(b"/") { + return None; + } + } + Some(strings) +} + +/// The C entry reads these owned NUL-terminated strings without modifying them. +/// They and the null-terminated pointer array remain live for the complete call. +/// The count is bounded to 256, so its conversion to C int cannot truncate. +#[allow(unsafe_code)] +fn execute(arguments: &[CString]) -> i32 { + #[link(name = "cyber_plugin_exec", kind = "static")] + unsafe extern "C" { + fn cyber_plugin_exec(argc: i32, argv: *const *const std::ffi::c_char) -> i32; + } + let mut pointers: Vec<_> = arguments.iter().map(|value| value.as_ptr()).collect(); + pointers.push(std::ptr::null()); + // SAFETY: pointer/count/lifetime preconditions are established above; C takes + // no ownership and either returns an error or performs the replacing exec. + unsafe { cyber_plugin_exec(arguments.len() as i32, pointers.as_ptr()) } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn argument_bounds_refuse_relative_paths_bypass_and_embedded_nul() { + for arguments in [ + vec!["--plugin-native-exec", "-", "/bin/sh"], + vec!["--plugin-native-exec", "/profile", "relative"], + vec!["wrong-mode", "/profile", "/bin/sh"], + vec![ + "--plugin-native-exec", + "/profile", + "/bin/sh", + "bad\0argument", + ], + ] { + assert!(bounded(arguments.into_iter().map(OsString::from)).is_none()); + } + let mut arguments = vec![ + OsString::from("--plugin-native-exec"), + "/profile".into(), + "/target".into(), + ]; + arguments.extend((0..MAX_ARGS).map(|_| OsString::from("arg"))); + assert!(bounded(arguments).is_none()); + let large = vec![ + OsString::from("--plugin-native-exec"), + "/profile".into(), + "/target".into(), + "x".repeat(MAX_BYTES).into(), + ]; + assert!(bounded(large).is_none()); + } +} diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c b/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c new file mode 100644 index 00000000..df602644 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c @@ -0,0 +1,25 @@ +/* Only this controlled process opens a dummy descriptor and lowers its limit. */ +#include +#include +#include +#include +int main(int argc, char **argv) { + if (argc != 5) return 2; + struct rlimit limit; + if (getrlimit(RLIMIT_NOFILE, &limit) != 0 || limit.rlim_max <= 500) return 2; + if (limit.rlim_cur <= 500) { + limit.rlim_cur = 501; + if (setrlimit(RLIMIT_NOFILE, &limit) != 0) return 2; + } + int source = open(argv[4], O_RDONLY); + if (source < 0 || dup2(source, 500) < 0 || fcntl(500, F_SETFD, 0) < 0) return 2; + if (source != 500) close(source); + limit.rlim_cur = 64; + if (setrlimit(RLIMIT_NOFILE, &limit) != 0) return 2; + if (strcmp(argv[1], "-") == 0) { + execl(argv[3], argv[3], "fd-check", "500", (char *)NULL); + } else { + execl(argv[1], argv[1], "--plugin-native-exec", argv[2], argv[3], "fd-check", "500", (char *)NULL); + } + return 2; +} diff --git a/crates/cyber-sandbox/tests/native_exec.rs b/crates/cyber-sandbox/tests/native_exec.rs new file mode 100644 index 00000000..7cbb1ec8 --- /dev/null +++ b/crates/cyber-sandbox/tests/native_exec.rs @@ -0,0 +1,314 @@ +//! Exercise the packaged helper, not a separately compiled replacement launcher. +use std::process::Command; + +#[test] +fn bypass_or_unsupported_native_mode_never_starts_the_target() { + let output = Command::new(env!("CARGO_BIN_EXE_cyber-sandbox-exec")) + .args([ + "--plugin-native-exec", + "-", + "/bin/echo", + "TARGET-MUST-NOT-RUN", + ]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(if cfg!(target_os = "macos") { 64 } else { 69 }) + ); + assert!(output.stdout.is_empty()); + assert!(!String::from_utf8_lossy(&output.stderr).contains("TARGET-MUST-NOT-RUN")); +} + +#[cfg(target_os = "macos")] +mod macos { + use super::*; + use serde_json::{Value, json}; + use sha2::{Digest, Sha256}; + use std::os::unix::fs::{PermissionsExt, symlink}; + use std::os::unix::process::ExitStatusExt; + use std::path::{Path, PathBuf}; + + struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, + helper: PathBuf, + helper_id: String, + payload: PathBuf, + } + fn successful(command: &mut Command) -> std::process::Output { + let output = command.output().unwrap(); + assert!( + output.status.success(), + "status={:?} stdout={} stderr={}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output + } + fn sign(path: &Path, identifier: &str) { + successful( + Command::new("/usr/bin/codesign") + .args([ + "--force", + "--sign", + "-", + "--timestamp=none", + "--identifier", + identifier, + ]) + .arg(path), + ); + successful( + Command::new("/usr/bin/codesign") + .args(["--verify", "--strict", "--all-architectures"]) + .arg(path), + ); + } + fn compile(root: &Path, source: &str, name: &str) -> PathBuf { + let input = root.join(format!("{name}.c")); + std::fs::write(&input, source).unwrap(); + let output = root.join(name); + successful( + Command::new("/usr/bin/cc") + .args([ + "-Wall", + "-Wextra", + "-Werror", + "-Wno-error=deprecated-declarations", + ]) + .arg(&input) + .arg("-o") + .arg(&output), + ); + output + } + impl Fixture { + fn new() -> Self { + let temp = tempfile::Builder::new() + .permissions(std::fs::Permissions::from_mode(0o700)) + .tempdir() + .unwrap(); + let root = temp.path().canonicalize().unwrap(); + let helper = root.join("helper"); + std::fs::copy(env!("CARGO_BIN_EXE_cyber-sandbox-exec"), &helper).unwrap(); + let nonce = root.file_name().unwrap().to_str().unwrap(); + let helper_id = format!("cyber.test.helper.{nonce}"); + sign(&helper, &helper_id); + let payload = compile( + &root, + include_str!("../../../scripts/probes/plugin-confinement/fixture_exec.c"), + "payload", + ); + sign(&payload, &format!("cyber.test.payload.{nonce}")); + Self { + _temp: temp, + root, + helper, + helper_id, + payload, + } + } + fn profile(&self, name: &str, text: &str) -> PathBuf { + let path = self.root.join(name); + std::fs::write(&path, text).unwrap(); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); + path + } + fn one_shot(&self, target: &Path) -> PathBuf { + // Broad reads isolate exec compatibility; this is not fs.read enforcement. + let base = "(version 1)(deny default)(allow file-read* file-map-executable)(allow sysctl-read)(allow signal (target same-sandbox))(allow process-info* (target same-sandbox))"; + self.profile( + "one-shot.sb", + &format!( + "{base}(allow process-exec (require-all (signing-identifier {}) (literal {})))", + json!(self.helper_id), + json!(target) + ), + ) + } + fn command(&self, profile: &Path, target: &Path) -> Command { + let mut command = Command::new(&self.helper); + command + .arg("--plugin-native-exec") + .arg(profile) + .arg(target) + .env_clear() + .env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin"); + command + } + } + fn packet(output: std::process::Output) -> Value { + assert!( + output.status.success(), + "status={:?} stderr={}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() + } + + #[test] + fn packaged_entry_starts_native_and_original_system_images_but_denies_later_exec() { + let fixture = Fixture::new(); + let control = fixture.profile("control.sb", "(version 1)(allow default)"); + let unrestricted = packet(successful(&mut fixture.command(&control, &fixture.payload))); + assert_eq!(unrestricted, json!({"initial_exec":true,"self_exec":true})); + let profile = fixture.one_shot(&fixture.payload); + let confined = packet(successful(&mut fixture.command(&profile, &fixture.payload))); + assert_eq!(confined, json!({"initial_exec":true,"self_exec":false})); + + let bash = Path::new("/bin/bash"); + let original_hash = Sha256::digest(std::fs::read(bash).unwrap()); + let args = [ + "-c", + "printf 'initial-admitted\\n'; exec /bin/bash -c 'printf self-exec'", + ]; + let original_control = successful(fixture.command(&control, bash).args(args)); + assert_eq!(original_control.stdout, b"initial-admitted\nself-exec"); + let denied = fixture + .command(&fixture.one_shot(bash), bash) + .args(args) + .output() + .unwrap(); + assert_eq!( + denied.status.signal(), + None, + "signal termination cannot prove policy denial" + ); + assert!(!denied.status.success()); + assert_eq!(denied.stdout, b"initial-admitted\n"); + let error = String::from_utf8_lossy(&denied.stderr); + assert!( + error.contains("Operation not permitted") || error.contains("Permission denied"), + "{error}" + ); + assert_eq!(Sha256::digest(std::fs::read(bash).unwrap()), original_hash); + + // Admission MUST independently reject this collision. The entry alone is + // not an admitted launcher: matching subject identities are an observed bypass. + sign(&fixture.payload, &fixture.helper_id); + let collision = packet(successful( + &mut fixture.command(&fixture.one_shot(&fixture.payload), &fixture.payload), + )); + assert_eq!(collision, json!({"initial_exec":true,"self_exec":true})); + } + + #[test] + fn constructor_exec_and_spawn_enter_after_the_packaged_policy() { + let fixture = Fixture::new(); + std::fs::write( + fixture.root.join("process_policy.h"), + include_str!("../../../scripts/probes/plugin-confinement/process_policy.h"), + ) + .unwrap(); + let payload = compile( + &fixture.root, + include_str!("../../../scripts/probes/plugin-confinement/fixture_native.c"), + "constructors", + ); + sign(&payload, "cyber.test.constructor.payload"); + let control = fixture.profile("control.sb", "(version 1)(allow default)"); + for (kind, key) in [("ctor-exec", "self_exec"), ("ctor-spawn", "native_spawn")] { + let mut args = vec![kind]; + args.extend(std::iter::repeat_n("/", 12)); + let positive = packet(successful(fixture.command(&control, &payload).args(&args))); + assert_eq!(positive[key], true, "{kind}: {positive}"); + let confined = packet(successful( + fixture + .command(&fixture.one_shot(&payload), &payload) + .args(&args), + )); + assert_eq!(confined[key], false, "{kind}: {confined}"); + } + } + + #[test] + fn packaged_exec_closes_inherited_descriptors_above_a_lowered_soft_limit() { + let fixture = Fixture::new(); + let descriptors = compile( + &fixture.root, + include_str!("fixtures/plugin_native_fds.c"), + "descriptors", + ); + let private = fixture.root.join("dummy-private"); + std::fs::write(&private, "dummy fixture only").unwrap(); + let profile = fixture.one_shot(&fixture.payload); + let direct = packet(successful( + Command::new(&descriptors) + .arg("-") + .arg(&profile) + .arg(&fixture.payload) + .arg(&private), + )); + assert_eq!(direct["inherited_descriptor"], true); + let confined = packet(successful( + Command::new(&descriptors) + .arg(&fixture.helper) + .arg(&profile) + .arg(&fixture.payload) + .arg(&private), + )); + assert_eq!(confined["inherited_descriptor"], false); + } + + #[test] + fn invalid_linked_and_oversized_profiles_refuse_before_target_start() { + let fixture = Fixture::new(); + let mut profiles = vec![ + fixture.profile("empty.sb", ""), + fixture.profile("nul.sb", "(version 1)(allow default)\0ignored"), + fixture.profile("large.sb", &"x".repeat(65537)), + fixture.profile("invalid.sb", "(PRIVATE-PROFILE-PAYLOAD-MUST-NOT-BE-ECHOED"), + ]; + let linked = fixture.profile("linked.sb", "(version 1)(allow default)"); + std::fs::hard_link(&linked, fixture.root.join("alias")).unwrap(); + profiles.push(linked); + let indirect = fixture.root.join("symlink.sb"); + symlink(&profiles[0], &indirect).unwrap(); + profiles.push(indirect); + profiles.push(fixture.root.clone()); + profiles.push(fixture.root.join("missing.sb")); + for profile in profiles { + let output = fixture + .command(&profile, Path::new("/bin/echo")) + .arg("TARGET-MUST-NOT-RUN") + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2), "{output:?}"); + assert!(output.stdout.is_empty()); + assert!(!String::from_utf8_lossy(&output.stderr).contains("PRIVATE-PROFILE-PAYLOAD")); + } + } + #[test] + fn a_fifo_profile_is_refused_without_waiting_for_a_writer() { + let fixture = Fixture::new(); + let fifo = fixture.root.join("policy.fifo"); + successful(Command::new("/usr/bin/mkfifo").arg(&fifo)); + let mut child = fixture + .command(&fifo, Path::new("/bin/echo")) + .arg("TARGET-MUST-NOT-RUN") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + let completed = loop { + if child.try_wait().unwrap().is_some() { + break true; + } + if std::time::Instant::now() >= deadline { + break false; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + }; + if !completed { + child.kill().unwrap(); + } + let output = child.wait_with_output().unwrap(); + assert!(completed, "profile open blocked waiting for a FIFO writer"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + } +} diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md new file mode 100644 index 00000000..948f7f90 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md @@ -0,0 +1,106 @@ +# Packaged policy-before-target entry + +This increment promotes the macOS C entry from the existing confinement probe +into the distributed `cyber-sandbox-exec` binary. Cargo builds its native archive +with the package, so starting a plugin will not need a user-installed C compiler. +The private binary module invokes it through a small reviewed FFI boundary. The +owner library exposes no function that could accidentally apply this replacing +exec to the owner process itself. + +## Internal entry and bounds + +`--plugin-native-exec [target-args...]` accepts +at most 256 arguments and 64 KiB including terminators, with profile/target paths +at most 4,096 bytes. Owned NUL-terminated strings and the pointer vector remain +alive throughout the native call; the C ABI receives a bounded 32-bit count. +Relative paths, embedded NUL and the probe's `-` policy bypass are refused. +Errors do not echo arguments or profile contents. + +The native entry opens the profile without following its final symlink, using +nonblocking/no-controlling-terminal flags. It requires a regular, singly linked, +nonempty file of at most 64 KiB, reads the exact captured size and rejects embedded +NUL. A FIFO is refused before waiting for a writer. The owner must protect the +profile and all ancestor bindings from every admitted untrusted writer; the file +checks alone do not establish immutable ownership or race-proof provenance. + +The policy is installed before replacing the process with the target. Exec-time +`POSIX_SPAWN_CLOEXEC_DEFAULT` closes inherited descriptors, including descriptors +above a lowered soft limit; only present stdin/stdout/stderr are explicitly +inherited. `POSIX_SPAWN_SETEXEC` retains the process identity owned by the caller. +Policy failure refuses startup, with no ordinary-tool or unconfined fallback. +The C build intentionally permits the SDK deprecation diagnostics for the +`sandbox_init`/`sandbox_free_error` API used by the proven mechanism. Native +platform tests remain required; a successful compiler invocation is insufficient. + +## Actual packaged-helper verification + +Tests copy and sign the built helper with a fresh subject identifier and verify +all architecture slices. They reuse the original probe's native exec and +constructor fixtures. Real controls require: + +- Normal initial execution and self-exec under a broad exec-only control profile. +- Initial execution followed by denied self-exec under the helper-subject/exact + target rule, for a controlled native image and the original `/bin/bash`. +- An unchanged original system image, normal startup output and a permission + error for denied exec; a signal-terminated image cannot satisfy the check. +- Exec/spawn attempts from constructors to see policy before `main`. +- Descriptor 500 to survive a direct control after the soft limit is lowered to + 64, then be absent when entering through the packaged helper. +- Refusal before target startup for malformed, missing, empty, oversized, + NUL-containing, linked, directory, symlink and FIFO profiles. + +A matching target/helper signing identifier deliberately self-execs successfully. +That observed counterexample remains mandatory: the native entry alone cannot +certify safe admission. The owner must establish immutable, all-architecture +identity separation and protected package/runtime/helper provenance before using +this mechanism. The profile used to isolate exec tests grants broad reads and +therefore does not prove `fs.read`, network or secret confinement. + +On local macOS arm64, the argument unit test and six real-process integration +tests pass. Removing nonblocking profile open makes the named FIFO test fail at +its deadline; it kills/reaps its owned child before reporting that failure. The +correct code is restored before the positive suite. Other owner/copy regressions +remain required after this packaging change. + +The focused workflow tests macOS arm64/Intel and explicit unsupported internal +mode behavior on Linux arm64/x86_64 and Windows. It records the checked-out source +hash and built helper digest alongside the job. Tests execute private freshly +signed helper copies, so that build digest identifies their input rather than +claiming the modified signatures have the same hash. Source provenance is not +itself a passing test result. Native Linux packaging remains implementation work; +the approved Linux runtime support scope stays required for integration. + +## Remaining integration and gates + +The entry is currently implemented for macOS. Other platforms return the explicit +unavailable-mode result without starting a target. Existing plugin/ordinary-tool +launches retain their existing behavior in this increment. The strict approved +platform/runtime contract remains the delivery target. + +Task 2.2 remains open for Linux packaging, reviewed runtime resolution, complete +private-root/alias composition, immutable image/helper identity, sanitized +environment and launch lifetime. The owner must bind its generation to the current +plugin/digest/Location and serialize the final launch check with revocation; +already-running processes must be stopped before their resources are released. +Actual App/server enforcement, independent fs.read/fs.write/shell/network/secret +intersections and required native acceptance remain before completion. The +existing proof/design gate before protocol integration remains in force. + +## Verification recorded for this increment + +On macOS arm64, one binary argument unit test, six packaged-entry tests, nine +preparation/authority tests and five actual copy-helper tests pass. The copy +suite's two SQLite/digest fixture entrypoints are explicitly invoked by its +regression. Scoped sandbox Clippy across all targets passes with warnings denied +for Rust; the native compiler emits the two explicitly permitted SDK deprecation +diagnostics described above. Formatting, diff checks, workflow YAML/all shell +blocks, the Windows test-profile mapping and independently recalculated source +and binary digests pass. Strict OpenSpec passes all 107 items; spec lint reports +0 errors and 25 warnings. Hosted platform execution is still required. + +The scoped code/security review covers the private binary-only FFI, C string +lifetimes and count bounds, mandatory policy installation, profile types/limits, +error redaction, exec-time descriptor inheritance, real constructor controls and +the explicit identity-collision counterexample. Immutable helper/profile/package +ownership, kernel signature-mutation coverage and final launch authority remain +integration requirements; this review does not waive them. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 6a43128a..e48cd8cb 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); owner admission and production 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); owner admission and production 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 new file mode 100644 index 00000000..79a2898e --- /dev/null +++ b/scripts/probes/plugin-native-entry/report.mts @@ -0,0 +1,33 @@ +/** Source/build provenance only. Test outcomes remain in the associated job log. */ +import { createHash } from "node:crypto"; +import { createReadStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { parseArgs } from "node:util"; + +const { values } = parseArgs({ options: { binary: { type: "string" }, out: { type: "string" } } }); +if (!values.binary || !values.out) throw new Error("--binary and --out are required"); +const sources = [ + "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/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", +]; +const sourceHash = createHash("sha256"); +for (const source of sources) sourceHash.update(source + "\0").update(readFileSync(source)); +const binary = resolve(values.binary); +let image: { sha256: string; bytes: number } | null = null; +if (existsSync(binary)) { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(binary)) hash.update(chunk); + image = { sha256: hash.digest("hex"), bytes: statSync(binary).size }; +} +const report = { + recorded_at: new Date().toISOString(), platform: process.platform, architecture: process.arch, + checkout: process.env.GITHUB_SHA ?? null, run_id: process.env.GITHUB_RUN_ID ?? null, + sources, source_sha256: sourceHash.digest("hex"), packaged_binary: image, + 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 Linux native packaging.", +}; +mkdirSync(dirname(resolve(values.out)), { recursive: true }); +writeFileSync(values.out, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 }); From 6b349331b6eb5f74b025d8459fab077f19ed6b0a Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 07:52:05 -0400 Subject: [PATCH 02/50] feat(sandbox): package Linux native plugin startup boundary --- .github/workflows/plugin-native-launcher.yml | 37 +++ crates/cyber-sandbox/build.rs | 49 ++- crates/cyber-sandbox/native/linux_filter.h | 53 +++ .../cyber-sandbox/native/linux_initial_exec.h | 137 ++++++++ .../cyber-sandbox/native/linux_plugin_exec.c | 120 +++++++ .../src/bin/cyber-sandbox-exec.rs | 6 +- crates/cyber-sandbox/src/native_exec.rs | 39 ++- .../tests/fixtures/plugin_native_fds.c | 4 + crates/cyber-sandbox/tests/native_exec.rs | 10 +- .../tests/support/native_exec_linux.rs | 303 ++++++++++++++++++ .../packaged-linux-entry.md | 72 +++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- scripts/probes/plugin-native-entry/report.mts | 4 +- 13 files changed, 815 insertions(+), 21 deletions(-) create mode 100644 crates/cyber-sandbox/native/linux_filter.h create mode 100644 crates/cyber-sandbox/native/linux_initial_exec.h create mode 100644 crates/cyber-sandbox/native/linux_plugin_exec.c create mode 100644 crates/cyber-sandbox/tests/support/native_exec_linux.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml index 3bdea36e..70bd69d2 100644 --- a/.github/workflows/plugin-native-launcher.yml +++ b/.github/workflows/plugin-native-launcher.yml @@ -47,12 +47,49 @@ jobs: node-version: 22 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - name: Verify native musl C compilation + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* + sudo apt-get update + sudo apt-get install -y musl-tools + arch=$(uname -m) + musl-gcc -Wall -Wextra -Werror -idirafter "/usr/include/$arch-linux-gnu" -idirafter /usr/include -c crates/cyber-sandbox/native/linux_plugin_exec.c -o "$RUNNER_TEMP/linux-native-musl.o" - name: Verify packaged entry and explicit unsupported-mode behavior shell: bash run: | set -euo pipefail cargo test --locked -p cyber-sandbox --bin cyber-sandbox-exec native_exec cargo test --locked -p cyber-sandbox --test native_exec + - name: Verify high-descriptor regression detects missing cleanup + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/native/linux_plugin_exec.c + cp "$source" "$RUNNER_TEMP/linux-native-original.c" + trap 'cp "$RUNNER_TEMP/linux-native-original.c" "$source"' EXIT + python3 - <<'PY' + from pathlib import Path + path = Path('crates/cyber-sandbox/native/linux_plugin_exec.c') + text = path.read_text() + original = 'syscall(SYS_close_range, 3U, UINT_MAX, 0) != 0' + assert text.count(original) == 1 + path.write_text(text.replace(original, '0 /* missing-cleanup control */')) + PY + if cargo test --locked -p cyber-sandbox --test native_exec inherited_descriptors_above_lowered_limit_are_closed > "$RUNNER_TEMP/native-fd-control.log" 2>&1; then + cat "$RUNNER_TEMP/native-fd-control.log" + echo 'Missing-cleanup control unexpectedly passed' >&2 + exit 1 + fi + cat "$RUNNER_TEMP/native-fd-control.log" + rg 'linux::inherited_descriptors_above_lowered_limit_are_closed .* FAILED' "$RUNNER_TEMP/native-fd-control.log" + rg -F 'left: Bool(true)' "$RUNNER_TEMP/native-fd-control.log" + rg -F 'right: Bool(false)' "$RUNNER_TEMP/native-fd-control.log" + cp "$RUNNER_TEMP/linux-native-original.c" "$source" + cargo test --locked -p cyber-sandbox --test native_exec - name: Record exact source and built helper provenance if: always() shell: bash diff --git a/crates/cyber-sandbox/build.rs b/crates/cyber-sandbox/build.rs index 164c60c8..aa308ae7 100644 --- a/crates/cyber-sandbox/build.rs +++ b/crates/cyber-sandbox/build.rs @@ -1,13 +1,42 @@ fn main() { - println!("cargo:rerun-if-changed=native/macos_plugin_exec.c"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") { - return; + for source in [ + "macos_plugin_exec.c", + "linux_plugin_exec.c", + "linux_filter.h", + "linux_initial_exec.h", + ] { + println!("cargo:rerun-if-changed=native/{source}"); } - cc::Build::new() - .file("native/macos_plugin_exec.c") - .flag("-Wall") - .flag("-Wextra") - .flag("-Werror") - .flag("-Wno-error=deprecated-declarations") - .compile("cyber_plugin_exec"); + let mut build = cc::Build::new(); + build.flag("-Wall").flag("-Wextra").flag("-Werror"); + match std::env::var("CARGO_CFG_TARGET_OS").as_deref() { + Ok("macos") => { + build + .file("native/macos_plugin_exec.c") + .flag("-Wno-error=deprecated-declarations"); + } + Ok("linux") => { + build.file("native/linux_plugin_exec.c"); + // Debian's native musl compiler omits Linux UAPI directories. Search + // them after musl's standard headers, never shadowing libc headers. + if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("musl") + && std::env::var("HOST") + .ok() + .map(|host| host.split('-').next().unwrap_or("").to_owned()) + == std::env::var("CARGO_CFG_TARGET_ARCH").ok() + { + let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + for directory in [ + format!("/usr/include/{arch}-linux-gnu"), + "/usr/include".into(), + ] { + if std::path::Path::new(&directory).is_dir() { + build.flag("-idirafter").flag(&directory); + } + } + } + } + _ => return, + } + build.compile("cyber_plugin_exec"); } diff --git a/crates/cyber-sandbox/native/linux_filter.h b/crates/cyber-sandbox/native/linux_filter.h new file mode 100644 index 00000000..7f1a23bd --- /dev/null +++ b/crates/cyber-sandbox/native/linux_filter.h @@ -0,0 +1,53 @@ +/* Native startup seccomp definitions, promoted from the confinement proof. */ +#ifndef CYBER_PLUGIN_LINUX_FILTER_H +#define CYBER_PLUGIN_LINUX_FILTER_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__x86_64__) +#define CYBER_ARCH AUDIT_ARCH_X86_64 +#elif defined(__aarch64__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define CYBER_ARCH AUDIT_ARCH_AARCH64 +#else +#error "Linux plugin entry requires native x86_64 or little-endian aarch64" +#endif + +#define CYBER_DENY(nr) \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (nr), 0, 1), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM) +#define CYBER_ARCH_CHECK \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CYBER_ARCH, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), \ + BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, 0x40000000U, 0, 1), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS) + +#define CYBER_ESCAPE_DENIALS \ + CYBER_DENY(SYS_ptrace), \ + CYBER_DENY(SYS_process_vm_readv), \ + CYBER_DENY(SYS_process_vm_writev), \ + CYBER_DENY(SYS_pidfd_getfd), \ + CYBER_DENY(SYS_open_by_handle_at), \ + CYBER_DENY(SYS_bpf), \ + CYBER_DENY(SYS_userfaultfd), \ + CYBER_DENY(SYS_io_uring_setup), \ + CYBER_DENY(SYS_mount), \ + CYBER_DENY(SYS_umount2), \ + CYBER_DENY(SYS_pivot_root), \ + CYBER_DENY(SYS_move_mount), \ + CYBER_DENY(SYS_open_tree), \ + CYBER_DENY(SYS_fsopen), \ + CYBER_DENY(SYS_fsmount), \ + CYBER_DENY(SYS_setns), \ + CYBER_DENY(SYS_unshare) + +#endif diff --git a/crates/cyber-sandbox/native/linux_initial_exec.h b/crates/cyber-sandbox/native/linux_initial_exec.h new file mode 100644 index 00000000..6de3b734 --- /dev/null +++ b/crates/cyber-sandbox/native/linux_initial_exec.h @@ -0,0 +1,137 @@ +/* One trusted startup exec only. Called before any guest code or threads. + * The supervisor never reads guest pointers or authorizes a later request. */ +#ifndef CYBER_PLUGIN_LINUX_INITIAL_EXEC_H +#define CYBER_PLUGIN_LINUX_INITIAL_EXEC_H +#include "linux_filter.h" +#include +#include +#include +#include + +static int initial_filter(int shell) { + unsigned int execute = shell ? SECCOMP_RET_ALLOW : SECCOMP_RET_USER_NOTIF; + unsigned int spawn = shell ? SECCOMP_RET_ALLOW : SECCOMP_RET_ERRNO | EPERM; + unsigned int thread_mask = shell ? 0 : CLONE_THREAD | CLONE_VM | CLONE_SIGHAND; + struct sock_filter instructions[] = { + CYBER_ARCH_CHECK, + CYBER_ESCAPE_DENIALS, + CYBER_DENY(SYS_seccomp), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_prctl, 0, 3), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, PR_SET_SECCOMP, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_execve, 0, 1), + BPF_STMT(BPF_RET | BPF_K, execute), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_execveat, 0, 1), + BPF_STMT(BPF_RET | BPF_K, spawn), +#ifdef SYS_fork + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_fork, 0, 1), + BPF_STMT(BPF_RET | BPF_K, spawn), +#endif +#ifdef SYS_vfork + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_vfork, 0, 1), + BPF_STMT(BPF_RET | BPF_K, spawn), +#endif + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_clone3, 0, 1), + BPF_STMT(BPF_RET | BPF_K, shell ? SECCOMP_RET_ALLOW : SECCOMP_RET_ERRNO | ENOSYS), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_clone, 0, 4), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), + BPF_STMT(BPF_ALU | BPF_AND | BPF_K, thread_mask), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, thread_mask, 1, 0), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + struct sock_fprog program = { .len = sizeof(instructions) / sizeof(instructions[0]), .filter = instructions }; + return syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, + shell ? 0 : SECCOMP_FILTER_FLAG_NEW_LISTENER, &program); +} + +static int transfer_listener(int socket, int listener) { + char marker = 'L'; + struct iovec body = { .iov_base = &marker, .iov_len = 1 }; + union { struct cmsghdr alignment; char bytes[CMSG_SPACE(sizeof(int))]; } control = {0}; + struct msghdr message = { .msg_iov = &body, .msg_iovlen = 1, + .msg_control = control.bytes, .msg_controllen = sizeof(control.bytes) }; + struct cmsghdr *header = CMSG_FIRSTHDR(&message); + header->cmsg_level = SOL_SOCKET; + header->cmsg_type = SCM_RIGHTS; + header->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(header), &listener, sizeof(listener)); + return sendmsg(socket, &message, MSG_NOSIGNAL) == 1 ? 0 : -1; +} + +static int wait_readable(int fd) { + struct pollfd pending = { .fd = fd, .events = POLLIN }; + return poll(&pending, 1, 10000) == 1 && (pending.revents & POLLIN) != 0 ? 0 : -1; +} + +static int receive_listener(int socket) { + if (wait_readable(socket) != 0) return -1; + char marker = 0; + struct iovec body = { .iov_base = &marker, .iov_len = 1 }; + union { struct cmsghdr alignment; char bytes[CMSG_SPACE(sizeof(int))]; } control = {0}; + struct msghdr message = { .msg_iov = &body, .msg_iovlen = 1, + .msg_control = control.bytes, .msg_controllen = sizeof(control.bytes) }; + if (recvmsg(socket, &message, MSG_CMSG_CLOEXEC) != 1 || marker != 'L' || + (message.msg_flags & (MSG_CTRUNC | MSG_TRUNC)) != 0) return -1; + struct cmsghdr *header = CMSG_FIRSTHDR(&message); + if (header == NULL || header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS || + header->cmsg_len != CMSG_LEN(sizeof(int))) return -1; + int fd; + memcpy(&fd, CMSG_DATA(header), sizeof(fd)); + return fd; +} + +static int continue_initial_exec(int listener, pid_t expected) { + struct seccomp_notif_sizes sizes; + if (syscall(SYS_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, &sizes) != 0 || + sizes.seccomp_notif < sizeof(struct seccomp_notif) || + sizes.seccomp_notif_resp < sizeof(struct seccomp_notif_resp)) return -1; + struct seccomp_notif *request = calloc(1, sizes.seccomp_notif); + struct seccomp_notif_resp *response = calloc(1, sizes.seccomp_notif_resp); + int result = -1; + if (request == NULL || response == NULL || wait_readable(listener) != 0) goto done; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, request) != 0 || + request->pid != (unsigned int)expected || request->flags != 0 || + request->data.arch != CYBER_ARCH || request->data.nr != SYS_execve || + ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &request->id) != 0) goto done; + response->id = request->id; + response->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE; + result = ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, response); +done: + free(request); + free(response); + return result; +} + +static int initial_exec_boundary(int shell) { + if (shell) return initial_filter(1) == 0 ? 0 : -1; + int channel[2]; + if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, channel) != 0) return -1; + pid_t target = getpid(); + pid_t supervisor = fork(); + if (supervisor < 0) { close(channel[0]); close(channel[1]); return -1; } + if (supervisor == 0) { + // Even with a runtime's private-proc read roots, untrusted target code + // must not inspect the supervisor's memory, environment or listener. + if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) != 0) _exit(2); + close(channel[0]); + close(STDIN_FILENO); + close(STDOUT_FILENO); + int listener = receive_listener(channel[1]); + close(channel[1]); + int result = listener < 0 ? -1 : continue_initial_exec(listener, target); + if (listener >= 0) close(listener); + /* Last listener close makes all future exec requests fail with ENOSYS. + * This supervisor never authorizes a second request, even if queued. */ + _exit(result == 0 ? 0 : 2); + } + close(channel[1]); + int listener = initial_filter(0); + int result = listener < 0 ? -1 : transfer_listener(channel[0], listener); + if (listener >= 0) close(listener); + close(channel[0]); + return result; +} +#endif diff --git a/crates/cyber-sandbox/native/linux_plugin_exec.c b/crates/cyber-sandbox/native/linux_plugin_exec.c new file mode 100644 index 00000000..82b70f2e --- /dev/null +++ b/crates/cyber-sandbox/native/linux_plugin_exec.c @@ -0,0 +1,120 @@ +/* Native Linux startup boundary, promoted from the confinement proof. + * The caller must supply private mount/PID/network namespaces and protected + * subtree masks: Landlock cannot subtract a deny from an allowed ancestor. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "linux_initial_exec.h" + +#define READ_RIGHTS (LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR) +#define WRITE_RIGHTS (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE | \ + LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK | \ + LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_TRUNCATE) +#define HANDLED_RIGHTS (READ_RIGHTS | WRITE_RIGHTS | LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_BLOCK) + +static int add_path(int ruleset, const char *path, uint64_t rights) { + int fd = open(path, O_PATH | O_CLOEXEC); + struct stat info; + if (fd < 0 || fstat(fd, &info) != 0) { + if (fd >= 0) close(fd); + return -1; + } + if (!S_ISDIR(info.st_mode)) { + rights &= LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE; + if (!S_ISREG(info.st_mode)) rights &= ~LANDLOCK_ACCESS_FS_EXECUTE; + } + struct landlock_path_beneath_attr rule = { .allowed_access = rights, .parent_fd = fd }; + int result = syscall(SYS_landlock_add_rule, ruleset, LANDLOCK_RULE_PATH_BENEATH, &rule, 0); + close(fd); + return result; +} + +static int confine(const char *policy) { + int abi = syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 3) { fprintf(stderr, "Landlock ABI >= 3 is mandatory (observed %d)\n", abi); return -1; } + int fd = open(policy, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | O_NOCTTY); + struct stat info; + if (fd < 0 || fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || + info.st_nlink != 1 || info.st_size <= 0 || info.st_size > 65536) { + if (fd >= 0) close(fd); + return -1; + } + size_t size = (size_t)info.st_size, offset = 0; + char *text = calloc(size + 1, 1); + if (text == NULL) { close(fd); return -1; } + while (offset < size) { + ssize_t count = read(fd, text + offset, size - offset); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) { free(text); close(fd); return -1; } + offset += (size_t)count; + } + close(fd); + if (strlen(text) != size) { free(text); return -1; } + struct landlock_ruleset_attr attributes = { .handled_access_fs = HANDLED_RIGHTS }; + /* Passing just the fs member size supports ABI 3 without newer struct fields. */ + int ruleset = syscall(SYS_landlock_create_ruleset, &attributes, sizeof(attributes.handled_access_fs), 0); + if (ruleset < 0) { free(text); return -1; } + int result = -1; + char *state = NULL; + for (char *line = strtok_r(text, "\n", &state); line != NULL; line = strtok_r(NULL, "\n", &state)) { + if ((line[0] != 'R' && line[0] != 'W') || line[1] != '\t' || line[2] != '/' || + strcmp(line + 2, "/") == 0 || strchr(line + 2, '\t') != NULL) goto done; + if (add_path(ruleset, line + 2, line[0] == 'R' ? READ_RIGHTS : WRITE_RIGHTS) != 0) goto done; + } + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) goto done; + result = syscall(SYS_landlock_restrict_self, ruleset, 0); +done: + close(ruleset); + free(text); + return result; +} + +/* The listener supervisor forks only from the dedicated single-threaded helper. + * Fail closed if procfs cannot attest that precondition before Landlock entry. */ +static int single_threaded(void) { + DIR *tasks = opendir("/proc/self/task"); + if (tasks == NULL) return 0; + unsigned int count = 0; + struct dirent *entry; + errno = 0; + while ((entry = readdir(tasks)) != NULL) { + if (entry->d_name[0] != '.') count++; + } + int valid = errno == 0 && count == 1; + closedir(tasks); + return valid; +} + +int cyber_plugin_exec(int argc, const char *const argv[]) { + if (argc < 4 || argv[1][0] != '/' || argv[3][0] != '/') return 2; + int shell; + if (strcmp(argv[2], "--initial-shell=0") == 0) shell = 0; + else if (strcmp(argv[2], "--initial-shell=1") == 0) shell = 1; + else return 2; + if (!single_threaded() || confine(argv[1]) != 0) { + fputs("native Landlock admission failed\n", stderr); + return 2; + } + /* close_range covers descriptors above a lowered RLIMIT_NOFILE too. */ + if (syscall(SYS_close_range, 3U, UINT_MAX, 0) != 0 || + initial_exec_boundary(shell) != 0) { + fputs("native initial-exec policy admission failed\n", stderr); + return 2; + } + /* execv does not modify argv. Rust retains the strings and pointer vector. */ + execv(argv[3], (char *const *)(argv + 3)); + fputs("native initial runtime exec failed\n", stderr); + return 2; +} diff --git a/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs b/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs index 10ea8840..7714cb62 100644 --- a/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs +++ b/crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "linux"))] #[path = "../native_exec.rs"] mod native_exec; @@ -20,9 +20,9 @@ fn main() -> ExitCode { .nth(1) .is_some_and(|arg| arg == "--plugin-native-exec") { - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "linux"))] return native_exec::run(std::env::args_os().skip(1)); - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "linux")))] { eprintln!( "cyber-sandbox-exec: native plugin exec entry is unavailable on this platform" diff --git a/crates/cyber-sandbox/src/native_exec.rs b/crates/cyber-sandbox/src/native_exec.rs index 2babe1b2..e056c3e2 100644 --- a/crates/cyber-sandbox/src/native_exec.rs +++ b/crates/cyber-sandbox/src/native_exec.rs @@ -1,6 +1,6 @@ -//! Dedicated helper-only macOS policy-before-target entry, not execution admission. +//! Dedicated helper-only native policy-before-target entry, not execution admission. //! This private binary module must never be called in the owner process. -//! Callers must first prove profile/image ownership, signature-identity separation, +//! Callers must first prove profile/image ownership, platform identity separation, //! generation authority, environment/namespace isolation and runtime compatibility. use std::ffi::{CString, OsString}; use std::os::unix::ffi::OsStrExt; @@ -10,7 +10,8 @@ const MAX_ARGS: usize = 256; const MAX_BYTES: usize = 64 * 1024; /// Arguments include the native mode flag as argv[0], then absolute profile and -/// target paths and the target's arguments. A successful call replaces this PID. +/// target paths and the target's arguments. Linux requires --initial-shell=0/1 +/// between the profile and target. A successful call replaces this PID. pub fn run(arguments: impl IntoIterator) -> ExitCode { let Some(arguments) = bounded(arguments) else { eprintln!("cyber-sandbox-exec: invalid native plugin exec arguments"); @@ -33,10 +34,18 @@ fn bounded(arguments: impl IntoIterator) -> Option } strings.push(CString::new(value).ok()?); } - if strings.len() < 3 || strings[0].as_bytes() != b"--plugin-native-exec" { + let target = if cfg!(target_os = "linux") { 3 } else { 2 }; + if strings.len() <= target || strings[0].as_bytes() != b"--plugin-native-exec" { return None; } - for argument in &strings[1..3] { + #[cfg(target_os = "linux")] + if !matches!( + strings[2].as_bytes(), + b"--initial-shell=0" | b"--initial-shell=1" + ) { + return None; + } + for argument in [&strings[1], &strings[target]] { if argument.as_bytes().len() > 4096 || !argument.as_bytes().starts_with(b"/") { return None; } @@ -64,6 +73,26 @@ fn execute(arguments: &[CString]) -> i32 { mod tests { use super::*; #[test] + fn valid_native_arguments_require_platform_policy_shape() { + let mut arguments = vec!["--plugin-native-exec", "/profile"]; + if cfg!(target_os = "linux") { + arguments.push("--initial-shell=0"); + } + arguments.extend(["/target", "argument"]); + assert!(bounded(arguments.into_iter().map(OsString::from)).is_some()); + #[cfg(target_os = "linux")] + for flag in ["--initial-shell=2", "--initial-shell=", "-"] { + assert!( + bounded( + ["--plugin-native-exec", "/profile", flag, "/target"] + .into_iter() + .map(OsString::from) + ) + .is_none() + ); + } + } + #[test] fn argument_bounds_refuse_relative_paths_bypass_and_embedded_nul() { for arguments in [ vec!["--plugin-native-exec", "-", "/bin/sh"], diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c b/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c index df602644..5336ffe0 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_native_fds.c @@ -19,7 +19,11 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "-") == 0) { execl(argv[3], argv[3], "fd-check", "500", (char *)NULL); } else { +#ifdef __linux__ + execl(argv[1], argv[1], "--plugin-native-exec", argv[2], "--initial-shell=0", argv[3], "fd-check", "500", (char *)NULL); +#else execl(argv[1], argv[1], "--plugin-native-exec", argv[2], argv[3], "fd-check", "500", (char *)NULL); +#endif } return 2; } diff --git a/crates/cyber-sandbox/tests/native_exec.rs b/crates/cyber-sandbox/tests/native_exec.rs index 7cbb1ec8..c75b808e 100644 --- a/crates/cyber-sandbox/tests/native_exec.rs +++ b/crates/cyber-sandbox/tests/native_exec.rs @@ -14,7 +14,11 @@ fn bypass_or_unsupported_native_mode_never_starts_the_target() { .unwrap(); assert_eq!( output.status.code(), - Some(if cfg!(target_os = "macos") { 64 } else { 69 }) + Some(if cfg!(any(target_os = "macos", target_os = "linux")) { + 64 + } else { + 69 + }) ); assert!(output.stdout.is_empty()); assert!(!String::from_utf8_lossy(&output.stderr).contains("TARGET-MUST-NOT-RUN")); @@ -312,3 +316,7 @@ mod macos { assert!(output.stdout.is_empty()); } } + +#[cfg(target_os = "linux")] +#[path = "support/native_exec_linux.rs"] +mod linux; diff --git a/crates/cyber-sandbox/tests/support/native_exec_linux.rs b/crates/cyber-sandbox/tests/support/native_exec_linux.rs new file mode 100644 index 00000000..0e584bf2 --- /dev/null +++ b/crates/cyber-sandbox/tests/support/native_exec_linux.rs @@ -0,0 +1,303 @@ +//! Real packaged Linux entry; no probe launcher or optional-policy path. +#![cfg(target_os = "linux")] +use serde_json::Value; +use std::io::Write; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; +use std::time::{Duration, Instant}; + +fn output(command: &mut Command, input: Option<&[u8]>) -> Output { + let mut child = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + if let Some(input) = input { + child.stdin.take().unwrap().write_all(input).unwrap(); + } + drop(child.stdin.take()); + let deadline = Instant::now() + Duration::from_secs(15); + loop { + if child.try_wait().unwrap().is_some() { + return child.wait_with_output().unwrap(); + } + if Instant::now() >= deadline { + let _ = child.kill(); + let result = child.wait_with_output().unwrap(); + panic!("native entry did not settle: {:?}", result); + } + std::thread::sleep(Duration::from_millis(10)); + } +} +fn success(command: &mut Command, input: Option<&[u8]>) -> Output { + let result = output(command, input); + assert!(result.status.success(), "{result:?}"); + result +} +struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, + payload: PathBuf, +} +impl Fixture { + fn new() -> Self { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().canonicalize().unwrap(); + for directory in ["project", "private", "scratch"] { + std::fs::create_dir(root.join(directory)).unwrap(); + } + for name in ["package", "project/read", "private/secret"] { + std::fs::write(root.join(name), b"fixture").unwrap(); + } + symlink(root.join("private/secret"), root.join("project/alias")).unwrap(); + let payload = root.join("payload"); + success( + Command::new("cc") + .args(["-Wall", "-Wextra", "-Werror", "-pthread"]) + .arg( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../scripts/probes/plugin-confinement/fixture_native.c"), + ) + .arg("-o") + .arg(&payload), + None, + ); + Self { + _temp: temp, + root, + payload, + } + } + fn policy(&self, read: bool, write: bool) -> PathBuf { + let mut text = String::new(); + for path in ["/usr", "/lib", "/lib64", "/bin"] { + if Path::new(path).exists() { + text.push_str(&format!("R\t{path}\n")); + } + } + for path in [&self.payload, &self.root.join("package")] { + text.push_str(&format!("R\t{}\n", path.display())); + } + text.push_str("W\t/dev/null\n"); + text.push_str(&format!("W\t{}\n", self.root.join("scratch").display())); + if read { + text.push_str(&format!("R\t{}\n", self.root.join("project").display())); + } + if write { + text.push_str(&format!("W\t{}\n", self.root.join("project").display())); + } + let path = self.root.join("policy"); + std::fs::write(&path, text).unwrap(); + path + } + fn command(&self, policy: &Path, shell: bool) -> Command { + let mut command = Command::new(env!("CARGO_BIN_EXE_cyber-sandbox-exec")); + command + .arg("--plugin-native-exec") + .arg(policy) + .arg(if shell { + "--initial-shell=1" + } else { + "--initial-shell=0" + }) + .arg(&self.payload) + .env_clear() + .env("CYBER_PROOF_PRIVATE", self.root.join("private/secret")); + command + } + fn inspect(&self, read: bool, write: bool, shell: bool) -> Value { + let policy = self.policy(read, write); + let mut command = self.command(&policy, shell); + command.arg("inspect"); + for name in [ + "package", + "project/read", + "private/secret", + "scratch/out", + "project/written", + "project/alias", + "private/secret", + "scratch", + "project", + ] { + command.arg(self.root.join(name)); + } + command.args(["0", "0", "1"]); + let result = success(&mut command, Some(b"{\"jsonrpc\":\"2.0\",\"id\":\"native-proof\",\"method\":\"tool/execute\",\"params\":{\"name\":\"native-probe\",\"input\":{}}}\n")); + serde_json::from_slice::(&result.stdout).unwrap()["result"].clone() + } +} + +#[test] +fn native_reads_writes_threads_and_pre_main_policy_are_independent() { + for (read, write) in [(false, true), (true, false)] { + let f = Fixture::new(); + let result = f.inspect(read, write, false); + for key in [ + "stdio_tool", + "package_read", + "thread", + "scratch_write", + "cannot_relax", + "filter_replacement_denied", + ] { + assert_eq!(result[key], true, "{key}: {result}"); + } + for key in [ + "private_read", + "private_write", + "private_list", + "symlink_private_read", + "hardlink_private", + "protected_read", + "protected_write", + "scratch_read", + "constructor_private_read", + "constructor_fork", + "native_fork", + "native_spawn", + "shell", + "thread_shell", + "inherited_descriptor", + ] { + assert_eq!(result[key], false, "{key}: {result}"); + } + assert_eq!(result["project_read"], read); + assert_eq!(result["project_list"], read); + assert_eq!(result["project_write"], write); + assert_eq!(result["written_project_read"], read && write); + assert_eq!(result["thread_reads"], serde_json::json!([read, false])); + assert_eq!( + std::fs::read(f.root.join("private/secret")).unwrap(), + b"fixture" + ); + } +} + +#[test] +fn shell_permission_controls_exec_and_descendants_inherit_read_restrictions() { + let f = Fixture::new(); + let policy = f.policy(true, false); + // Exact same executable and read roots: only the mandatory shell flag varies. + for shell in [false, true] { + let result = success( + f.command(&policy, shell).arg("exec").args(["unused"; 12]), + None, + ); + let value: Value = serde_json::from_slice(&result.stdout).unwrap(); + assert_eq!(value["self_exec"], shell); + let value = f.inspect(true, false, shell); + for key in [ + "shell", + "native_fork", + "native_spawn", + "constructor_fork", + "thread_shell", + ] { + assert_eq!(value[key], shell, "{key}: {value}"); + } + if shell { + assert_eq!(value["child_reads"], serde_json::json!([true, false])); + assert_eq!( + value["native_child_reads"], + serde_json::json!([true, false]) + ); + } + } +} + +#[test] +fn inherited_descriptors_above_lowered_limit_are_closed() { + let f = Fixture::new(); + let payload = f.root.join("fd-payload"); + let wrapper = f.root.join("fd-wrapper"); + let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); + for (source, destination) in [ + ( + manifest.join("../../scripts/probes/plugin-confinement/fixture_exec.c"), + &payload, + ), + ( + manifest.join("tests/fixtures/plugin_native_fds.c"), + &wrapper, + ), + ] { + success( + Command::new("cc") + .args(["-Wall", "-Wextra", "-Werror"]) + .arg(source) + .arg("-o") + .arg(destination), + None, + ); + } + let policy = f.policy(false, false); + let mut text = std::fs::read_to_string(&policy).unwrap(); + text.push_str(&format!("R\t{}\n", payload.display())); + std::fs::write(&policy, text).unwrap(); + for (helper, expected) in [ + ("-", true), + (env!("CARGO_BIN_EXE_cyber-sandbox-exec"), false), + ] { + let result = success( + Command::new(&wrapper) + .arg(helper) + .arg(&policy) + .arg(&payload) + .arg(f.root.join("private/secret")), + None, + ); + let value: Value = serde_json::from_slice(&result.stdout).unwrap(); + assert_eq!(value["inherited_descriptor"], serde_json::json!(expected)); + } +} + +#[test] +fn invalid_profiles_and_missing_shell_policy_refuse_before_target() { + let f = Fixture::new(); + let policy = f.root.join("bad-policy"); + for bytes in [ + Vec::new(), + vec![b'R'; 65537], + b"R\t/usr\0bad\n".to_vec(), + b"R\t/\n".to_vec(), + b"X\t/usr\n".to_vec(), + ] { + std::fs::write(&policy, bytes).unwrap(); + let result = output(f.command(&policy, false).arg("receipt"), None); + assert_eq!(result.status.code(), Some(2), "{result:?}"); + assert!(result.stdout.is_empty()); + } + std::fs::write(&policy, b"R\t/usr\n").unwrap(); + let linked = f.root.join("linked-policy"); + std::fs::hard_link(&policy, &linked).unwrap(); + let alias = f.root.join("policy-symlink"); + symlink(&policy, &alias).unwrap(); + for path in [&linked, &alias, &f.root, &f.root.join("missing")] { + let result = output(f.command(path, false).arg("receipt"), None); + assert_eq!(result.status.code(), Some(2), "{result:?}"); + assert!(result.stdout.is_empty()); + } + std::fs::remove_file(&linked).unwrap(); + std::fs::remove_file(&policy).unwrap(); + success(Command::new("mkfifo").arg(&policy), None); + let started = Instant::now(); + let result = output(f.command(&policy, false).arg("receipt"), None); + assert_eq!(result.status.code(), Some(2)); + assert!( + started.elapsed() < Duration::from_secs(3), + "profile open waited for a FIFO writer" + ); + let result = output( + Command::new(env!("CARGO_BIN_EXE_cyber-sandbox-exec")) + .arg("--plugin-native-exec") + .arg(f.policy(true, true)) + .arg(&f.payload) + .arg("receipt"), + None, + ); + assert_eq!(result.status.code(), Some(64)); + assert!(result.stdout.is_empty()); +} diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md new file mode 100644 index 00000000..d2df8062 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -0,0 +1,72 @@ +# Packaged Linux startup entry + +This extends the packaged entry in PR #66, reusing the existing reviewed native +proof sources rather than replacing them. Task 2.2 remains open: these startup +primitives are not yet wired into production plugin admission or launch. + +The helper-only `--plugin-native-exec` mode accepts an absolute profile, a +mandatory `--initial-shell=0` or `--initial-shell=1`, and an absolute target plus +arguments. There is no unfiltered startup mode. The Rust entry retains the same +256-argument / 64 KiB total and 4096-byte path limits, and never runs in the owner. + +The C entry requires a single-threaded helper (attested through procfs), Landlock +ABI 3 or newer, a bounded regular single-link no-follow profile, and successful +`close_range(3, UINT_MAX, 0)`. Opening a FIFO profile cannot wait for a writer. +Landlock is installed before any target instruction. The shell=false path uses +the proof's one-shot seccomp listener: a nondumpable helper child authorizes the +trusted initial exec once; subsequent exec requests fail after listener close. +The filter rejects replacement, fork/process creation and escape syscalls while +allowing native threads. Shell=true permits descendants; they inherit Landlock +and the escape/filter-replacement restrictions. + +The caller must still establish immutable helper/profile/runtime ownership, +cleared environment, private mount/PID/network namespaces, protected-subtree +masks, generation-bound final admission and runtime compatibility. Landlock +cannot subtract a deny under an allowed ancestor. These tests deliberately use +broad system-runtime roots to isolate the packaged kernel entry. They do not +prove protected-state masking under those roots, network/secret policy, Windows +support, all supported runtimes, or actual plugin-host integration. No support +reduction or completion of tasks 2.1–2.4 is implied. + +## Verification + +On local macOS arm64: both native argument tests and all six existing packaged +native-entry integration tests passed; scoped sandbox all-target Clippy passed. +The SDK's two deprecated Seatbelt calls remain explicitly allowed C warnings. +Linux C compilation and execution cannot run on this macOS host (no local Linux +container runtime was found); native Linux x86_64/arm64 CI evidence is pending. + +Linux CI runs four new packaged-entry regressions (plus the shared refusal test): + +- Read and write independence, pre-main private-read/fork denial, threads, + symlink/private aliases, scratch writes, filter replacement and protected-file + content preservation. +- Identical-target self-exec denied/allowed by shell policy; positive descendants + retain project access and private-read denial. +- A dummy inherited descriptor 500 survives an unconfined control despite a + lowered soft limit of 64, but is closed through the packaged entry. +- Invalid/oversized/NUL/root profiles, FIFO profiles and omitted shell policy + fail before target receipts. + +CI also compiles the C entry with native musl on each Linux architecture. For +Debian's native musl compiler, `-idirafter` adds kernel UAPI directories after +libc's standard headers, following the [GCC include-search contract](https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html). +Full workspace GNU/musl release builds remain separate CI checks. A behavioral +control removes close_range, requires the named descriptor assertion to fail, +restores the exact source, and reruns the positive suite. Build provenance hashes +both native implementations, promoted headers, entry/test sources and fixtures; +test results remain in the associated job logs. + +## Code/security review + +The review covers the binary dispatch, bounded Rust FFI and ownership, C profile +reads, Landlock rules, seccomp architecture guard and listener transfer, and +source/build/test workflow. Domain APIs are unchanged; the platform adapter stays +private to the helper. No shell command strings or profile contents are echoed +by the new entry. The owner must not invoke this C entry in-process. + +The former probe's optional filter and soft-limit descriptor loop are removed +from the packaged path. The original probes are preserved as separate evidence. +Remaining composed-admission and runtime gates above are required before this +entry can replace production plugin launch. This is an implementation review, +not an independent security audit or completed plugin acceptance. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index e48cd8cb..949013ac 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); owner admission and production 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); owner admission and production 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 79a2898e..eadf23ee 100644 --- a/scripts/probes/plugin-native-entry/report.mts +++ b/scripts/probes/plugin-native-entry/report.mts @@ -7,6 +7,8 @@ import { parseArgs } from "node:util"; const { values } = parseArgs({ options: { binary: { type: "string" }, out: { type: "string" } } }); if (!values.binary || !values.out) throw new Error("--binary and --out are required"); const sources = [ + "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", @@ -27,7 +29,7 @@ const report = { recorded_at: new Date().toISOString(), platform: process.platform, architecture: process.arch, checkout: process.env.GITHUB_SHA ?? null, run_id: process.env.GITHUB_RUN_ID ?? null, sources, source_sha256: sourceHash.digest("hex"), packaged_binary: image, - 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 Linux native packaging.", + 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.", }; mkdirSync(dirname(resolve(values.out)), { recursive: true }); writeFileSync(values.out, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 }); From afdf44feb3250f89383dae4725994b388a9a9065 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 07:56:11 -0400 Subject: [PATCH 03/50] test(sandbox): grant fixture null-device reads and exercise argument limits --- crates/cyber-sandbox/src/native_exec.rs | 41 ++++++++++++------- .../tests/support/native_exec_linux.rs | 2 +- .../packaged-linux-entry.md | 10 ++++- .../packaged-native-entry.md | 12 +++--- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/cyber-sandbox/src/native_exec.rs b/crates/cyber-sandbox/src/native_exec.rs index e056c3e2..b23dfcd8 100644 --- a/crates/cyber-sandbox/src/native_exec.rs +++ b/crates/cyber-sandbox/src/native_exec.rs @@ -94,7 +94,7 @@ mod tests { } #[test] fn argument_bounds_refuse_relative_paths_bypass_and_embedded_nul() { - for arguments in [ + for mut arguments in [ vec!["--plugin-native-exec", "-", "/bin/sh"], vec!["--plugin-native-exec", "/profile", "relative"], vec!["wrong-mode", "/profile", "/bin/sh"], @@ -105,21 +105,32 @@ mod tests { "bad\0argument", ], ] { + if cfg!(target_os = "linux") { + arguments.insert(2, "--initial-shell=0"); + } assert!(bounded(arguments.into_iter().map(OsString::from)).is_none()); } - let mut arguments = vec![ - OsString::from("--plugin-native-exec"), - "/profile".into(), - "/target".into(), - ]; - arguments.extend((0..MAX_ARGS).map(|_| OsString::from("arg"))); - assert!(bounded(arguments).is_none()); - let large = vec![ - OsString::from("--plugin-native-exec"), - "/profile".into(), - "/target".into(), - "x".repeat(MAX_BYTES).into(), - ]; - assert!(bounded(large).is_none()); + } + #[test] + fn argument_count_and_byte_limits_accept_boundary_and_refuse_overflow() { + let mut prefix = vec![OsString::from("--plugin-native-exec"), "/profile".into()]; + if cfg!(target_os = "linux") { + prefix.push("--initial-shell=0".into()); + } + prefix.push("/target".into()); + let mut count = prefix.clone(); + count.resize(MAX_ARGS, "argument".into()); + assert!(bounded(count.clone()).is_some()); + count.push("one-too-many".into()); + assert!(bounded(count).is_none()); + let prefix_bytes: usize = prefix + .iter() + .map(|arg| arg.as_os_str().as_bytes().len() + 1) + .sum(); + let mut bytes = prefix.clone(); + bytes.push("x".repeat(MAX_BYTES - prefix_bytes - 1).into()); + assert!(bounded(bytes).is_some()); + prefix.push("x".repeat(MAX_BYTES - prefix_bytes).into()); + assert!(bounded(prefix).is_none()); } } diff --git a/crates/cyber-sandbox/tests/support/native_exec_linux.rs b/crates/cyber-sandbox/tests/support/native_exec_linux.rs index 0e584bf2..891f2ac1 100644 --- a/crates/cyber-sandbox/tests/support/native_exec_linux.rs +++ b/crates/cyber-sandbox/tests/support/native_exec_linux.rs @@ -80,7 +80,7 @@ impl Fixture { for path in [&self.payload, &self.root.join("package")] { text.push_str(&format!("R\t{}\n", path.display())); } - text.push_str("W\t/dev/null\n"); + text.push_str("R\t/dev/null\nW\t/dev/null\n"); text.push_str(&format!("W\t{}\n", self.root.join("scratch").display())); if read { text.push_str(&format!("R\t{}\n", self.root.join("project").display())); diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md index d2df8062..36724466 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -30,11 +30,17 @@ reduction or completion of tasks 2.1–2.4 is implied. ## Verification -On local macOS arm64: both native argument tests and all six existing packaged +On local macOS arm64: all three native argument tests and all six existing packaged native-entry integration tests passed; scoped sandbox all-target Clippy passed. The SDK's two deprecated Seatbelt calls remain explicitly allowed C warnings. Linux C compilation and execution cannot run on this macOS host (no local Linux -container runtime was found); native Linux x86_64/arm64 CI evidence is pending. +container runtime was found). At `6b349331`, native Linux x86_64 CI compiled the +packaged entry and its musl C object, passed both then-present argument tests, +and passed four of five integration tests. The positive shell/descendant fixture +failed because it opens `/dev/null` with O_RDWR but the test profile granted only +writes. The profile now grants read/write for that explicit bootstrap device; +project/private grants remain unchanged. Full current-source Linux x86_64/arm64 +acceptance and the missing-cleanup control remain pending CI. Linux CI runs four new packaged-entry regressions (plus the shared refusal test): diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md index 948f7f90..a9b7e61b 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-native-entry.md @@ -62,8 +62,10 @@ its deadline; it kills/reaps its owned child before reporting that failure. The correct code is restored before the positive suite. Other owner/copy regressions remain required after this packaging change. -The focused workflow tests macOS arm64/Intel and explicit unsupported internal -mode behavior on Linux arm64/x86_64 and Windows. It records the checked-out source +The initial focused workflow tested macOS arm64/Intel and explicit unsupported +internal mode behavior on Linux arm64/x86_64 and Windows. +[The Linux packaging follow-up](packaged-linux-entry.md) adds the Linux entry, +mandatory shell policy and native tests; Windows still reports unavailability. It records the checked-out source hash and built helper digest alongside the job. Tests execute private freshly signed helper copies, so that build digest identifies their input rather than claiming the modified signatures have the same hash. Source provenance is not @@ -72,12 +74,12 @@ the approved Linux runtime support scope stays required for integration. ## Remaining integration and gates -The entry is currently implemented for macOS. Other platforms return the explicit -unavailable-mode result without starting a target. Existing plugin/ordinary-tool +The entry was introduced for macOS; the linked follow-up adds Linux. Windows +returns the explicit unavailable-mode result without starting a target. Existing plugin/ordinary-tool launches retain their existing behavior in this increment. The strict approved platform/runtime contract remains the delivery target. -Task 2.2 remains open for Linux packaging, reviewed runtime resolution, complete +Task 2.2 remains open for native platform acceptance, reviewed runtime resolution, complete private-root/alias composition, immutable image/helper identity, sanitized environment and launch lifetime. The owner must bind its generation to the current plugin/digest/Location and serialize the final launch check with revocation; From 95848c1c1b960d6ceb325ce12340baf88c67807d Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 08:00:57 -0400 Subject: [PATCH 04/50] test(sandbox): exercise packaged helper across native runtime matrix --- .github/workflows/plugin-native-launcher.yml | 62 +++++++++++++++---- .../packaged-linux-entry.md | 33 ++++++++++ scripts/probes/plugin-confinement/run.mts | 52 +++++++++++----- scripts/probes/plugin-native-entry/report.mts | 1 + 4 files changed, 122 insertions(+), 26 deletions(-) diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml index 70bd69d2..de46a212 100644 --- a/.github/workflows/plugin-native-launcher.yml +++ b/.github/workflows/plugin-native-launcher.yml @@ -5,9 +5,7 @@ on: pull_request: paths: - crates/cyber-sandbox/** - - scripts/probes/plugin-confinement/fixture_exec.c - - scripts/probes/plugin-confinement/fixture_native.c - - scripts/probes/plugin-confinement/process_policy.h + - scripts/probes/plugin-confinement/** - Cargo.lock - .github/workflows/plugin-native-launcher.yml - scripts/probes/plugin-native-entry/** @@ -15,9 +13,7 @@ on: branches: [main] paths: - crates/cyber-sandbox/** - - scripts/probes/plugin-confinement/fixture_exec.c - - scripts/probes/plugin-confinement/fixture_native.c - - scripts/probes/plugin-confinement/process_policy.h + - scripts/probes/plugin-confinement/** - Cargo.lock - .github/workflows/plugin-native-launcher.yml - scripts/probes/plugin-native-entry/** @@ -45,6 +41,18 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 + - uses: actions/setup-python@v5 + if: runner.os != 'Windows' + with: + python-version: '3.13' + - uses: oven-sh/setup-bun@v2 + if: runner.os != 'Windows' + with: + bun-version: '1.4.4' + - uses: denoland/setup-deno@v2 + if: runner.os != 'Windows' + with: + deno-version: 'v2.9.8' - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Verify native musl C compilation @@ -54,7 +62,8 @@ jobs: set -euo pipefail sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* sudo apt-get update - sudo apt-get install -y musl-tools + sudo apt-get install -y musl-tools bubblewrap linux-libc-dev strace + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true arch=$(uname -m) musl-gcc -Wall -Wextra -Werror -idirafter "/usr/include/$arch-linux-gnu" -idirafter /usr/include -c crates/cyber-sandbox/native/linux_plugin_exec.c -o "$RUNNER_TEMP/linux-native-musl.o" - name: Verify packaged entry and explicit unsupported-mode behavior @@ -63,6 +72,30 @@ jobs: set -euo pipefail cargo test --locked -p cyber-sandbox --bin cyber-sandbox-exec native_exec cargo test --locked -p cyber-sandbox --test native_exec + - name: Verify packaged entry across supported runtimes + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/packaged-runtime" + helper="$PWD/target/debug/cyber-sandbox-exec" + for runtime in node python bun deno native; do + args=(--runtime "$runtime" --packaged-helper "$helper" --report "$RUNNER_TEMP/packaged-runtime/$runtime.json") + if [[ "$runtime" == python ]]; then + args+=(--python "$(command -v python3)") + elif [[ "$runtime" == bun || "$runtime" == deno ]]; then + args+=(--executable "$(command -v "$runtime")") + fi + if [[ "$RUNNER_OS" == Linux ]]; then + args+=(--initial-exec --trace "$RUNNER_TEMP/packaged-runtime/trace-$runtime") + else + args+=(--macos-signing-runtime) + fi + node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/packaged-runtime/$runtime.log" 2>&1 + done + if [[ "$RUNNER_OS" == macOS ]]; then + node --experimental-strip-types scripts/probes/plugin-confinement/run.mts --runtime python --python /usr/bin/python3 --macos-signing-runtime --packaged-helper "$helper" --report "$RUNNER_TEMP/packaged-runtime/python-system.json" > "$RUNNER_TEMP/packaged-runtime/python-system.log" 2>&1 + fi - name: Verify high-descriptor regression detects missing cleanup if: runner.os == 'Linux' shell: bash @@ -85,9 +118,13 @@ jobs: exit 1 fi cat "$RUNNER_TEMP/native-fd-control.log" - rg 'linux::inherited_descriptors_above_lowered_limit_are_closed .* FAILED' "$RUNNER_TEMP/native-fd-control.log" - rg -F 'left: Bool(true)' "$RUNNER_TEMP/native-fd-control.log" - rg -F 'right: Bool(false)' "$RUNNER_TEMP/native-fd-control.log" + python3 - "$RUNNER_TEMP/native-fd-control.log" <<'PYCONTROL' + import re, sys + from pathlib import Path + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert 'test linux::inherited_descriptors_above_lowered_limit_are_closed ... FAILED' in text + assert 'left: Bool(true)' in text and 'right: Bool(false)' in text + PYCONTROL cp "$RUNNER_TEMP/linux-native-original.c" "$source" cargo test --locked -p cyber-sandbox --test native_exec - name: Record exact source and built helper provenance @@ -105,6 +142,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: native-entry-provenance-${{ matrix.os }} - path: ${{ runner.temp }}/native-entry-provenance.json + path: | + ${{ runner.temp }}/native-entry-provenance.json + ${{ runner.temp }}/packaged-runtime/ + ${{ runner.temp }}/native-fd-control.log if-no-files-found: error retention-days: 14 diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md index 36724466..c8fb62f2 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -76,3 +76,36 @@ from the packaged path. The original probes are preserved as separate evidence. Remaining composed-admission and runtime gates above are required before this entry can replace production plugin launch. This is an implementation review, not an independent security audit or completed plugin acceptance. + +## Packaged runtime matrix + +The existing confinement driver now accepts an explicit `--packaged-helper`. +It copies the distributed binary into the private test root and invokes its +mandatory mode rather than compiling a substitute launcher. On macOS it gives +that helper copy a fresh signing identifier; targets keep their original +signatures. On Linux it requires the one-shot initial-exec mode and existing +private mount/PID/network namespace composition. Python runs its actual reviewed +executable directly with `-I -S`; it does not use the old embedding launcher. + +The unconfined control executes the target directly and must actually retain the +inherited dummy descriptor. Confined cases require that descriptor to be absent, +plus all existing capability expectations. The report records input and executed +helper hashes (different after macOS signing), exact probe sources, runtime and +kernel versions and each of nine cases. The workflow's separate provenance also +hashes the native entry sources, matrix driver and workflow. This binds observed +results without treating build hashes alone as passing enforcement evidence. + +Locally, the packaged matrix passed all nine capability combinations for native, +Node and `/usr/bin/python3` on macOS arm64, with zero unsupported cases. The +unchanged legacy invocation is separately checked to preserve historical proof +coverage. CI adds Node, Python 3.13, Bun, Deno and native on macOS/Linux arm64 and +x86_64, with an additional system Python run on each macOS host. Those hosted +current-source runtime outcomes remain pending; they cannot be inferred from the +three local runtimes or from earlier separately compiled probes. + +At follow-up `afdf44fe`, both native Linux architectures passed all three argument +tests and all five packaged integration tests. Both also produced the exact +missing-cleanup assertion (descriptor present when it must be absent). The jobs +then failed because their log verifier assumed `rg` was installed. That verifier +now uses the already required Python standard library. A fully restored positive +rerun and the expanded packaged-runtime matrix still require current-source CI. diff --git a/scripts/probes/plugin-confinement/run.mts b/scripts/probes/plugin-confinement/run.mts index dd849b7c..87f18326 100644 --- a/scripts/probes/plugin-confinement/run.mts +++ b/scripts/probes/plugin-confinement/run.mts @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; type Runtime = { kind: "python" | "node" | "bun" | "deno" | "native"; executable: string; prefix: string; include: string; libdir: string; library?: string; version: string }; -type Artifacts = { executable: string; addon?: string; landlock?: string; initialExec: boolean; stagedPlatform?: boolean; signingIdentifier?: string; trace?: string }; +type Artifacts = { executable: string; addon?: string; landlock?: string; initialExec: boolean; stagedPlatform?: boolean; signingIdentifier?: string; trace?: string; packaged?: boolean }; type Flags = { read: boolean; write: boolean; shell: boolean; unrestricted?: boolean }; type Observation = Record; const source = dirname(fileURLToPath(import.meta.url)); @@ -30,7 +30,7 @@ function pythonInfo(program: string, embedded = true): Runtime { const query = [ "import json,sys,sysconfig", 'info={"executable":sys.executable,"prefix":sys.base_prefix,"include":sysconfig.get_path("include"),"libdir":sysconfig.get_config_var("LIBDIR"),"framework":sysconfig.get_config_var("PYTHONFRAMEWORK"),"version":sys.version.split()[0]}', - ...embedded ? [] : [ + ...embedded || isLinux ? [] : [ // Framework command-line shims may spawn a second interpreter image. // Resolve the loaded image instead of granting that shim another exec. "import ctypes,os", @@ -93,26 +93,28 @@ function outside(root: string, allowed: string): boolean { return child === ".." || child.startsWith(".." + sep) || isAbsolute(child); } -function build(root: string, runtime: Runtime, initialExec: boolean, signingRuntime = false): Artifacts { +function build(root: string, runtime: Runtime, initialExec: boolean, signingRuntime = false, packagedHelper?: string): Artifacts { const clang = isLinux ? "/usr/bin/cc" : command("/usr/bin/xcrun", ["--find", "clang"]).trim(); const platform = isLinux ? [] : ["-isysroot", command("/usr/bin/xcrun", ["--show-sdk-path"]).trim()]; const warnings = ["-O1", "-Wall", "-Wextra", "-Werror", ...isLinux ? [] : ["-Wno-error=deprecated-declarations"]]; const [major, minor] = runtime.version.split("."); const executable = join(root, "native-proof"); - const landlock = isLinux ? join(root, "landlock-proof") : undefined; - if (landlock) command(clang, [...warnings, join(source, "linux_exec.c"), "-o", landlock]); + const landlock = isLinux ? packagedHelper ? executable : join(root, "landlock-proof") : undefined; + if (packagedHelper) copyFileSync(packagedHelper, executable); + if (landlock && !packagedHelper) command(clang, [...warnings, join(source, "linux_exec.c"), "-o", landlock]); if (runtime.kind === "native") { command(clang, [...platform, ...warnings, "-pthread", join(source, "fixture_native.c"), "-o", runtime.executable]); - command(clang, [...platform, ...warnings, join(source, isLinux ? "native_exec.c" : "macos_exec.c"), "-o", executable]); + if (!packagedHelper) command(clang, [...platform, ...warnings, join(source, isLinux ? "native_exec.c" : "macos_exec.c"), "-o", executable]); return { executable, landlock, initialExec }; } if (runtime.kind !== "python") { const addon = join(root, "native.node"); - command(clang, [...platform, ...warnings, join(source, signingRuntime ? "macos_exec.c" : "native_exec.c"), "-o", executable]); + if (!packagedHelper) command(clang, [...platform, ...warnings, join(source, signingRuntime ? "macos_exec.c" : "native_exec.c"), "-o", executable]); command(clang, [...platform, ...warnings, ...isLinux ? ["-shared", "-fPIC"] : ["-bundle", "-undefined", "dynamic_lookup"], `-I${runtime.include}`, join(source, "macos_node.c"), "-o", addon]); return { executable, addon, landlock, initialExec }; } + if (packagedHelper) return { executable, landlock, initialExec }; if (signingRuntime) { command(clang, [...platform, ...warnings, join(source, "macos_exec.c"), "-o", executable]); return { executable, initialExec }; @@ -215,8 +217,8 @@ function linuxLaunch(artifacts: Artifacts, runtime: Runtime, paths: Record Date: Thu, 29 Oct 2026 08:06:33 -0400 Subject: [PATCH 05/50] test(sandbox): distinguish Deno descriptor control and collect runtime failures --- .github/workflows/plugin-native-launcher.yml | 12 ++++++++++-- .../packaged-linux-entry.md | 14 ++++++++++++-- scripts/probes/plugin-confinement/run.mts | 4 +++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml index de46a212..e195499b 100644 --- a/.github/workflows/plugin-native-launcher.yml +++ b/.github/workflows/plugin-native-launcher.yml @@ -79,6 +79,7 @@ jobs: set -euo pipefail mkdir -p "$RUNNER_TEMP/packaged-runtime" helper="$PWD/target/debug/cyber-sandbox-exec" + matrix_status=0 for runtime in node python bun deno native; do args=(--runtime "$runtime" --packaged-helper "$helper" --report "$RUNNER_TEMP/packaged-runtime/$runtime.json") if [[ "$runtime" == python ]]; then @@ -91,11 +92,18 @@ jobs: else args+=(--macos-signing-runtime) fi - node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/packaged-runtime/$runtime.log" 2>&1 + if ! node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/packaged-runtime/$runtime.log" 2>&1; then + cat "$RUNNER_TEMP/packaged-runtime/$runtime.log" + matrix_status=1 + fi done if [[ "$RUNNER_OS" == macOS ]]; then - node --experimental-strip-types scripts/probes/plugin-confinement/run.mts --runtime python --python /usr/bin/python3 --macos-signing-runtime --packaged-helper "$helper" --report "$RUNNER_TEMP/packaged-runtime/python-system.json" > "$RUNNER_TEMP/packaged-runtime/python-system.log" 2>&1 + if ! node --experimental-strip-types scripts/probes/plugin-confinement/run.mts --runtime python --python /usr/bin/python3 --macos-signing-runtime --packaged-helper "$helper" --report "$RUNNER_TEMP/packaged-runtime/python-system.json" > "$RUNNER_TEMP/packaged-runtime/python-system.log" 2>&1; then + cat "$RUNNER_TEMP/packaged-runtime/python-system.log" + matrix_status=1 + fi fi + exit "$matrix_status" - name: Verify high-descriptor regression detects missing cleanup if: runner.os == 'Linux' shell: bash diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md index c8fb62f2..51943ffa 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -87,8 +87,10 @@ signatures. On Linux it requires the one-shot initial-exec mode and existing private mount/PID/network namespace composition. Python runs its actual reviewed executable directly with `-I -S`; it does not use the old embedding launcher. -The unconfined control executes the target directly and must actually retain the -inherited dummy descriptor. Confined cases require that descriptor to be absent, +The unconfined control executes the target directly. Native, Node, Python and +Bun must actually retain the inherited dummy descriptor; Deno reports it absent +even in this direct control, so that runtime case cannot prove helper cleanup. +The independent native high-descriptor test remains required for that proof. Confined cases require that descriptor to be absent, plus all existing capability expectations. The report records input and executed helper hashes (different after macOS signing), exact probe sources, runtime and kernel versions and each of nine cases. The workflow's separate provenance also @@ -109,3 +111,11 @@ missing-cleanup assertion (descriptor present when it must be absent). The jobs then failed because their log verifier assumed `rg` was installed. That verifier now uses the already required Python standard library. A fully restored positive rerun and the expanded packaged-runtime matrix still require current-source CI. + +At `95848c1c`, Linux x86_64 completed all nine packaged runtime cases for Node, +Python and Bun. Deno then failed only the direct unconfined FD expectation: +it reported the descriptor absent. The driver now records that specific direct +control behavior, without treating it as evidence for native descriptor cleanup. +All confined cases still require absence; the separate native FD test and +missing-cleanup control remain mandatory. The matrix now collects all runtime +failures before exiting nonzero, so one runtime failure cannot hide later ones. diff --git a/scripts/probes/plugin-confinement/run.mts b/scripts/probes/plugin-confinement/run.mts index 87f18326..b0a24edb 100644 --- a/scripts/probes/plugin-confinement/run.mts +++ b/scripts/probes/plugin-confinement/run.mts @@ -324,7 +324,9 @@ function check(root: string, artifacts: Artifacts, runtime: Runtime, name: strin if (!flags.unrestricted) writeFileSync(profile, isLinux ? `shell=${flags.shell ? 1 : 0}\n` : policy(runtime, paths, flags, artifacts.stagedPlatform, artifacts.signingIdentifier), { mode: 0o600 }); const actual = observe(artifacts, runtime, paths, profile, "inspect", flags); const expectations = expected(flags); - if (artifacts.packaged && flags.unrestricted) expectations.inherited_descriptor = true; + // Deno's direct unconfined control reports this descriptor absent too. It + // cannot establish helper cleanup; the dedicated native FD control does that. + if (artifacts.packaged && flags.unrestricted) expectations.inherited_descriptor = runtime.kind !== "deno"; if (artifacts.addon) expectations.startup_artifacts = false; if (isLinux && runtime.kind === "bun") { expectations.proc_root_reads = [flags.read, !!flags.unrestricted]; From bc8509005274dc53f0e730340ac646a72d504099 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 08:17:03 -0400 Subject: [PATCH 06/50] docs(sandbox): record native Linux packaged runtime acceptance --- .../packaged-linux-entry.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md index 51943ffa..deedb029 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -119,3 +119,22 @@ control behavior, without treating it as evidence for native descriptor cleanup. All confined cases still require absence; the separate native FD test and missing-cleanup control remain mandatory. The matrix now collects all runtime failures before exiting nonzero, so one runtime failure cannot hide later ones. + +## Recorded Linux packaged acceptance at `758b86f8` + +The focused [Linux x86_64 job](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40870601578/job/123436350500) +and [Linux arm64 job](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40870601578/job/123436351018) +both pass: native musl C compilation, all three argument tests, all five packaged +integration tests, all nine capability combinations for each of Node, Python, +Bun, Deno and native (45 cases per architecture), the exact missing-cleanup +assertion, and the restored positive integration suite. Windows' focused job +passes its explicit unavailable-mode behavior; this is not Windows confinement. + +Both retained artifacts were inspected. Each of the five runtime reports has +nine rows and no unsupported cases; each input helper digest matches the +retained built-helper digest. Independently recalculated source manifests match +the checked-out PR sources. The retained checkout `1b54f95b` is GitHub's test +merge with parents `d90f4c16` and `758b86f8`, verified through the commit API. +These source/binary bindings accompany actual passing tests, rather than being +substituted for them. macOS hosted packaged matrix execution and the remaining +production admission/integration gates above remain outstanding. From 3a5fa82f6629b35d8db4283dfd927e31a9b727d8 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 08:16:32 -0400 Subject: [PATCH 07/50] feat(sandbox): serialize generation revocation with final launch commit --- .github/workflows/plugin-package-copy.yml | 54 +++- crates/cyber-sandbox/src/plugin_image.rs | 69 +---- .../src/plugin_image/authority.rs | 264 ++++++++++++++++++ crates/cyber-sandbox/tests/plugin_copy.rs | 27 ++ .../image-preparation.md | 5 + .../launch-authority.md | 74 +++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- 7 files changed, 438 insertions(+), 57 deletions(-) create mode 100644 crates/cyber-sandbox/src/plugin_image/authority.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/launch-authority.md diff --git a/.github/workflows/plugin-package-copy.yml b/.github/workflows/plugin-package-copy.yml index 0962012f..031f956c 100644 --- a/.github/workflows/plugin-package-copy.yml +++ b/.github/workflows/plugin-package-copy.yml @@ -47,6 +47,55 @@ jobs: cargo test --locked -p cyber-core --lib plugins::image cargo test --locked -p cyber-sandbox --lib plugin_image cargo test --locked -p cyber-sandbox --test plugin_copy + cargo test --locked -p cyber-sandbox --doc + - name: Verify generation commit controls + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/src/plugin_image/authority.rs + cp "$source" "$RUNNER_TEMP/authority-positive.rs" + trap 'cp "$RUNNER_TEMP/authority-positive.rs" "$source"' EXIT + for control in gate check; do + cp "$RUNNER_TEMP/authority-positive.rs" "$source" + python3 - "$source" "$control" <<'PY' + from pathlib import Path + import sys + path = Path(sys.argv[1]) + text = path.read_text() + if sys.argv[2] == 'gate': + start = text.index(' let _guard = match self.authority.gate.lock() {') + end = text.index(' lease.check()?;', start) + text = text[:start] + text[end:] + else: + assert text.count(' lease.check()?;') == 1 + text = text.replace(' lease.check()?;', '') + path.write_text(text) + PY + if [[ "$control" == gate ]]; then + name=revocation_serializes_with_an_already_committed_start + else + name=revoked_and_dropped_authorities_never_enter_a_commit + fi + if cargo test --locked -p cyber-sandbox --lib "plugin_image::authority::tests::$name" -- --exact > "$RUNNER_TEMP/authority-$control-control.log" 2>&1; then + cat "$RUNNER_TEMP/authority-$control-control.log" + echo "Missing-$control control unexpectedly passed" >&2 + exit 1 + fi + cat "$RUNNER_TEMP/authority-$control-control.log" + python3 - "$RUNNER_TEMP/authority-$control-control.log" "$control" "$name" <<'PY' + from pathlib import Path + import re, sys + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert f'{sys.argv[3]} ... FAILED' in text + if sys.argv[2] == 'gate': + assert 'revocation returned while start was still active' in text or 'committed start lost its generation gate' in text + else: + assert 'revoked start executed' in text + PY + done + cp "$RUNNER_TEMP/authority-positive.rs" "$source" + cargo test --locked -p cyber-sandbox --lib plugin_image - name: Verify owner-process source IO breaks the SQLite lock regression if: runner.os != 'Windows' shell: bash @@ -65,6 +114,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: package-copy-owner-control-${{ matrix.os }} - path: ${{ runner.temp }}/owner-control.log + path: | + ${{ runner.temp }}/owner-control.log + ${{ runner.temp }}/authority-gate-control.log + ${{ runner.temp }}/authority-check-control.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/src/plugin_image.rs b/crates/cyber-sandbox/src/plugin_image.rs index bdf6d1fe..97099e90 100644 --- a/crates/cyber-sandbox/src/plugin_image.rs +++ b/crates/cyber-sandbox/src/plugin_image.rs @@ -33,61 +33,8 @@ pub enum Error { Revoked, } -/// A single owner-controlled generation. Issue a fresh authority for every host -/// incarnation, even when plugin name, location and content digest are unchanged. -/// Leases cannot renew or revoke authority. Dropping this owner revokes them all. -#[derive(Debug)] -pub struct Generation { - signal: tokio::sync::watch::Sender, -} -impl Generation { - pub fn new() -> Self { - let (signal, _) = tokio::sync::watch::channel(false); - Self { signal } - } - pub fn lease(&self) -> Lease { - Lease { - signal: self.signal.subscribe(), - } - } - pub fn revoke(&self) { - self.signal.send_replace(true); - } -} -impl Default for Generation { - fn default() -> Self { - Self::new() - } -} -impl Drop for Generation { - fn drop(&mut self) { - self.revoke(); - } -} - -/// Read-only authority for one generation; cloning it does not keep the owner -/// alive. Re-admission always requires a fresh Generation rather than renewal. -#[derive(Debug, Clone)] -pub struct Lease { - signal: tokio::sync::watch::Receiver, -} -impl Lease { - pub fn is_current(&self) -> bool { - !*self.signal.borrow() && self.signal.has_changed().is_ok() - } - fn check(&self) -> Result<(), Error> { - if self.is_current() { - Ok(()) - } else { - Err(Error::Revoked) - } - } - #[cfg(unix)] - async fn revoked(&self) { - let mut signal = self.signal.clone(); - let _ = signal.wait_for(|revoked| *revoked).await; - } -} +mod authority; +pub use authority::{Generation, Lease}; /// An owned copy, not a sealed or execution-admitted image. Retain this value for /// as long as a launcher or host uses the copy. Do not publish without checking @@ -107,6 +54,18 @@ impl Package { self.lease.check()?; Ok(&self.report.destination) } + /// Serialize the final synchronous start with generation revocation. The + /// callback must actually perform the start, not return a future to run later. + /// This supplies only generation authority: private provenance, immutable + /// runtime/helper binding and native OS admission are still caller obligations. + /// Do not revoke the same generation from the callback (the gate is held). + pub fn commit( + &self, + owner: &Generation, + start: impl FnOnce(&Path) -> T, + ) -> Result { + owner.commit(&self.lease, || start(&self.report.destination)) + } pub fn digest(&self) -> &str { &self.report.digest } diff --git a/crates/cyber-sandbox/src/plugin_image/authority.rs b/crates/cyber-sandbox/src/plugin_image/authority.rs new file mode 100644 index 00000000..9af78f53 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_image/authority.rs @@ -0,0 +1,264 @@ +//! One owner per host incarnation, with a serialized final synchronous effect. +use std::sync::{Arc, Mutex, PoisonError}; + +use super::Error; + +#[derive(Debug)] +struct Authority { + gate: Mutex<()>, + signal: tokio::sync::watch::Sender, +} + +/// The single owner of a host incarnation. Dropping it permanently revokes all +/// leases, even though they retain the private signal used for panic revocation. +#[derive(Debug)] +pub struct Generation { + authority: Arc, +} +impl Generation { + pub fn new() -> Self { + let (signal, _) = tokio::sync::watch::channel(false); + Self { + authority: Arc::new(Authority { + gate: Mutex::new(()), + signal, + }), + } + } + pub fn lease(&self) -> Lease { + Lease { + authority: Arc::clone(&self.authority), + signal: self.authority.signal.subscribe(), + } + } + /// Commit only a lease issued by this same owner, while revocation is + /// excluded. Borrowing the owner also prevents moving/dropping it inside the + /// callback. If revocation won the gate, the callback is never called. Returning a future + /// does not authorize its later execution. Do not call revoke/commit for this + /// same authority from inside the callback; this gate is not reentrant. + /// + /// The callback owns native admission and child lifetime. A successful commit + /// does not keep the generation live: its process supervisor must observe + /// revocation and stop/reap the process before releasing protected resources. + /// + /// ```compile_fail + /// use cyber_sandbox::plugin_image::Generation; + /// let owner = Generation::new(); + /// let lease = owner.lease(); + /// owner.commit(&lease, || drop(owner)); // Cannot drop the borrowed owner. + /// ``` + pub fn commit(&self, lease: &Lease, start: impl FnOnce() -> T) -> Result { + if !Arc::ptr_eq(&self.authority, &lease.authority) { + return Err(Error::Revoked); + } + let _guard = match self.authority.gate.lock() { + Ok(guard) => guard, + Err(_) => { + self.authority.signal.send_replace(true); + return Err(Error::Revoked); + } + }; + lease.check()?; + let _panic_revocation = PanicRevocation(&self.authority.signal); + Ok(start()) + } + /// Wait for any committed synchronous start, then permanently revoke. + /// Process termination/reaping remains the process owner's responsibility. + pub fn revoke(&self) { + let _guard = self + .authority + .gate + .lock() + .unwrap_or_else(PoisonError::into_inner); + self.authority.signal.send_replace(true); + } +} +impl Default for Generation { + fn default() -> Self { + Self::new() + } +} +impl Drop for Generation { + fn drop(&mut self) { + self.revoke(); + } +} + +/// Read-only authority. Cloning a lease cannot renew its owner generation. +#[derive(Debug, Clone)] +pub struct Lease { + authority: Arc, + signal: tokio::sync::watch::Receiver, +} +impl Lease { + pub fn is_current(&self) -> bool { + !*self.signal.borrow() + } + pub(super) fn check(&self) -> Result<(), Error> { + if self.is_current() { + Ok(()) + } else { + Err(Error::Revoked) + } + } + /// Observe permanent revocation, including owner drop and commit panic. + pub async fn revoked(&self) { + let mut signal = self.signal.clone(); + let _ = signal.wait_for(|revoked| *revoked).await; + } +} + +struct PanicRevocation<'a>(&'a tokio::sync::watch::Sender); +impl Drop for PanicRevocation<'_> { + fn drop(&mut self) { + if std::thread::panicking() { + self.0.send_replace(true); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::mpsc; + use std::time::Duration; + + #[test] + fn revoked_and_dropped_authorities_never_enter_a_commit() { + for drop_owner in [false, true] { + let owner = Generation::new(); + let lease = owner.lease(); + if drop_owner { + drop(owner); + } else { + owner.revoke(); + assert_eq!( + owner.commit(&lease, || panic!("revoked start executed")), + Err(Error::Revoked) + ); + } + let fresh = Generation::new(); + assert_eq!( + fresh.commit(&lease, || panic!("revoked start executed")), + Err(Error::Revoked) + ); + assert_eq!(fresh.commit(&fresh.lease(), || 42), Ok(42)); + assert!(!lease.is_current()); + } + } + + #[test] + fn a_different_owner_cannot_commit_a_still_live_lease() { + let original = Generation::new(); + let lease = original.lease(); + let other = Generation::new(); + assert_eq!( + other.commit(&lease, || panic!("foreign generation start executed")), + Err(Error::Revoked) + ); + assert!(lease.is_current()); + assert_eq!(original.commit(&lease, || 42), Ok(42)); + } + + #[tokio::test] + async fn a_panicking_commit_revokes_all_leases_and_wakes_supervision() { + let owner = Generation::new(); + let lease = owner.lease(); + let observer = lease.clone(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + owner.commit(&lease, || panic!("controlled startup panic")) + })); + assert!(result.is_err()); + tokio::time::timeout(Duration::from_secs(1), observer.revoked()) + .await + .unwrap(); + assert!(!observer.is_current()); + assert!(!owner.lease().is_current()); + assert_eq!( + owner.commit(&observer, || panic!("poisoned start executed")), + Err(Error::Revoked) + ); + owner.revoke(); // Poison recovery cannot renew authority or panic on drop. + } + + #[test] + fn revocation_serializes_with_an_already_committed_start() { + let owner = Arc::new(Generation::new()); + let lease = owner.lease(); + let worker_owner = Arc::clone(&owner); + let revoking_owner = Arc::clone(&owner); + let worker_lease = lease.clone(); + let (entered, entrance) = mpsc::channel(); + let (release, released) = mpsc::channel(); + let worker = std::thread::spawn(move || { + worker_owner.commit(&worker_lease, || { + // In addition to the observable ordering below, require mutual + // exclusion deterministically even on a heavily delayed test host. + let protected = matches!( + worker_lease.authority.gate.try_lock(), + Err(std::sync::TryLockError::WouldBlock) + ); + entered.send(()).unwrap(); + released.recv_timeout(Duration::from_secs(5)).unwrap(); + (protected, worker_lease.is_current()) + }) + }); + entrance.recv_timeout(Duration::from_secs(5)).unwrap(); + let (requested, request) = mpsc::channel(); + let (done, completion) = mpsc::channel(); + let revoker = std::thread::spawn(move || { + requested.send(()).unwrap(); + revoking_owner.revoke(); + done.send(()).unwrap(); + }); + request.recv_timeout(Duration::from_secs(5)).unwrap(); + let early = completion.recv_timeout(Duration::from_millis(100)); + release.send(()).unwrap(); + // Settle both threads before assertions, including the missing-gate control. + let result = worker.join().unwrap().unwrap(); + revoker.join().unwrap(); + assert_eq!( + early, + Err(mpsc::RecvTimeoutError::Timeout), + "revocation returned while start was still active" + ); + assert_eq!( + result, + (true, true), + "committed start lost its generation gate" + ); + assert!(!lease.is_current()); + assert_eq!( + owner.commit(&lease, || panic!("late start executed")), + Err(Error::Revoked) + ); + } + + #[cfg(unix)] + #[test] + fn actual_process_start_is_refused_after_revocation() { + let root = tempfile::tempdir().unwrap(); + let owner = Generation::new(); + let lease = owner.lease(); + let marker = root.path().join("committed"); + let mut child = owner + .commit(&lease, || { + std::process::Command::new("/usr/bin/touch") + .arg(&marker) + .spawn() + }) + .unwrap() + .unwrap(); + owner.revoke(); + assert!(child.wait().unwrap().success()); + assert!(marker.is_file()); + let refused = root.path().join("refused"); + assert!(matches!( + owner.commit(&lease, || std::process::Command::new("/usr/bin/touch") + .arg(&refused) + .spawn()), + Err(Error::Revoked) + )); + assert!(!refused.exists()); + } +} diff --git a/crates/cyber-sandbox/tests/plugin_copy.rs b/crates/cyber-sandbox/tests/plugin_copy.rs index e020dc16..dfca3d5d 100644 --- a/crates/cyber-sandbox/tests/plugin_copy.rs +++ b/crates/cyber-sandbox/tests/plugin_copy.rs @@ -362,14 +362,35 @@ async fn replacing_identical_content_cannot_revive_the_previous_package() { .await .unwrap(); let old_path = old.path().unwrap().to_owned(); + assert_eq!( + old.commit(&Generation::new(), |_| panic!( + "foreign package commit executed" + )), + Err(Error::Revoked) + ); + assert_eq!(old.path().unwrap(), old_path); + assert_eq!( + old.commit(&previous, |path| std::fs::read(path.join("main"))) + .unwrap() + .unwrap(), + b"same content" + ); previous.revoke(); assert_eq!(old.path(), Err(Error::Revoked)); + assert_eq!( + old.commit(&previous, |_| panic!("revoked package commit executed")), + Err(Error::Revoked) + ); let current = Generation::new(); let new = prepare(¤t.lease(), helper, &base, &source, &digest) .await .unwrap(); assert_eq!(old.digest(), new.digest()); assert_ne!(new.path().unwrap(), old_path); + assert_eq!( + new.commit(¤t, |path| path.to_owned()).unwrap(), + new.path().unwrap() + ); assert_eq!(old.path(), Err(Error::Revoked)); assert_eq!( prepare(&old_lease, helper, &base, &source, &digest) @@ -380,6 +401,12 @@ async fn replacing_identical_content_cannot_revive_the_previous_package() { assert_eq!(std::fs::read_dir(&base).unwrap().count(), 2); drop(current); assert_eq!(new.path(), Err(Error::Revoked)); + assert_eq!( + new.commit(&Generation::new(), |_| panic!( + "dropped owner package commit executed" + )), + Err(Error::Revoked) + ); drop(old); drop(new); assert_eq!(std::fs::read_dir(&base).unwrap().count(), 0); diff --git a/openspec/changes/complete-plugin-hook-runtime/image-preparation.md b/openspec/changes/complete-plugin-hook-runtime/image-preparation.md index 139835f6..457d7f2d 100644 --- a/openspec/changes/complete-plugin-hook-runtime/image-preparation.md +++ b/openspec/changes/complete-plugin-hook-runtime/image-preparation.md @@ -260,3 +260,8 @@ an execution permit: the native launch commit must serialize its final authority check with revocation and ensure already-running processes stop. Complete private roots, immutable runtime/helper binding and OS policy integration remain required. The existing native proof/design gate and task 2.2 remain open. + + +The [launch-authority follow-up](launch-authority.md) now provides that synchronous +final commit gate and exact owner/lease binding. It does not supply native +admission, stop a running process or complete production host reconciliation. diff --git a/openspec/changes/complete-plugin-hook-runtime/launch-authority.md b/openspec/changes/complete-plugin-hook-runtime/launch-authority.md new file mode 100644 index 00000000..f9ee45a1 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/launch-authority.md @@ -0,0 +1,74 @@ +# Generation-bound final launch commit + +Task 2.2 requires the final process start to serialize with generation revocation. +A path read earlier through `Package::path()` cannot establish that ordering. +The preparation authority now supplies a synchronous final-effect gate, without +claiming native OS admission or complete production host integration. + +`Generation::commit(&lease, start)` checks that the lease belongs to this exact +owner, acquires the shared generation gate, checks revocation again, and performs +the synchronous callback before releasing the gate. `Generation::revoke()` uses +the same gate: revocation that wins prevents the callback; a committed start that +wins completes before revocation returns. An unrelated fresh owner cannot use an +old lease, even if both remain live or the package content is identical. + +`Package::commit(&owner, start)` supplies the owned destination path inside that +same commit, instead of asking the caller to copy a path and check later. This +only establishes process-local generation authority. The callback must perform +the actual synchronous spawn, not return a deferred future. The owner is borrowed +for the complete call; a compile-fail example verifies it cannot be moved/dropped +inside the callback. The gate is non-reentrant: callbacks must not call revoke or +commit on the same authority. + +A startup panic permanently revokes the generation and wakes `Lease::revoked()` +before releasing the poisoned gate. Later commits fail closed. Explicit revoke +and owner drop recover the poisoned mutex only to publish permanent revocation; +they cannot renew the authority. Leases retain the private notification machinery +but never keep owner authority live after `Generation` drops. No plugin-supplied +identifier can manufacture a lease or owner binding. + +The production host must still own one generation per plugin/digest/Location +incarnation, bind all admission work to it, observe revocation, and stop/reap a +running process before releasing its image, profile, scratch and proxy resources. +A successful synchronous spawn can precede revocation; this gate does not itself +terminate that process. Immutable package/runtime/helper admission, namespace and +protected-root policy, native runtime support and actual host launch integration +remain required. No native proof/design gate, platform support requirement or +parent task is closed by this increment. + +## Verification and review + +Regression coverage includes a same-owner revoked commit, owner drop, foreign +live-owner refusal, a concurrent admitted start versus revocation, startup panic +and notification/poison recovery, and a real Unix process positive start followed +by refusal of a second process. The existing actual-helper package replacement +regression now verifies path binding inside commit, refusal through a different +live owner, stale-package refusal and refusal after owner drop. It retains its +identical-content replacement and cleanup checks. + +The concurrency regression settles both worker threads before asserting a +failure, and verifies the gate remains held independently of scheduling delays. +Missing-check and missing-gate behavioral controls, local test results and native +platform CI must be recorded before acceptance. The existing five-platform +package-copy workflow selects these regressions through `plugin_image` and the +actual `plugin_copy` integration target. + +The scoped code/security review covers ownership and binding, watch lifetime, +mutex poisoning and panic ordering, callback reentrancy and asynchronous misuse, +process/resource lifetime, and unchanged preparation cancellation/SQLite lock +behavior. The first draft exposed commit through a lease alone; review tightened +it to require a borrowed matching owner, preventing accidental owner capture/drop +inside the final callback and explicitly rejecting cross-generation authority. +This is an implementation review, not an independent security audit. + +On local macOS arm64, all 14 owner/authority tests and five actual-helper tests +pass; the helper suite's two ignored fixture entrypoints are explicitly invoked +by its SQLite/digest regression. The compile-fail ownership example passes. +Removing the commit gate fails the named serialization test; removing the final +lease check fails the named revoked-start test. The exact positive source is +restored and all 14 owner/authority tests pass again. Scoped sandbox all-target +Clippy passes with Rust warnings denied; its native build reports the two +explicitly permitted deprecated Seatbelt APIs. Formatting/diff checks, workflow +YAML and all Bash blocks pass. Strict OpenSpec passes 107 items, while spec lint +reports 0 errors and 25 warnings. Hosted current-source platform checks remain +pending; local tests do not establish Windows or Linux execution acceptance. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 949013ac..ad817e2a 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); owner admission and production 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; owner admission and production 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. From a4e97c1c164c4b5c8ab6b151b7c4ea8eb8c1b04e Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 08:48:22 -0400 Subject: [PATCH 08/50] fix(plugins): isolate host bootstrap environment from tool overlays --- .../plugin-bootstrap-environment.yml | 98 +++++++++++++ crates/cyber-app/tests/plugins.rs | 67 ++++++++- .../src/plugin_host/environment.rs | 138 ++++++++++++++++++ crates/cyber-tools/src/plugin_host/launch.rs | 37 +---- crates/cyber-tools/src/plugin_host/mod.rs | 1 + docs/local-core-guide.md | 1 + .../bootstrap-environment.md | 66 +++++++++ .../specs/plugins-marketplace/spec.md | 5 + .../complete-plugin-hook-runtime/tasks.md | 2 +- sdk/plugin/README.md | 10 ++ 10 files changed, 389 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/plugin-bootstrap-environment.yml create mode 100644 crates/cyber-tools/src/plugin_host/environment.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml new file mode 100644 index 00000000..7d090f7a --- /dev/null +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -0,0 +1,98 @@ +name: Plugin bootstrap environment + +on: + workflow_dispatch: + pull_request: + paths: + - crates/cyber-tools/src/plugin_host/** + - crates/cyber-app/tests/plugins.rs + - crates/cyber-sandbox/src/env.rs + - .github/workflows/plugin-bootstrap-environment.yml + - Cargo.lock + push: + branches: [main] + paths: + - crates/cyber-tools/src/plugin_host/** + - crates/cyber-app/tests/plugins.rs + - crates/cyber-sandbox/src/env.rs + - .github/workflows/plugin-bootstrap-environment.yml + - Cargo.lock + +permissions: + contents: read + +concurrency: + group: plugin-bootstrap-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + bootstrap: + name: Host bootstrap (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, windows-latest] + env: + CARGO_INCREMENTAL: 0 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Linux sandbox prerequisites + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* + sudo apt-get update + sudo apt-get install -y bubblewrap + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + - name: Verify explicit environment construction + shell: bash + run: cargo test --locked -p cyber-tools --lib plugin_host::environment + - name: Verify real production plugin hosts + if: runner.os != 'Windows' + shell: bash + run: cargo test --locked -p cyber-app --test plugins + - name: Verify unsafe tool overlays fail the production regression + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-tools/src/plugin_host/launch.rs + cp "$source" "$RUNNER_TEMP/plugin-launch-positive.rs" + trap 'cp "$RUNNER_TEMP/plugin-launch-positive.rs" "$source"' EXIT + python3 - <<'PY' + from pathlib import Path + path = Path('crates/cyber-tools/src/plugin_host/launch.rs') + text = path.read_text() + original = ' let environment =\n super::environment::build(plugin.name, plugin.root, &scratch.path, &wrapped.env);' + assert text.count(original) == 1 + replacement = original.replace('let environment =', 'let mut environment =') + '\n environment.extend(launch.config.env.set.clone());' + path.write_text(text.replace(original, replacement)) + PY + if cargo test --locked -p cyber-app --test plugins plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home -- --exact > "$RUNNER_TEMP/plugin-overlay-control.log" 2>&1; then + cat "$RUNNER_TEMP/plugin-overlay-control.log" + echo 'Unsafe-overlay control unexpectedly passed' >&2 + exit 1 + fi + cat "$RUNNER_TEMP/plugin-overlay-control.log" + python3 - "$RUNNER_TEMP/plugin-overlay-control.log" <<'PY' + import re, sys + from pathlib import Path + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert 'plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home ... FAILED' in text + assert 'inspect_bootstrap was not advertised' in text or 'assertion `left == right` failed' in text + PY + cp "$RUNNER_TEMP/plugin-launch-positive.rs" "$source" + cargo test --locked -p cyber-app --test plugins plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home -- --exact + - name: Retain unsafe-overlay counterexample + if: always() && runner.os != 'Windows' + uses: actions/upload-artifact@v4 + with: + name: plugin-overlay-control-${{ matrix.os }} + path: ${{ runner.temp }}/plugin-overlay-control.log + if-no-files-found: warn + retention-days: 14 diff --git a/crates/cyber-app/tests/plugins.rs b/crates/cyber-app/tests/plugins.rs index 16849cc8..7070fc75 100644 --- a/crates/cyber-app/tests/plugins.rs +++ b/crates/cyber-app/tests/plugins.rs @@ -26,7 +26,7 @@ use serde_json::{Value, json}; /// carried. With `restart_marker`, a crash leaves that file behind and every later start /// exits during `initialize`. const HOST: &str = r#" -import json, os, socket, sys, urllib.request +import json, os, socket, sys, urllib.request, urllib.parse BEHAVIOR = json.loads(__BEHAVIOR__) def send(message): sys.stdout.write(json.dumps(message) + "\n") @@ -54,6 +54,7 @@ def probe(port): return "\n".join(out) marker = BEHAVIOR.get("restart_marker") config = None +plugin_root = None while True: line = sys.stdin.readline() if not line: @@ -64,6 +65,7 @@ while True: if marker and os.path.exists(marker): sys.exit(3) config = params.get("config") + plugin_root = params["plugin"]["root"] reply(id, {"protocolVersion": BEHAVIOR.get("version", "1"), "tools": BEHAVIOR["tools"]}) elif method == "tool/execute": name = params["name"] @@ -73,6 +75,25 @@ while True: os._exit(9) if name in BEHAVIOR.get("probe", []): reply(id, {"content": probe(params["input"]["port"])}) + elif name in BEHAVIOR.get("environment", []): + forbidden = ["PYTHONHOME", "PYTHONPATH", "LD_PRELOAD", "LD_LIBRARY_PATH", + "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "NODE_OPTIONS", + "DENO_AUTH_TOKENS", "PROJECT_CALLBACK"] + home = os.environ.get("HOME", "") + paths = [os.environ.get(key, "") for key in ["XDG_CONFIG_HOME", "XDG_CACHE_HOME", + "XDG_STATE_HOME", "XDG_DATA_HOME", "DENO_DIR", "BUN_RUNTIME_TRANSPILER_CACHE_PATH"]] + proxy = urllib.parse.urlsplit(os.environ.get("HTTP_PROXY", "")) + managed_proxy = (proxy.scheme == "http" and proxy.hostname == "127.0.0.1" and bool(proxy.port) + and not proxy.username and not proxy.password and os.environ.get("NO_PROXY") == "" + and os.environ.get("HTTP_PROXY") == os.environ.get("HTTPS_PROXY") == os.environ.get("ALL_PROXY")) + observed = {"managed_proxy": managed_proxy, "untrusted_absent": all(key not in os.environ for key in forbidden), + "private_home": bool(home) and os.path.isdir(home) and home == os.environ.get("TMPDIR"), + "private_paths": bool(home) and all(path and os.path.commonpath([home, path]) == home for path in paths), + "plugin_root": os.environ.get("CYBER_PLUGIN_ROOT") == plugin_root, + "plugin_name": os.environ.get("CYBER_PLUGIN_NAME") == "env-kit", + "fixed_path": os.environ.get("PATH") == "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin", + "stdout_utf8": sys.stdout.encoding.lower().replace("-", "") == "utf8"} + reply(id, {"content": json.dumps(observed)}) elif name in BEHAVIOR.get("secret", []): answer = ask_core("secrets/get", {"key": params["input"]["key"]}) reply(id, {"content": json.dumps(answer.get("result") or answer.get("error"))}) @@ -421,6 +442,50 @@ async fn plugin_tool_is_forwarded_via_tool_execute() { f.app.runtime.shutdown().await; } +/// Real production spawn: ordinary-tool overlays must not inject Python startup +/// paths, loaders, provider credentials or project environment into the host. +#[tokio::test] +async fn plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home() { + fn reply(body: &Value) -> Value { + if let Some(title) = title(body) { + return title; + } + if after_tool(body) { + return json!({"content": "Done."}); + } + call(&call_id(body), "inspect_bootstrap", json!({})) + } + let f = Fixture::new(reply, json!({"sandbox": {"env": {"allow": ["*"], "set": { + "PYTHONHOME": "/fixture-invalid-python-home", "PYTHONPATH": "/fixture-untrusted-python", + "LD_PRELOAD": "/fixture-untrusted-loader.so", "LD_LIBRARY_PATH": "/fixture-untrusted-library", + "DYLD_INSERT_LIBRARIES": "/fixture-untrusted-loader.dylib", "DYLD_LIBRARY_PATH": "/fixture-untrusted-library", + "NODE_OPTIONS": "--require=/fixture-untrusted-node", "DENO_AUTH_TOKENS": "fixture-only", + "PROJECT_CALLBACK": "fixture-only", "PATH": "/fixture-untrusted-bin", "HOME": "/fixture-owner-home", + "HTTP_PROXY": "http://fixture.invalid:1", "HTTPS_PROXY": "http://fixture.invalid:1", + "ALL_PROXY": "http://fixture.invalid:1", "NO_PROXY": "*" + }}}})).await; + f.install(json!({"name": "env-kit"}), json!({"tools": [read_only("inspect_bootstrap", "Inspect dummy bootstrap", json!({"type": "object"}))], "environment": ["inspect_bootstrap"]}), &[]); + let id = f.session().await; + f.turn(&id, "Inspect the plugin bootstrap", 1).await; + let requests = chats(&f.model); + let advertised = requests + .first() + .is_some_and(|request| tool_names(request).contains(&"inspect_bootstrap".to_owned())); + let results = requests.last().map(tool_results).unwrap_or_default(); + let observed = results + .first() + .map(|result| serde_json::from_str::(result)); + // Settle the real host before assertions, including the unsafe-overlay control. + f.app.runtime.shutdown().await; + assert!(advertised, "inspect_bootstrap was not advertised"); + let observed = observed.unwrap().unwrap(); + assert_eq!( + observed, + json!({"managed_proxy": true, "untrusted_absent": true, "private_home": true, "private_paths": true, + "plugin_root": true, "plugin_name": true, "fixed_path": true, "stdout_utf8": true}) + ); +} + #[tokio::test] async fn plugin_crash_fails_the_call_and_session_continues() { fn reply(body: &Value) -> Value { diff --git a/crates/cyber-tools/src/plugin_host/environment.rs b/crates/cyber-tools/src/plugin_host/environment.rs new file mode 100644 index 00000000..ecd06cfe --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/environment.rs @@ -0,0 +1,138 @@ +//! Explicit plugin bootstrap inputs. Ordinary-tool environment overlays cannot +//! inject loaders, runtime options, credentials or startup configuration here. +use std::collections::BTreeMap; +use std::path::Path; + +#[cfg(unix)] +const CHILD_PATH: &str = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"; + +pub(super) fn build( + name: &str, + root: &Path, + scratch: &Path, + wrapped: &BTreeMap, +) -> BTreeMap { + let mut environment = BTreeMap::from([ + ("LANG".into(), "C".into()), + ("LC_ALL".into(), "C".into()), + ("TERM".into(), "dumb".into()), + ("HOME".into(), text(scratch)), + ("TMPDIR".into(), text(scratch)), + ("TMP".into(), text(scratch)), + ("TEMP".into(), text(scratch)), + ("XDG_CONFIG_HOME".into(), text(&scratch.join("config"))), + ("XDG_CACHE_HOME".into(), text(&scratch.join("cache"))), + ("XDG_DATA_HOME".into(), text(&scratch.join("data"))), + ("XDG_STATE_HOME".into(), text(&scratch.join("state"))), + ("DENO_DIR".into(), text(&scratch.join("deno-cache"))), + ( + "BUN_RUNTIME_TRANSPILER_CACHE_PATH".into(), + text(&scratch.join("bun-cache")), + ), + ("PYTHONNOUSERSITE".into(), "1".into()), + ("PYTHONUTF8".into(), "1".into()), + ("PYTHONIOENCODING".into(), "utf-8".into()), + ("CYBER_PLUGIN_NAME".into(), name.into()), + ("CYBER_PLUGIN_ROOT".into(), text(root)), + ]); + platform(&mut environment, scratch); + // Only the owner's sandbox wrapper supplies proxy endpoints. Inherited and + // configured proxy variables never enter this environment. + environment.extend( + wrapped + .iter() + .filter(|(name, _)| proxy_variable(name)) + .map(|(name, value)| (name.clone(), value.clone())), + ); + cyber_sandbox::owner_environment(environment) + .into_iter() + .collect() +} + +fn text(path: &Path) -> String { + path.to_string_lossy().into_owned() +} +fn proxy_variable(name: &str) -> bool { + matches!( + name.to_ascii_uppercase().as_str(), + "HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" | "NO_PROXY" + ) +} +#[cfg(unix)] +fn platform(environment: &mut BTreeMap, _scratch: &Path) { + environment.insert("PATH".into(), CHILD_PATH.into()); +} +#[cfg(windows)] +fn platform(environment: &mut BTreeMap, scratch: &Path) { + // The system locator comes only from the trusted owner's startup environment, + // never sandbox.env.set. Missing/relative locators cannot create a relative PATH. + let root = std::env::var_os("SystemRoot") + .map(std::path::PathBuf::from) + .filter(|root| root.is_absolute()); + if let Some(root) = root { + environment.insert("SystemRoot".into(), text(&root)); + environment.insert("WINDIR".into(), text(&root)); + environment.insert("COMSPEC".into(), text(&root.join("System32/cmd.exe"))); + environment.insert( + "PATH".into(), + format!("{};{}", root.join("System32").display(), root.display()), + ); + } else { + environment.insert("PATH".into(), String::new()); + } + environment.insert("USERPROFILE".into(), text(scratch)); + environment.insert("APPDATA".into(), text(&scratch.join("config"))); + environment.insert("LOCALAPPDATA".into(), text(&scratch.join("cache"))); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn private_bootstrap_and_proxy_inputs_are_explicit() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("package"); + let scratch = temp.path().join("scratch"); + let wrapped = BTreeMap::from([ + ("HTTP_PROXY".into(), "http://127.0.0.1:3128".into()), + ("NO_PROXY".into(), "".into()), + ("NODE_OPTIONS".into(), "--require=untrusted".into()), + ("UNREVIEWED_BOOTSTRAP".into(), "fixture-only".into()), + ]); + let environment = build("fixture", &root, &scratch, &wrapped); + assert_eq!(environment["CYBER_PLUGIN_ROOT"], text(&root)); + assert_eq!(environment["CYBER_PLUGIN_NAME"], "fixture"); + for key in ["HOME", "TMPDIR", "TMP", "TEMP"] { + assert_eq!(environment[key], text(&scratch)); + } + for key in [ + "XDG_CONFIG_HOME", + "XDG_CACHE_HOME", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + "DENO_DIR", + "BUN_RUNTIME_TRANSPILER_CACHE_PATH", + ] { + assert!(Path::new(&environment[key]).starts_with(&scratch), "{key}"); + } + assert_eq!(environment["HTTP_PROXY"], wrapped["HTTP_PROXY"]); + assert_eq!(environment["NO_PROXY"], ""); + assert!(!environment.contains_key("NODE_OPTIONS")); + assert!(!environment.contains_key("UNREVIEWED_BOOTSTRAP")); + #[cfg(unix)] + assert_eq!(environment["PATH"], CHILD_PATH); + assert_eq!(environment["PYTHONNOUSERSITE"], "1"); + assert_eq!(environment["PYTHONUTF8"], "1"); + } + + #[test] + fn owner_api_values_are_removed_even_from_wrapper_fields() { + let fixture = format!("plugin-environment-owner-{}", std::process::id()); + cyber_sandbox::reserve_owner_credential(&fixture); + let wrapped = BTreeMap::from([("HTTPS_PROXY".into(), fixture)]); + let temp = tempfile::tempdir().unwrap(); + let environment = build("fixture", temp.path(), temp.path(), &wrapped); + assert!(!environment.contains_key("HTTPS_PROXY")); + } +} diff --git a/crates/cyber-tools/src/plugin_host/launch.rs b/crates/cyber-tools/src/plugin_host/launch.rs index 028de945..02f47b6a 100644 --- a/crates/cyber-tools/src/plugin_host/launch.rs +++ b/crates/cyber-tools/src/plugin_host/launch.rs @@ -8,7 +8,6 @@ //! `network: { hosts }` allows only those hosts through the proxy; `network: true` is //! as wide as the resolved sandbox network, never wider ([`super::network`]). A machine without sandbox enforcement does not start plugin hosts. -use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::{Arc, Mutex, PoisonError}; @@ -77,7 +76,8 @@ impl LaunchContext<'_> { ); let wrapped = cyber_sandbox::wrap(&launch, &program, &args) .map_err(|error| format!("plugin {} cannot start: {error}", plugin.name))?; - let environment = self.environment(plugin, &launch.config, &scratch, &wrapped.env); + let environment = + super::environment::build(plugin.name, plugin.root, &scratch.path, &wrapped.env); let process = Process::spawn_with_stdin(&wrapped.program, &wrapped.args, self.helper, |child| { child @@ -140,31 +140,6 @@ impl LaunchContext<'_> { .map(Some) .map_err(|_| format!("plugin {}: network proxy creation failed", plugin.name)) } - - fn environment( - &self, - plugin: &PluginLaunch<'_>, - config: &SandboxConfig, - scratch: &Scratch, - wrapped: &BTreeMap, - ) -> BTreeMap { - let credentials = crate::sandboxing::credential_env_names(&self.resolved.value); - let mut env: BTreeMap = - cyber_sandbox::policy_env(std::env::vars(), config, &credentials) - .into_iter() - .filter(|(key, _)| !proxy_variable(key)) - .collect(); - env.extend(wrapped.clone()); - for key in ["TMPDIR", "TEMP", "TMP"] { - env.insert(key.into(), scratch.path.display().to_string()); - } - env.insert("CYBER_PLUGIN_NAME".into(), plugin.name.into()); - env.insert( - "CYBER_PLUGIN_ROOT".into(), - plugin.root.display().to_string(), - ); - cyber_sandbox::owner_environment(env).into_iter().collect() - } } /// The sandbox of one host: it writes only its scratch directory (plus the Location @@ -202,13 +177,6 @@ fn confinement( } } -fn proxy_variable(key: &str) -> bool { - matches!( - key.to_ascii_uppercase().as_str(), - "HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" | "NO_PROXY" - ) -} - /// The program and arguments that run `main` with its runtime. fn command(plugin: &PluginLaunch<'_>) -> Result<(String, Vec), String> { let relative = cyber_core::plugins::manifest_path(&plugin.host.main) @@ -285,6 +253,7 @@ impl Drop for Scratch { #[cfg(test)] mod tests { use super::*; + use std::collections::BTreeMap; fn launch_of( root: &Path, diff --git a/crates/cyber-tools/src/plugin_host/mod.rs b/crates/cyber-tools/src/plugin_host/mod.rs index 443e59f4..6ea6bb00 100644 --- a/crates/cyber-tools/src/plugin_host/mod.rs +++ b/crates/cyber-tools/src/plugin_host/mod.rs @@ -11,6 +11,7 @@ //! removes exactly its tools from the next step's list, without a server restart. mod connection; +mod environment; mod host; mod launch; pub(crate) mod network; diff --git a/docs/local-core-guide.md b/docs/local-core-guide.md index 64444a8c..0207531a 100644 --- a/docs/local-core-guide.md +++ b/docs/local-core-guide.md @@ -1346,6 +1346,7 @@ A plugin that names `main` runs it as a host process, one per plugin and Locatio - **Tools.** Host tools are offered with the built-in and MCP tools, under the same schema check, hooks, permission rules (`readOnlyHint: true` runs without asking and is offered in plan mode, but only from a plugin that declares none of `fs.write`, `network`, `shell` and `session.prompt`; any other tool asks by default) and output budget, and are deferred like MCP tools above `tool_output.deferred_threshold_tokens`. A built-in name always wins and the first plugin to claim a name keeps it. The first Turn waits for hosts to start; a plugin enabled during a Session offers its tools once its host is running. - **Crashes.** A call whose host exits fails with `plugin crashed` and the Turn goes on. The host restarts after 1 s, then 2, 4, 8 … up to 60 s for crashes in a row; the fifth crash within 10 minutes disables the plugin until the process ends (`plugin disabled after repeated crashes`). Disabling, removing or updating a plugin stops its host before the next step and removes exactly its tools, hooks, commands and agents; no server restart is needed. `GET /api/v1/plugins` shows each host's `state` (`not_started`, `starting`, `running`, `restarting`, `disabled`, `failed`), `error`, `tools` and recent `crashes`. - **Capabilities.** `fs.read`, `fs.write`, `network` (`true`, or `{ "hosts": [...] }` with `*.` wildcards), `shell`, `secrets` and `session.prompt`; anything else fails the manifest. `install` and `enable` print them before committing. The host always runs in the sandbox, whatever the Session's policy: it writes only to its own temporary directory, and to the Location with `fs.write`. Without `network` every connection is refused, and a refused host adds `[sandbox] plugin was denied network access to ` to the call's result; `{ hosts }` allows only those hosts. `fs.read` and `shell` are not enforced yet: a host reads what any sandboxed command reads and may start subprocesses. On Windows the AppContainer grants the host its plugin root and, without `fs.write`, the Location read-only. Where the sandbox is unavailable (Linux without bubblewrap) no host starts. +- **Host environment.** Hosts receive an explicit environment: private scratch for home/temp/cache paths, fixed locale and Python encoding settings, plugin identity/root, and proxy endpoints created by the owner. They do not inherit provider credentials, loader/preload options, runtime search paths, project environment or ordinary-tool `sandbox.env.allow` / `sandbox.env.set` overlays. Unix child command lookup uses `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin`; use an explicit executable path for other child programs. Windows retains the owner's absolute system-root locator for OS bootstrap and uses scratch for profile/application-data paths. This environment hardening does not complete the `fs.read` / `shell` enforcement work described above or reviewed initial runtime resolution. - **Settings.** Values live under `plugins..config` and are checked against `userConfig`; an invalid or missing required value keeps the host down with an error naming the setting. A property marked `secret` is never read from a configuration file: it is stored in the OS keyring (macOS Keychain, Windows Credential Manager; an owner-only `/plugin-secrets.json` on Linux, which every sandboxed command, hook, MCP, LSP and plugin host process is denied, like `/mcp-auth.json`) and reaches the host only through `secrets/get`. Secrets belong to one plugin in one scope and checkout: a same-named plugin in another scope or checkout (say, from a cloned repository) never reads them, and `remove` deletes only the removed entries' secrets. `install`, `enable` and `update` ask for required values that are not set (secrets without echo on a terminal); `cyber plugins configure ` asks for every setting, `--set key=value` sets an ordinary one, and `remove` deletes the stored secrets. - **TypeScript.** `@cyber-code/plugin` (`sdk/plugin`) provides `definePlugin`, `tool` (JSON Schema or Zod-style input), typed hook handlers, `provider` and an in-memory `createHarness`; `export default definePlugin({ tools: { hello: tool({...}) } })` is a complete `main`. diff --git a/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md b/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md new file mode 100644 index 00000000..eda26359 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md @@ -0,0 +1,66 @@ +# Explicit production host environment + +The production `LaunchContext::start` now builds the plugin environment from an +explicit template instead of ordinary-tool `policy_env(std::env::vars(), ...)`. +That former path could carry runtime loader options and arbitrary non-secret +variables; tool environment `allow`/`set` could also inject startup settings or +credentials. A plugin capability must not implicitly delegate that owner state. + +The host gets private scratch for HOME/temp/XDG directories and Bun/Deno caches, +fixed C locale and UTF-8 Python I/O, plugin name/root, and only proxy variables +provided by the owner's sandbox wrapper. Unix child PATH is a fixed system/tool +search list. On Windows, only the absolute system-root locator is additionally +read from the trusted owner's startup environment for OS bootstrap; project/tool +overlays do not supply it. Windows profile/application-data paths use scratch. +The final owner-credential value filter still runs after construction. + +No inherited/configured loader option, language import path, runtime option, +proxy override, user identity, provider credential or arbitrary metadata variable +is included. Plugin settings and secrets continue through validated initialize +configuration and the existing capability-checked secrets callback. Ordinary +tool environment policy is unchanged. The template accepts no owner-environment +iterator or resolved tool policy, so future additions require an explicit review. + +This changes the real host spawn path, but is not complete native admission. +Initial runtime lookup still needs fixed reviewed resolution; implicit runtime +startup/project configuration, immutable helper/runtime/package ownership, +private-root policy, generation-bound launch and full native fs.read/shell +intersections remain open. Separate plugin MCP/hook subprocesses must also be +covered by their required enforcement work. No parent task or Windows native +support gate is closed here. + +## Verification and scoped review + +Unit regressions check private bootstrap paths, owner proxy inputs, rejection of +unreviewed wrapper fields and final owner-credential alias removal. A real +production App/HTTP regression starts the Python host with deliberately invalid +Python loader/search paths, dynamic-loader settings, Node options, Deno auth, +proxy overrides and arbitrary metadata in tool-policy overlays. It requires +successful initialize/tool execution and reports only dummy fixture booleans for +their absence, private paths, identity, fixed PATH and UTF-8 output. It does not +print the owner's environment or credential values. + +Local checks, behavioral controls and hosted platform outcomes must be recorded +before acceptance. The scoped code/security review covers the production call +site, explicit template, proxy source and final reserved-value filtering, +platform differences, credential/configuration channels and omitted runtime +admission gates. No OS enforcement result is inferred from environment tests. + +On local macOS arm64, both template unit tests and all 15 real production plugin +App/HTTP tests pass, including settings changes, secret callbacks and network +intersections. Reintroducing tool-policy overlays at the real spawn call site +makes the named bootstrap test fail because the host cannot initialize under the +deliberately invalid Python prefix. The regression shuts down the App before +asserting, including this failing control. Restoring the exact positive source +makes the bootstrap test pass again. + +Scoped Tools/App Clippy across all targets passes with Rust warnings denied. +Formatting/diff checks and the new workflow's YAML/four Bash blocks pass. Strict +OpenSpec passes all 107 items; spec lint reports 0 errors and 25 warnings. Native +compilation emits the two explicitly permitted deprecated Seatbelt diagnostics; +the App test linker also reports a Mach-O EH-frame size warning. No origin is +assigned to that observed linker warning here. Current-source hosted Linux, +macOS Intel and Windows results remain pending. Windows only runs the template +unit checks; its existing unavailable native-admission scope is not expanded by +this change. The Unix workflow executes the complete production plugin suite, +checks the unsafe-overlay counterexample and restores the positive regression. diff --git a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md index 2f9e3020..a1ac0611 100644 --- a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md +++ b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md @@ -11,6 +11,11 @@ - **WHEN** a plugin with `shell` launches a child without Location read or network capability - **THEN** the child cannot gain those capabilities through the host's broader Session Mode, inherited descriptors or environment +#### Scenario: Tool environment cannot inject plugin bootstrap +- **WHEN** the owner environment or ordinary-tool environment policy contains runtime loader options, search paths, proxy overrides or provider credentials +- **THEN** the plugin host receives only its explicit bootstrap environment and owner-created proxy settings, with its home and cache paths inside private scratch +- **AND** plugin configuration and authorized secrets continue through the host protocol rather than implicit environment inheritance + ### Requirement: Runtime contribution generation ownership (P1) Runtime hooks, providers and catalog transforms SHALL be validated and owned by plugin identity, digest, Location and host generation. Revocation SHALL remove only that generation before later dispatch, and stale results SHALL NOT restore it. Runtime hooks SHALL follow ordinary plugin hook scope and managed-only policy. Provider streams SHALL use normal usage/cancellation paths and SHALL NOT supply trusted reserved bounds by declaration alone. Catalog transforms SHALL be bounded and ordered, preserve policy/configuration ceilings and leave the previous valid view intact on failure. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index ad817e2a..0a417ad7 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; owner admission and production 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; owner admission and production 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/sdk/plugin/README.md b/sdk/plugin/README.md index b1992c58..1c967757 100644 --- a/sdk/plugin/README.md +++ b/sdk/plugin/README.md @@ -56,3 +56,13 @@ npm ci npm run typecheck npm test # CYBER_BIN= also runs the end-to-end test ``` + +### Host bootstrap environment + +Use `initialize` configuration and the authorized `secrets/get` API for plugin +settings and credentials. Host processes receive an explicit bootstrap +environment; ordinary-tool environment overlays and inherited runtime/preload +variables are not passed through. Home, temporary and runtime cache paths point +inside the host's private scratch, which is removed when the host stops. The +owner supplies any permitted proxy endpoint. This does not complete native +filesystem/subprocess capability enforcement or runtime admission. From c812102e54c17fc3df7d004239a1b6207c9f2a8b Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 09:04:37 -0400 Subject: [PATCH 09/50] fix(ci): build sandbox helper before plugin bootstrap acceptance --- .github/workflows/plugin-bootstrap-environment.yml | 7 +++++-- .../bootstrap-environment.md | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index 7d090f7a..ea8830b7 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -6,7 +6,7 @@ on: paths: - crates/cyber-tools/src/plugin_host/** - crates/cyber-app/tests/plugins.rs - - crates/cyber-sandbox/src/env.rs + - crates/cyber-sandbox/** - .github/workflows/plugin-bootstrap-environment.yml - Cargo.lock push: @@ -14,7 +14,7 @@ on: paths: - crates/cyber-tools/src/plugin_host/** - crates/cyber-app/tests/plugins.rs - - crates/cyber-sandbox/src/env.rs + - crates/cyber-sandbox/** - .github/workflows/plugin-bootstrap-environment.yml - Cargo.lock @@ -49,6 +49,9 @@ jobs: sudo apt-get update sudo apt-get install -y bubblewrap sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + - name: Build distributed sandbox helper + shell: bash + run: cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec - name: Verify explicit environment construction shell: bash run: cargo test --locked -p cyber-tools --lib plugin_host::environment diff --git a/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md b/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md index eda26359..99ebbbae 100644 --- a/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md +++ b/openspec/changes/complete-plugin-hook-runtime/bootstrap-environment.md @@ -64,3 +64,14 @@ macOS Intel and Windows results remain pending. Windows only runs the template unit checks; its existing unavailable native-admission scope is not expanded by this change. The Unix workflow executes the complete production plugin suite, checks the unsafe-overlay counterexample and restores the positive regression. + + +The first focused Linux run at `a4e97c1c` compiled successfully but failed 14 of +15 production tests because the fresh job had not built `cyber-sandbox-exec`. +The protocol-version case reported the exact SandboxUnavailableError for the +missing helper. Local macOS had passed with the existing distributed helper in +the shared target cache; that did not verify the fresh Linux job dependency. +The workflow now builds the actual sandbox helper explicitly before environment +and production tests, and watches sandbox sources as a dependency. No unconfined +fallback or skipped host acceptance is introduced. Updated native Linux results +remain required; this correction is not itself a passing Linux execution claim. From 9e802ec1efbdb9ac02b02304db9aae02e08527f1 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 14:48:53 -0400 Subject: [PATCH 10/50] fix(plugins): select package runtime startup configuration --- .../plugin-bootstrap-environment.yml | 99 +++++- crates/cyber-app/tests/plugins.rs | 3 +- crates/cyber-sandbox/src/plugin_image.rs | 49 +++ crates/cyber-sandbox/tests/plugin_copy.rs | 17 + .../src/plugin_host/environment.rs | 1 + crates/cyber-tools/src/plugin_host/launch.rs | 123 ++++++- .../src/plugin_host/startup_tests.rs | 324 ++++++++++++++++++ docs/local-core-guide.md | 1 + .../runtime-startup.md | 116 +++++++ .../specs/plugins-marketplace/spec.md | 6 + .../complete-plugin-hook-runtime/tasks.md | 2 +- sdk/plugin/README.md | 11 + 12 files changed, 743 insertions(+), 9 deletions(-) create mode 100644 crates/cyber-tools/src/plugin_host/startup_tests.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/runtime-startup.md diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index ea8830b7..30b4b8bc 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -38,6 +38,15 @@ jobs: CARGO_INCREMENTAL: 0 steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.4.4' + - uses: denoland/setup-deno@v2 + with: + deno-version: 'v2.9.8' - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Linux sandbox prerequisites @@ -55,6 +64,88 @@ jobs: - name: Verify explicit environment construction shell: bash run: cargo test --locked -p cyber-tools --lib plugin_host::environment + - name: Verify bundled runtime startup receipts + shell: bash + run: | + set -euo pipefail + bun --version + deno --version + cargo test --locked -p cyber-tools --lib plugin_host::launch -- --include-ignored + - name: Verify missing runtime startup filters are detected + shell: bash + run: | + set -euo pipefail + source=crates/cyber-tools/src/plugin_host/launch.rs + cp "$source" "$RUNNER_TEMP/runtime-startup-positive.rs" + trap 'cp "$RUNNER_TEMP/runtime-startup-positive.rs" "$source"' EXIT + for runtime in bun deno; do + cp "$RUNNER_TEMP/runtime-startup-positive.rs" "$source" + python3 - "$runtime" <<'PY' + import sys + from pathlib import Path + path = Path('crates/cyber-tools/src/plugin_host/launch.rs') + text = path.read_text() + start = text.index('fn command(') + end = text.index('\nfn find_program(', start) + body = text[start:end] + flag, replacement = ('"--no-env-file".into(),', '') if sys.argv[1] == 'bun' else ('vec!["--no-config".into()]', 'Vec::new()') + assert body.count(flag) == 1 + body = body.replace(flag, replacement) + path.write_text(text[:start] + body + text[end:]) + PY + if [[ "$runtime" == bun ]]; then + name=bun_starts_bundled_rpc_without_project_preloads_or_dotenv + else + name=deno_starts_bundled_rpc_without_project_configuration_or_lock_changes + fi + if cargo test --locked -p cyber-tools --lib "plugin_host::launch::startup_tests::$name" -- --ignored --exact > "$RUNNER_TEMP/runtime-$runtime-control.log" 2>&1; then + cat "$RUNNER_TEMP/runtime-$runtime-control.log" + echo "Missing $runtime startup filter unexpectedly passed" >&2 + exit 1 + fi + cat "$RUNNER_TEMP/runtime-$runtime-control.log" + python3 - "$RUNNER_TEMP/runtime-$runtime-control.log" "$name" <<'PY' + import re, sys + from pathlib import Path + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert f'{sys.argv[2]} ... FAILED' in text + assert 'project startup code ran before the approved entry' in text or 'runtime startup failed' in text + PY + done + cp "$RUNNER_TEMP/runtime-startup-positive.rs" "$source" + cargo test --locked -p cyber-tools --lib plugin_host::launch -- --include-ignored + - name: Verify prepared workspace boundary is required + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/src/plugin_image.rs + cp "$source" "$RUNNER_TEMP/workspace-boundary-positive.rs" + trap 'cp "$RUNNER_TEMP/workspace-boundary-positive.rs" "$source"' EXIT + python3 - <<'PYCONTROL' + from pathlib import Path + path = Path('crates/cyber-sandbox/src/plugin_image.rs') + text = path.read_text() + original = 'const NAME: &str = "package.json";' + assert text.count(original) == 1 + path.write_text(text.replace(original, 'const NAME: &str = "without-boundary.json";')) + PYCONTROL + name=deno_starts_bundled_rpc_without_project_configuration_or_lock_changes + if cargo test --locked -p cyber-tools --lib "plugin_host::launch::startup_tests::$name" -- --ignored --exact > "$RUNNER_TEMP/workspace-boundary-control.log" 2>&1; then + cat "$RUNNER_TEMP/workspace-boundary-control.log" + echo 'Missing-workspace-boundary control unexpectedly passed' >&2 + exit 1 + fi + cat "$RUNNER_TEMP/workspace-boundary-control.log" + python3 - "$RUNNER_TEMP/workspace-boundary-control.log" <<'PYCONTROL' + from pathlib import Path + import re, sys + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + assert 'deno_starts_bundled_rpc_without_project_configuration_or_lock_changes ... FAILED' in text + assert 'runtime startup failed' in text + PYCONTROL + cp "$RUNNER_TEMP/workspace-boundary-positive.rs" "$source" + cargo test --locked -p cyber-tools --lib plugin_host::launch -- --include-ignored - name: Verify real production plugin hosts if: runner.os != 'Windows' shell: bash @@ -92,10 +183,14 @@ jobs: cp "$RUNNER_TEMP/plugin-launch-positive.rs" "$source" cargo test --locked -p cyber-app --test plugins plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home -- --exact - name: Retain unsafe-overlay counterexample - if: always() && runner.os != 'Windows' + if: always() uses: actions/upload-artifact@v4 with: name: plugin-overlay-control-${{ matrix.os }} - path: ${{ runner.temp }}/plugin-overlay-control.log + path: | + ${{ runner.temp }}/plugin-overlay-control.log + ${{ runner.temp }}/runtime-bun-control.log + ${{ runner.temp }}/runtime-deno-control.log + ${{ runner.temp }}/workspace-boundary-control.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-app/tests/plugins.rs b/crates/cyber-app/tests/plugins.rs index 7070fc75..1692df35 100644 --- a/crates/cyber-app/tests/plugins.rs +++ b/crates/cyber-app/tests/plugins.rs @@ -90,6 +90,7 @@ while True: "private_home": bool(home) and os.path.isdir(home) and home == os.environ.get("TMPDIR"), "private_paths": bool(home) and all(path and os.path.commonpath([home, path]) == home for path in paths), "plugin_root": os.environ.get("CYBER_PLUGIN_ROOT") == plugin_root, + "package_cwd": os.getcwd() == plugin_root, "plugin_name": os.environ.get("CYBER_PLUGIN_NAME") == "env-kit", "fixed_path": os.environ.get("PATH") == "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin", "stdout_utf8": sys.stdout.encoding.lower().replace("-", "") == "utf8"} @@ -481,7 +482,7 @@ async fn plugin_bootstrap_environment_ignores_tool_overlays_and_owns_home() { let observed = observed.unwrap().unwrap(); assert_eq!( observed, - json!({"managed_proxy": true, "untrusted_absent": true, "private_home": true, "private_paths": true, + json!({"package_cwd": true, "managed_proxy": true, "untrusted_absent": true, "private_home": true, "private_paths": true, "plugin_root": true, "plugin_name": true, "fixed_path": true, "stdout_utf8": true}) ); } diff --git a/crates/cyber-sandbox/src/plugin_image.rs b/crates/cyber-sandbox/src/plugin_image.rs index 97099e90..f98a38cd 100644 --- a/crates/cyber-sandbox/src/plugin_image.rs +++ b/crates/cyber-sandbox/src/plugin_image.rs @@ -43,6 +43,10 @@ pub use authority::{Generation, Lease}; pub struct Package { #[cfg(unix)] _root: cap_std::fs::Dir, + #[cfg(unix)] + _namespace: cap_std::fs::Dir, + #[cfg(unix)] + _workspace: cap_std::fs::File, _parent: tempfile::TempDir, report: PreparedCopy, lease: Lease, @@ -253,14 +257,40 @@ fn bind( if (metadata.dev(), metadata.ino()) != report.root_identity { return Err(Error::Binding); } + // A Deno package config otherwise searches arbitrary ancestors for a + // workspace, even when selected by an absolute --config argument. Keep this + // static boundary outside the approved image, so its digest stays unchanged. + // The caller's protected-namespace obligation applies to this file too. + let workspace = bootstrap_boundary(&directory)?; Ok(Package { _root: root, + _namespace: directory, + _workspace: workspace, _parent: parent, report, lease, }) } +#[cfg(unix)] +fn bootstrap_boundary(directory: &cap_std::fs::Dir) -> Result { + use std::io::Write; + use std::os::unix::fs::PermissionsExt; + const NAME: &str = "package.json"; + let mut options = cap_std::fs::OpenOptions::new(); + options.write(true).create_new(true); + let mut file = directory.open_with(NAME, &options).map_err(|_| Error::Io)?; + file.write_all(b"{\"private\":true,\"workspaces\":[]}\n") + .map_err(|_| Error::Io)?; + file.sync_all().map_err(|_| Error::Io)?; + file.set_permissions(cap_std::fs::Permissions::from_std( + std::fs::Permissions::from_mode(0o400), + )) + .map_err(|_| Error::Io)?; + drop(file); + directory.open(NAME).map_err(|_| Error::Io) +} + /// Runtime shutdown can drop the supervisor before its asynchronous reap ends. /// In that case retain the protected namespace rather than racing a live writer. #[cfg(unix)] @@ -279,6 +309,25 @@ mod tests { use super::*; use std::os::unix::fs::MetadataExt; + #[test] + fn workspace_boundary_refuses_existing_files_and_symlinks_without_modifying_them() { + let namespace = tempfile::tempdir().unwrap(); + let directory = + cap_std::fs::Dir::open_ambient_dir(namespace.path(), cap_std::ambient_authority()) + .unwrap(); + let boundary = namespace.path().join("package.json"); + std::fs::write(&boundary, b"existing metadata").unwrap(); + assert!(matches!(bootstrap_boundary(&directory), Err(Error::Io))); + assert_eq!(std::fs::read(&boundary).unwrap(), b"existing metadata"); + std::fs::remove_file(&boundary).unwrap(); + let outside = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(outside.path(), b"outside metadata").unwrap(); + std::os::unix::fs::symlink(outside.path(), &boundary).unwrap(); + assert!(matches!(bootstrap_boundary(&directory), Err(Error::Io))); + assert_eq!(std::fs::read(outside.path()).unwrap(), b"outside metadata"); + assert!(std::fs::symlink_metadata(&boundary).unwrap().is_symlink()); + } + fn worker(root: &Path, control: u8) -> PathBuf { let helper = root.join(format!("helper-{control}")); let output = std::process::Command::new("cc") diff --git a/crates/cyber-sandbox/tests/plugin_copy.rs b/crates/cyber-sandbox/tests/plugin_copy.rs index dfca3d5d..a1187128 100644 --- a/crates/cyber-sandbox/tests/plugin_copy.rs +++ b/crates/cyber-sandbox/tests/plugin_copy.rs @@ -314,6 +314,22 @@ async fn owned_copy_pins_the_actual_worker_result_and_cleans_it_on_drop() { 0o700 ); assert_eq!(package.digest(), digest); + assert_eq!( + cyber_core::plugins::tree_digest(package.path().unwrap()).unwrap(), + digest + ); + let boundary = package + .path() + .unwrap() + .parent() + .unwrap() + .join("package.json"); + let boundary_metadata = std::fs::metadata(&boundary).unwrap(); + assert_eq!(boundary_metadata.permissions().mode() & 0o777, 0o400); + assert_eq!( + serde_json::from_slice::(&std::fs::read(&boundary).unwrap()).unwrap(), + serde_json::json!({"private": true, "workspaces": []}) + ); assert_eq!( std::fs::read(package.path().unwrap().join("main")).unwrap(), b"approved bytes" @@ -321,6 +337,7 @@ async fn owned_copy_pins_the_actual_worker_result_and_cleans_it_on_drop() { let path = package.path().unwrap().to_owned(); drop(package); assert!(!path.exists()); + assert!(!boundary.exists()); assert_eq!(std::fs::read_dir(&base).unwrap().count(), 0); } diff --git a/crates/cyber-tools/src/plugin_host/environment.rs b/crates/cyber-tools/src/plugin_host/environment.rs index ecd06cfe..0b5574cf 100644 --- a/crates/cyber-tools/src/plugin_host/environment.rs +++ b/crates/cyber-tools/src/plugin_host/environment.rs @@ -25,6 +25,7 @@ pub(super) fn build( ("XDG_DATA_HOME".into(), text(&scratch.join("data"))), ("XDG_STATE_HOME".into(), text(&scratch.join("state"))), ("DENO_DIR".into(), text(&scratch.join("deno-cache"))), + ("DENO_NO_UPDATE_CHECK".into(), "1".into()), ( "BUN_RUNTIME_TRANSPILER_CACHE_PATH".into(), text(&scratch.join("bun-cache")), diff --git a/crates/cyber-tools/src/plugin_host/launch.rs b/crates/cyber-tools/src/plugin_host/launch.rs index 02f47b6a..099b36ef 100644 --- a/crates/cyber-tools/src/plugin_host/launch.rs +++ b/crates/cyber-tools/src/plugin_host/launch.rs @@ -83,7 +83,7 @@ impl LaunchContext<'_> { child .env_clear() .envs(&environment) - .current_dir(self.location) + .current_dir(plugin.root) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -193,8 +193,8 @@ fn command(plugin: &PluginLaunch<'_>) -> Result<(String, Vec), String> { let (candidates, args): (&[&str], Vec) = match plugin.host.runtime { Runtime::Native => return Ok((main_arg, Vec::new())), Runtime::Node => (&["node"], vec![main_arg]), - Runtime::Bun => (&["bun"], vec!["run".into(), main_arg]), - Runtime::Deno => (&["deno"], vec!["run".into(), "-A".into(), main_arg]), + Runtime::Bun => (&["bun"], bun_arguments(plugin, main_arg)?), + Runtime::Deno => (&["deno"], deno_arguments(plugin, main_arg)?), Runtime::Python => (&["python3", "python"], vec!["-u".into(), main_arg]), }; let program = candidates @@ -210,6 +210,62 @@ fn command(plugin: &PluginLaunch<'_>) -> Result<(String, Vec), String> { Ok((program.display().to_string(), args)) } +fn bun_arguments(plugin: &PluginLaunch<'_>, main: String) -> Result, String> { + let config = runtime_config(plugin, &["bunfig.toml"])? + .map(|path| path.display().to_string()) + .unwrap_or_else(|| { + if cfg!(windows) { + "NUL".into() + } else { + "/dev/null".into() + } + }); + Ok(vec![ + format!("--config={config}"), + "--no-env-file".into(), + "--no-install".into(), + main, + ]) +} + +fn deno_arguments(plugin: &PluginLaunch<'_>, main: String) -> Result, String> { + let configuration = match runtime_config(plugin, &["deno.json", "deno.jsonc"])? { + Some(path) => vec![format!("--config={}", path.display())], + None => vec!["--no-config".into()], + }; + let lock = match runtime_config(plugin, &["deno.lock"])? { + Some(path) => vec![ + format!("--lock={}", path.display()), + "--frozen-lockfile".into(), + ], + None => vec!["--no-lock".into()], + }; + let mut args = vec!["run".into(), "-A".into()]; + args.extend(configuration); + args.extend(lock); + args.extend(["--node-modules-dir=manual".into(), main]); + Ok(args) +} + +/// Select configuration only inside the approved package. This metadata check +/// does not replace immutable package/ancestor admission at the final launch. +fn runtime_config(plugin: &PluginLaunch<'_>, names: &[&str]) -> Result, String> { + for name in names { + let path = plugin.root.join(name); + match std::fs::symlink_metadata(&path) { + Ok(metadata) if metadata.is_file() => return Ok(Some(path)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + _ => { + return Err(format!( + "plugin {}: runtime configuration must be a regular package file", + plugin.name + )); + } + } + } + Ok(None) +} + fn find_program(name: &str) -> Option { let path = std::env::var_os("PATH")?; let names: Vec = if cfg!(windows) { @@ -312,6 +368,38 @@ mod tests { } } + #[test] + fn runtime_configuration_directories_are_refused_before_startup() { + for (runtime, name) in [ + (Runtime::Bun, "bunfig.toml"), + (Runtime::Deno, "deno.json"), + (Runtime::Deno, "deno.lock"), + ] { + let root = tempfile::tempdir().unwrap(); + std::fs::write(root.path().join("host"), "").unwrap(); + std::fs::create_dir(root.path().join(name)).unwrap(); + assert!( + launch_of(root.path(), "host", runtime) + .unwrap_err() + .contains("runtime configuration must be a regular package file") + ); + } + } + + #[cfg(unix)] + #[test] + fn runtime_configuration_symlinks_are_refused_before_startup() { + let root = tempfile::tempdir().unwrap(); + std::fs::write(root.path().join("host"), "").unwrap(); + let outside = tempfile::NamedTempFile::new().unwrap(); + std::os::unix::fs::symlink(outside.path(), root.path().join("bunfig.toml")).unwrap(); + assert!( + launch_of(root.path(), "host", Runtime::Bun) + .unwrap_err() + .contains("runtime configuration must be a regular package file") + ); + } + #[test] fn each_runtime_starts_main_and_main_must_be_a_regular_file_inside_the_root() { let root = tempfile::tempdir().unwrap(); @@ -322,8 +410,29 @@ mod tests { (main.clone(), Vec::new()) ); for (runtime, args) in [ - (Runtime::Bun, vec!["run".to_owned(), main.clone()]), - (Runtime::Deno, vec!["run".into(), "-A".into(), main.clone()]), + ( + Runtime::Bun, + vec![ + format!( + "--config={}", + if cfg!(windows) { "NUL" } else { "/dev/null" } + ), + "--no-env-file".into(), + "--no-install".into(), + main.clone(), + ], + ), + ( + Runtime::Deno, + vec![ + "run".into(), + "-A".into(), + "--no-config".into(), + "--no-lock".into(), + "--node-modules-dir=manual".into(), + main.clone(), + ], + ), (Runtime::Python, vec!["-u".into(), main.clone()]), (Runtime::Node, vec![main.clone()]), ] { @@ -346,3 +455,7 @@ mod tests { assert!(launch_of(root.path(), "dir", Runtime::Native).is_err()); } } + +#[cfg(test)] +#[path = "startup_tests.rs"] +mod startup_tests; diff --git a/crates/cyber-tools/src/plugin_host/startup_tests.rs b/crates/cyber-tools/src/plugin_host/startup_tests.rs new file mode 100644 index 00000000..cf71566f --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/startup_tests.rs @@ -0,0 +1,324 @@ +//! Actual runtime receipts through the production command/environment builders. +//! This isolates startup behavior; it does not establish native OS confinement. +use super::*; +use serde_json::{Value, json}; +use std::io::Write; +use std::process::{Command, Output}; +use std::time::{Duration, Instant}; + +const HOST: &str = r#" +import readline from 'node:readline'; +import process from 'node:process'; +import {createRequire} from 'node:module'; +const require = createRequire(import.meta.url); +const bundled = require('./sibling.cjs'); +const alias = __BUNDLED_ALIAS__; +const lines = readline.createInterface({input: process.stdin}); +lines.on('line', line => { + const request = JSON.parse(line); + let result; + if (request.method === 'initialize') { + result = {protocolVersion: '1', tools: [{name: 'bootstrap', description: 'fixture', inputSchema: {type: 'object'}}]}; + } else if (request.method === 'tool/execute') { + result = {content: JSON.stringify({bundled, alias, packagePreload: process.env.CYBER_RUNTIME_PACKAGE_PRELOAD === 'fixture-only', preload: process.env.CYBER_RUNTIME_PRELOAD === 'fixture-only', dotenv: process.env.CYBER_RUNTIME_DOTENV === 'fixture-only'})}; + } else if (request.method === 'shutdown') { + process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: request.id, result: null}) + '\n', () => process.exit(0)); + return; + } else { throw new Error('unexpected fixture method'); } + process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: request.id, result}) + '\n'); +}); +"#; + +fn receipt(command: &mut Command) -> Output { + let mut child = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let mut input = String::new(); + for (id, method) in [(1, "initialize"), (2, "tool/execute"), (3, "shutdown")] { + input.push_str(&json!({"jsonrpc": "2.0", "id": id, "method": method, "params": {"name": "bootstrap", "input": {}}}).to_string()); + input.push('\n'); + } + let sent = child.stdin.take().unwrap().write_all(input.as_bytes()); + if let Err(error) = sent + && error.kind() != std::io::ErrorKind::BrokenPipe + { + let _ = child.kill(); + let _ = child.wait(); + panic!("fixture input failed: {error}"); + } + let deadline = Instant::now() + Duration::from_secs(30); + loop { + if child.try_wait().unwrap().is_some() { + return child.wait_with_output().unwrap(); + } + if Instant::now() >= deadline { + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + panic!("runtime bootstrap did not settle: {output:?}"); + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +struct StartupImage { + path: PathBuf, + _generation: cyber_sandbox::plugin_image::Generation, + #[cfg(unix)] + owned: Option, +} + +fn package_configuration(package: &Path, runtime: Runtime, config_name: Option<&str>) { + let bundled_config = config_name.is_some(); + let host_source = HOST.replace( + "__BUNDLED_ALIAS__", + if bundled_config && runtime == Runtime::Deno { + "(await import('fixture-bundled-alias')).default" + } else { + "'no-alias'" + }, + ); + std::fs::write(package.join("host.mjs"), host_source).unwrap(); + if bundled_config { + match runtime { + Runtime::Bun => { + let preload = package.join("package-preload.cjs"); + std::fs::write( + &preload, + "process.env.CYBER_RUNTIME_PACKAGE_PRELOAD = 'fixture-only';", + ) + .unwrap(); + std::fs::write( + package.join("bunfig.toml"), + format!( + "preload = [{}]\n", + serde_json::to_string("./package-preload.cjs").unwrap() + ), + ) + .unwrap(); + } + Runtime::Deno => { + std::fs::write( + package.join(config_name.unwrap()), + json!({"imports": {"fixture-bundled-alias": "./sibling.cjs"}}).to_string(), + ) + .unwrap(); + std::fs::write( + package.join("deno.lock"), + json!({"version":"4","specifiers":{}}).to_string(), + ) + .unwrap(); + } + _ => unreachable!(), + } + } +} + +#[cfg(unix)] +fn startup_image( + package: &Path, + root: &Path, + runtime: Runtime, + config_name: Option<&str>, +) -> StartupImage { + let generation = cyber_sandbox::plugin_image::Generation::new(); + if runtime != Runtime::Deno || config_name.is_none() { + return StartupImage { + path: package.to_owned(), + _generation: generation, + owned: None, + }; + } + let base = root.join("staging"); + std::fs::create_dir(&base).unwrap(); + let digest = cyber_core::plugins::tree_digest(package).unwrap(); + let helper = cyber_sandbox::find_helper().expect("focused workflow builds the sandbox helper"); + let executor = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let owned = executor + .block_on(cyber_sandbox::plugin_image::prepare( + &generation.lease(), + &helper, + &base, + package, + &digest, + )) + .unwrap(); + StartupImage { + path: owned.path().unwrap().to_owned(), + _generation: generation, + owned: Some(owned), + } +} + +#[cfg(windows)] +fn startup_image( + package: &Path, + root: &Path, + runtime: Runtime, + config_name: Option<&str>, +) -> StartupImage { + let generation = cyber_sandbox::plugin_image::Generation::new(); + if runtime != Runtime::Deno || config_name.is_none() { + return StartupImage { + path: package.to_owned(), + _generation: generation, + }; + } + // Windows copy admission remains unavailable; this controlled fixture tests + // only the CLI's workspace-boundary semantics, without claiming OS admission. + let namespace = root.join("staging"); + let image = namespace.join("image"); + std::fs::create_dir_all(&image).unwrap(); + for name in ["host.mjs", "sibling.cjs", config_name.unwrap(), "deno.lock"] { + std::fs::copy(package.join(name), image.join(name)).unwrap(); + } + std::fs::write( + namespace.join("package.json"), + b"{\"private\":true,\"workspaces\":[]}\n", + ) + .unwrap(); + StartupImage { + path: image, + _generation: generation, + } +} + +fn assert_rpc(result: Output, runtime: Runtime, bundled_config: bool) { + assert!( + result.status.success(), + "runtime startup failed ({runtime:?}, bundled={bundled_config}): {result:?}" + ); + let lines: Vec = String::from_utf8(result.stdout) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + assert_eq!(lines.len(), 3); + assert_eq!(lines[0]["id"], 1); + assert_eq!(lines[0]["result"]["protocolVersion"], "1"); + assert_eq!(lines[0]["result"]["tools"][0]["name"], "bootstrap"); + assert_eq!(lines[1]["id"], 2); + let observed: Value = + serde_json::from_str(lines[1]["result"]["content"].as_str().unwrap()).unwrap(); + let alias = if bundled_config && runtime == Runtime::Deno { + "bundled-module" + } else { + "no-alias" + }; + let package_preload = bundled_config && runtime == Runtime::Bun; + assert_eq!( + observed, + json!({"bundled": "bundled-module", "alias": alias, "packagePreload": package_preload, "preload": false, "dotenv": false}), + "project startup code ran before the approved entry" + ); + assert_eq!(lines[2]["id"], 3); + assert_eq!(lines[2]["result"], Value::Null); +} + +fn verify(runtime: Runtime, config_name: Option<&str>) { + let bundled_config = config_name.is_some(); + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().canonicalize().unwrap(); + let project = root.join("project"); + let package = project.join("package"); + let scratch = root.join("scratch"); + for directory in [&project, &package, &scratch] { + std::fs::create_dir(directory).unwrap(); + } + package_configuration(&package, runtime, config_name); + std::fs::write( + package.join("sibling.cjs"), + "module.exports = 'bundled-module';", + ) + .unwrap(); + let preload = project.join("preload.cjs"); + std::fs::write( + &preload, + "process.env.CYBER_RUNTIME_PRELOAD = 'fixture-only';", + ) + .unwrap(); + std::fs::write( + project.join("bunfig.toml"), + format!( + "preload = [{}]\n", + serde_json::to_string(&preload.to_string_lossy()).unwrap() + ), + ) + .unwrap(); + std::fs::write(project.join(".env"), "CYBER_RUNTIME_DOTENV=fixture-only\n").unwrap(); + std::fs::write(package.join(".env"), "CYBER_RUNTIME_DOTENV=fixture-only\n").unwrap(); + let config = "{ deliberately invalid project configuration"; + let lock = "deliberately invalid project lock"; + std::fs::write(project.join("deno.json"), config).unwrap(); + std::fs::write(project.join("deno.lock"), lock).unwrap(); + // A prepared image's private workspace boundary must stop config discovery + // before an unrelated ancestor of the staging namespace can be consulted. + std::fs::write(root.join("deno.json"), "{ invalid ancestor of staging").unwrap(); + let image = startup_image(&package, &root, runtime, config_name); + let package = &image.path; + let host = HostEntry { + main: "host.mjs".into(), + runtime, + }; + let capabilities = Capabilities::default(); + let plugin = PluginLaunch { + name: "runtime-fixture", + root: package, + host: &host, + capabilities: &capabilities, + }; + let (program, args) = + command(&plugin).expect("the focused runtime workflow must install this runtime"); + let environment = + crate::plugin_host::environment::build(plugin.name, package, &scratch, &Default::default()); + let result = receipt( + Command::new(program) + .args(args) + .env_clear() + .envs(environment) + .current_dir(package), + ); + assert_rpc(result, runtime, bundled_config); + assert_eq!( + std::fs::read_to_string(project.join("deno.json")).unwrap(), + config + ); + assert_eq!( + std::fs::read_to_string(project.join("deno.lock")).unwrap(), + lock + ); + assert!(!project.join("node_modules").exists()); + assert!(!package.join("node_modules").exists()); + #[cfg(unix)] + if let Some(owned) = &image.owned { + assert_eq!( + cyber_core::plugins::tree_digest(owned.path().unwrap()).unwrap(), + owned.digest() + ); + assert_eq!( + std::fs::read(owned.path().unwrap().parent().unwrap().join("package.json")).unwrap(), + b"{\"private\":true,\"workspaces\":[]}\n" + ); + } +} + +#[test] +#[ignore = "requires Bun; the focused workflow installs and explicitly executes this receipt"] +fn bun_starts_bundled_rpc_without_project_preloads_or_dotenv() { + for config in [None, Some("bunfig.toml")] { + verify(Runtime::Bun, config); + } +} + +#[test] +#[ignore = "requires Deno; the focused workflow installs and explicitly executes this receipt"] +fn deno_starts_bundled_rpc_without_project_configuration_or_lock_changes() { + for config in [None, Some("deno.json"), Some("deno.jsonc")] { + verify(Runtime::Deno, config); + } +} diff --git a/docs/local-core-guide.md b/docs/local-core-guide.md index 0207531a..4d8f919d 100644 --- a/docs/local-core-guide.md +++ b/docs/local-core-guide.md @@ -1347,6 +1347,7 @@ A plugin that names `main` runs it as a host process, one per plugin and Locatio - **Crashes.** A call whose host exits fails with `plugin crashed` and the Turn goes on. The host restarts after 1 s, then 2, 4, 8 … up to 60 s for crashes in a row; the fifth crash within 10 minutes disables the plugin until the process ends (`plugin disabled after repeated crashes`). Disabling, removing or updating a plugin stops its host before the next step and removes exactly its tools, hooks, commands and agents; no server restart is needed. `GET /api/v1/plugins` shows each host's `state` (`not_started`, `starting`, `running`, `restarting`, `disabled`, `failed`), `error`, `tools` and recent `crashes`. - **Capabilities.** `fs.read`, `fs.write`, `network` (`true`, or `{ "hosts": [...] }` with `*.` wildcards), `shell`, `secrets` and `session.prompt`; anything else fails the manifest. `install` and `enable` print them before committing. The host always runs in the sandbox, whatever the Session's policy: it writes only to its own temporary directory, and to the Location with `fs.write`. Without `network` every connection is refused, and a refused host adds `[sandbox] plugin was denied network access to ` to the call's result; `{ hosts }` allows only those hosts. `fs.read` and `shell` are not enforced yet: a host reads what any sandboxed command reads and may start subprocesses. On Windows the AppContainer grants the host its plugin root and, without `fs.write`, the Location read-only. Where the sandbox is unavailable (Linux without bubblewrap) no host starts. - **Host environment.** Hosts receive an explicit environment: private scratch for home/temp/cache paths, fixed locale and Python encoding settings, plugin identity/root, and proxy endpoints created by the owner. They do not inherit provider credentials, loader/preload options, runtime search paths, project environment or ordinary-tool `sandbox.env.allow` / `sandbox.env.set` overlays. Unix child command lookup uses `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin`; use an explicit executable path for other child programs. Windows retains the owner's absolute system-root locator for OS bootstrap and uses scratch for profile/application-data paths. This environment hardening does not complete the `fs.read` / `shell` enforcement work described above or reviewed initial runtime resolution. +- **Runtime startup.** Host processes start in the plugin package directory. Use `initialize.location.directory` for the Session's workspace rather than assuming the process working directory is that workspace. Bun selects its package's regular `bunfig.toml` (or an empty config), ignores automatic dotenv loading and does not auto-install dependencies. Deno selects regular package `deno.json` / `deno.jsonc`; without one it disables config discovery. Package locks are read frozen; without a package lock it disables lock discovery. Node module directories are managed explicitly, and update checks are disabled. Prepared packages carry a private workspace boundary so Deno does not borrow an ancestor's workspace. The existing native admission/integration work remains required; startup flags are not filesystem or subprocess enforcement. - **Settings.** Values live under `plugins..config` and are checked against `userConfig`; an invalid or missing required value keeps the host down with an error naming the setting. A property marked `secret` is never read from a configuration file: it is stored in the OS keyring (macOS Keychain, Windows Credential Manager; an owner-only `/plugin-secrets.json` on Linux, which every sandboxed command, hook, MCP, LSP and plugin host process is denied, like `/mcp-auth.json`) and reaches the host only through `secrets/get`. Secrets belong to one plugin in one scope and checkout: a same-named plugin in another scope or checkout (say, from a cloned repository) never reads them, and `remove` deletes only the removed entries' secrets. `install`, `enable` and `update` ask for required values that are not set (secrets without echo on a terminal); `cyber plugins configure ` asks for every setting, `--set key=value` sets an ordinary one, and `remove` deletes the stored secrets. - **TypeScript.** `@cyber-code/plugin` (`sdk/plugin`) provides `definePlugin`, `tool` (JSON Schema or Zod-style input), typed hook handlers, `provider` and an in-memory `createHarness`; `export default definePlugin({ tools: { hello: tool({...}) } })` is a complete `main`. diff --git a/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md b/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md new file mode 100644 index 00000000..8d1feb4e --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md @@ -0,0 +1,116 @@ +# Package runtime startup isolation + +Bun and Deno startup now selects configuration from the plugin package rather +than leaving discovery to the Session's Location. Host CWD is the package root; +workspace operations use the Location in initialize. No committed Session +Location transition or CwdChanged event is manufactured by the child CWD choice. + +Bun uses a regular package bunfig.toml or the platform's empty device, with +--no-env-file and --no-install. Relative package preloads therefore resolve from +the package root. Deno selects regular package deno.json/deno.jsonc, or --no-config +when absent. An explicit package lock is frozen; without one, --no-lock prevents +ambient lock discovery. Node module directories remain manual and the explicit +host environment disables Deno's external update check. Directories and symlinks +cannot supply a selected runtime config. These metadata checks are not immutable +source/ancestor admission and do not open source file contents in the owner. + +## Prepared workspace boundary + +Deno 2.9.8 still searches ancestors for a workspace even with an absolute config +argument. The controlled fixture reproduces that failure under a malformed +ancestor config. A Deno workspace listing the image as a member would also change +root-only configuration semantics. Instead, successful Unix Package binding +creates a fixed parent package.json with {"private":true,"workspaces":[]} in its +already-required protected namespace. The empty workspace stops ancestor lookup +without converting the package into a Deno workspace member. This follows the +runtime's [workspace/config model](https://docs.deno.com/runtime/fundamentals/workspaces/) +and is verified with the actual pinned executable, not inferred from the flag. + +The file is outside image/, so the approved image digest and source file count +remain unchanged. Creation refuses an existing entry, writes only fixed bytes, +uses mode 0400, and retains a read-only file plus namespace directory handle until +Package drops. Revocation still rejects publication/access; it does not delete +resources while an owner may still have to stop a process. Bootstrap metadata +adds one fixed-size file and two retained descriptors per prepared package. Only +new owned metadata is opened/closed by this addition, preserving the source-IO +separation needed for owner SQLite locks. + +The caller must protect the namespace and ancestors from every untrusted writer, +retain it through process reaping, and allow the exact boundary file as a reviewed +bootstrap read in native policy. Production host launch does not yet use the +prepared-image path or complete native admission; current raw-source Deno config +selection alone does not certify ancestor isolation. Immutable runtime/helper +resolution, private/alias composition, generation-bound spawn and native +fs.read/shell/network/secret intersections remain required. No enforcement or +support gate is closed here. + +## Regression and platform scope + +Real Bun/Deno processes use the production argv/environment constructors and +exchange initialize, tool/execute and shutdown messages. Cases cover absent and +bundled Bun config, Deno JSON and JSONC import maps, frozen package locks, relative +bundled imports/preloads, poisoned ancestor configuration, dotenv, unchanged +source lock/config and no implicit node_modules. Unix Deno cases with bundled +config use the actual preparation helper and Package ownership. Windows uses +controlled fixture staging only: its copy/native admission remains unavailable, +so those CLI receipts cannot establish Windows confinement. + +The focused workflow installs Bun 1.4.4 and Deno 2.9.8, explicitly runs the two +normally ignored runtime tests, checks missing-filter controls, restores positive +source and retains counterexamples. All parent native integration and complete +plugin acceptance obligations remain open. + +## Local verification (2026-10-29, macOS arm64) + +- Production plugin-host tests: 20 passed, including both explicitly enabled + runtime tests (two Bun and three Deno process cases). Production App plugin + integration tests: 15 passed, including package CWD and environment receipts. +- Sandbox owner/authority unit tests: 15 passed. Actual-helper integration tests: + five passed; two ignored subprocess fixture entrypoints are explicitly invoked + by the SQLite-lock regression, rather than counted as independent passes. + The package test checks unchanged approved image digest, boundary bytes/mode, + and cleanup after owner drop. Boundary collision tests refuse regular files + and symlinks without modifying existing bytes. +- Removing Bun's dotenv filter, Deno's no-config flag, or the recognized parent + package.json boundary each fails its named runtime regression. Restored source + passes. The workflow's exact missing-boundary script was executed locally and + its restored launch suite passed all six tests. +- Scoped Tools/Sandbox/App all-target Clippy passes with Rust warnings denied. + The macOS C compiler reports two deprecated Seatbelt SDK calls; their origin + is not assigned here. +- Formatting and diff checks pass; all eight workflow Bash blocks parse. + Strict OpenSpec validation passes all 107 items. Spec lint reports zero errors + and 25 warnings; this change does not establish their origin. + +Bun 1.4.4 and Deno 2.9.8 came from the official runtime release assets for macOS +arm64. The downloaded archives matched their release API SHA-256 digests: +Bun c240d7f0ccb90c263a849fcdb09b7ee62630d921348ed6aafda86b9d577bc510; +Deno 9f0bf660393da2eaff044cdc7444cb08c73f235849b61d224d825a194ce7c98e. +Only their binaries were extracted into a private temporary tools directory. +No global runtime installation or user PATH change was made. + +Separate existing packaged-helper native matrices passed nine cases per runtime +for these Bun/Deno executables on macOS arm64. Those exercise the experimental +native policy fixture, not this change's bundled-config/ancestor-boundary launch +path. They do not satisfy production admission or cross-platform acceptance. +Hosted results for the new startup matrix must be inspected after publication. + +## Scoped implementation review + +Review covered the production constructors, package lifetime and metadata, +App/owner/CLI regressions, workflow controls, SDK documentation and spec delta. +The runtime helper logic stays within the existing process adapter; no new +protocol or public schema is introduced. Config filenames and argv are explicit, +without shell interpolation. Source contents remain confined to the existing +copy worker/runtime; owner-side selection uses metadata only. The new static +boundary is created exclusively in the owned namespace, retained until Package +drop, and never inserted into the digest-approved image. Tests cover package +preloads/import maps, directory/symlink refusal, digest preservation and cleanup. +No blocking issue was found within this bootstrap scope. + +Remaining security requirements are explicit: metadata selection does not close +source/ancestor races; retained handles do not establish an immutable namespace; +initial runtime lookup still uses owner PATH; production launch is still the +legacy sandbox; prepared-image native integration and protected/alias-root +composition remain unimplemented. Windows fixture receipts prove CLI behavior +only. These are open parent tasks, not claims of completed capability enforcement. diff --git a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md index a1ac0611..21223eae 100644 --- a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md +++ b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md @@ -16,6 +16,12 @@ - **THEN** the plugin host receives only its explicit bootstrap environment and owner-created proxy settings, with its home and cache paths inside private scratch - **AND** plugin configuration and authorized secrets continue through the host protocol rather than implicit environment inheritance +#### Scenario: Package startup does not borrow workspace configuration +- **WHEN** a Bun or Deno host starts for a Location containing unrelated runtime configuration or dotenv files +- **THEN** startup uses its package working directory and explicit package configuration or reviewed empty configuration +- **AND** permitted bundled preloads, imports and frozen package locks remain usable without rewriting the approved package +- **AND** prepared Deno packages retain an owner-created boundary against ancestor workspace discovery while the workspace Location remains in the initialize envelope + ### Requirement: Runtime contribution generation ownership (P1) Runtime hooks, providers and catalog transforms SHALL be validated and owned by plugin identity, digest, Location and host generation. Revocation SHALL remove only that generation before later dispatch, and stale results SHALL NOT restore it. Runtime hooks SHALL follow ordinary plugin hook scope and managed-only policy. Provider streams SHALL use normal usage/cancellation paths and SHALL NOT supply trusted reserved bounds by declaration alone. Catalog transforms SHALL be bounded and ordered, preserve policy/configuration ceilings and leave the previous valid view intact on failure. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 0a417ad7..c905a86f 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; owner admission and production 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; 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/sdk/plugin/README.md b/sdk/plugin/README.md index 1c967757..5579c597 100644 --- a/sdk/plugin/README.md +++ b/sdk/plugin/README.md @@ -66,3 +66,14 @@ variables are not passed through. Home, temporary and runtime cache paths point inside the host's private scratch, which is removed when the host stops. The owner supplies any permitted proxy endpoint. This does not complete native filesystem/subprocess capability enforcement or runtime admission. + +Host working directories are package directories. Use the Location supplied in +`initialize.location.directory` for workspace operations. Bun package config and +Deno package JSON/JSONC config remain available. Automatic dotenv loading, +automatic dependency installation and ambient lock discovery are disabled. +Deno disables config discovery when the package has no config. +Provide bundled dependencies and an up-to-date package lock when one is needed. +Prepared Deno images also need their retained private workspace boundary; selecting +an absolute config path alone does not prevent ancestor workspace discovery. +Production launch still uses the source package; prepared-image integration and +complete native admission remain outstanding. From ef19583fcbfb039608345c4d8d4877f0252d6d60 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:03:03 -0400 Subject: [PATCH 11/50] feat(sandbox): inventory private plugin roots without content IO --- .github/workflows/plugin-package-copy.yml | 57 +++++ crates/cyber-sandbox/src/plugin_image.rs | 1 + .../src/plugin_image/private_roots.rs | 235 ++++++++++++++++++ .../src/plugin_image/private_roots/tests.rs | 167 +++++++++++++ crates/cyber-sandbox/tests/plugin_copy.rs | 14 ++ .../packaged-linux-entry.md | 28 +++ .../private-root-inventory.md | 89 +++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- 8 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 crates/cyber-sandbox/src/plugin_image/private_roots.rs create mode 100644 crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md diff --git a/.github/workflows/plugin-package-copy.yml b/.github/workflows/plugin-package-copy.yml index 031f956c..a37bc758 100644 --- a/.github/workflows/plugin-package-copy.yml +++ b/.github/workflows/plugin-package-copy.yml @@ -109,6 +109,60 @@ jobs: grep -F 'owner lock was released' "$RUNNER_TEMP/owner-control.log" # Restore the positive check after the intentional failing control. cargo test --locked -p cyber-sandbox --test plugin_copy worker_source_io_preserves_owner_sqlite_locks -- --exact + - name: Verify private inventory failure controls + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/src/plugin_image/private_roots.rs + cp "$source" "$RUNNER_TEMP/private-inventory-positive.rs" + trap 'cp "$RUNNER_TEMP/private-inventory-positive.rs" "$source"' EXIT + for control in alias recheck io; do + cp "$RUNNER_TEMP/private-inventory-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 == 'alias': + before, after = '|| (metadata.is_file() && metadata.nlink() != 1)', '' + elif control == 'recheck': + before, after = 'if current.records != self.records {', 'if false {' + else: + before = ' let is_directory = state.directory;' + after = ' if !state.directory { drop(parent.open(name).map_err(|_| Error::Io)?); }\n' + before + assert text.count(before) == 1 + path.write_text(text.replace(before, after)) + PY + if [[ "$control" == alias ]]; then + name=private_inventory_refuses_external_and_internal_hard_links_before_and_after_capture + args=(--lib "plugin_image::private_roots::tests::$name") + elif [[ "$control" == recheck ]]; then + name=private_inventory_accepts_nested_roots_and_detects_changed_bytes_names_and_modes + args=(--lib "plugin_image::private_roots::tests::$name") + else + name=worker_source_io_preserves_owner_sqlite_locks + args=(--test plugin_copy "$name") + fi + if cargo test --locked -p cyber-sandbox "${args[@]}" -- --exact > "$RUNNER_TEMP/private-$control-control.log" 2>&1; then + cat "$RUNNER_TEMP/private-$control-control.log" + echo "Private $control control unexpectedly passed" >&2 + exit 1 + fi + cat "$RUNNER_TEMP/private-$control-control.log" + python3 - "$RUNNER_TEMP/private-$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 = {'alias': 'unwrap_err()', 'recheck': 'assertion failed: inventory.recheck().is_err()', 'io': 'owner lock was released'} + assert expected[sys.argv[3]] in text + PY + done + cp "$RUNNER_TEMP/private-inventory-positive.rs" "$source" + cargo test --locked -p cyber-sandbox --lib plugin_image + cargo test --locked -p cyber-sandbox --test plugin_copy - name: Retain owner IO counterexample if: always() && runner.os != 'Windows' uses: actions/upload-artifact@v4 @@ -116,6 +170,9 @@ jobs: name: package-copy-owner-control-${{ matrix.os }} path: | ${{ runner.temp }}/owner-control.log + ${{ runner.temp }}/private-alias-control.log + ${{ runner.temp }}/private-recheck-control.log + ${{ runner.temp }}/private-io-control.log ${{ runner.temp }}/authority-gate-control.log ${{ runner.temp }}/authority-check-control.log if-no-files-found: warn diff --git a/crates/cyber-sandbox/src/plugin_image.rs b/crates/cyber-sandbox/src/plugin_image.rs index f98a38cd..5b752bd3 100644 --- a/crates/cyber-sandbox/src/plugin_image.rs +++ b/crates/cyber-sandbox/src/plugin_image.rs @@ -34,6 +34,7 @@ pub enum Error { } mod authority; +pub mod private_roots; pub use authority::{Generation, Lease}; /// An owned copy, not a sealed or execution-admitted image. Retain this value for diff --git a/crates/cyber-sandbox/src/plugin_image/private_roots.rs b/crates/cyber-sandbox/src/plugin_image/private_roots.rs new file mode 100644 index 00000000..e242dbd2 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_image/private_roots.rs @@ -0,0 +1,235 @@ +//! Metadata-only alias inventory, not an execution permit or namespace seal. +//! Callers must supply every protected root and exclude untrusted writers before +//! capture/recheck and throughout launch. No regular file is opened by this API. +use std::path::PathBuf; + +#[cfg(unix)] +const MAX_ROOTS: usize = 64; +#[cfg(unix)] +const MAX_ENTRIES: usize = 8192; +#[cfg(unix)] +const MAX_DEPTH: usize = 32; +#[cfg(unix)] +const MAX_PATH: usize = 4096; +#[cfg(unix)] +const MAX_PATH_BYTES: usize = 4 * 1024 * 1024; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("private-root inventory is unavailable on this platform")] + Unavailable, + #[error("invalid private-root inventory request")] + Request, + #[error("private-root metadata could not be inspected")] + Io, + #[error("private-root inventory contains an alias or unsupported object")] + Unsafe, + #[error("private-root inventory changed")] + Changed, + #[error("private-root inventory exceeded its metadata limit")] + Limit, +} + +/// Retains only directory handles and bounded metadata. It deliberately contains +/// no private contents and supplies no way to reopen a private regular file. +#[derive(Debug)] +pub struct Inventory { + #[cfg(unix)] + captured: unix::Captured, +} +impl Inventory { + pub fn capture(roots: &[PathBuf]) -> Result { + #[cfg(unix)] + return Ok(Self { + captured: unix::Captured::read(roots)?, + }); + #[cfg(not(unix))] + { + let _ = roots; + Err(Error::Unavailable) + } + } + /// Compare a fresh metadata inventory with the captured incarnation. This is + /// a check, not exclusion against a writer racing after it returns. + pub fn recheck(&self) -> Result<(), Error> { + #[cfg(unix)] + return self.captured.recheck(); + #[cfg(not(unix))] + Err(Error::Unavailable) + } +} + +#[cfg(unix)] +mod unix { + use super::*; + use cap_fs_ext::DirExt; + use cap_std::fs::{Dir, Metadata, MetadataExt}; + use std::collections::BTreeMap; + use std::os::unix::ffi::OsStrExt; + use std::path::{Component, Path}; + use std::time::SystemTime; + + #[derive(Debug, PartialEq, Eq)] + struct State { + identity: (u64, u64), + directory: bool, + links: u64, + length: u64, + mode: u32, + owner: (u32, u32), + modified: SystemTime, + changed: (i64, i64), + } + impl State { + fn read(metadata: &Metadata) -> Result { + if !(metadata.is_dir() || metadata.is_file()) + || (metadata.is_file() && metadata.nlink() != 1) + { + return Err(Error::Unsafe); + } + Ok(Self { + identity: (metadata.dev(), metadata.ino()), + directory: metadata.is_dir(), + links: metadata.nlink(), + length: metadata.len(), + mode: metadata.mode(), + owner: (metadata.uid(), metadata.gid()), + modified: metadata.modified().map_err(|_| Error::Io)?.into_std(), + changed: (metadata.ctime(), metadata.ctime_nsec()), + }) + } + } + + #[derive(Debug)] + pub(super) struct Captured { + roots: Vec, + _parents: Vec, + records: BTreeMap, + path_bytes: usize, + } + impl Captured { + pub(super) fn read(roots: &[PathBuf]) -> Result { + if roots.is_empty() || roots.len() > MAX_ROOTS { + return Err(Error::Request); + } + for root in roots { + validate(root)?; + if root.file_name().is_none() { + return Err(Error::Request); + } + } + let mut captured = Self { + roots: roots.to_vec(), + _parents: Vec::new(), + records: BTreeMap::new(), + path_bytes: 0, + }; + for root in roots { + let name = root.file_name().ok_or(Error::Request)?; + let parent = directory(root.parent().ok_or(Error::Request)?)?; + captured.visit(&parent, Path::new(name), root, 0)?; + captured._parents.push(parent); + } + Ok(captured) + } + pub(super) fn recheck(&self) -> Result<(), Error> { + let current = Self::read(&self.roots)?; + if current.records != self.records { + return Err(Error::Changed); + } + for (old, new) in self._parents.iter().zip(¤t._parents) { + let old = old.dir_metadata().map_err(|_| Error::Io)?; + let new = new.dir_metadata().map_err(|_| Error::Io)?; + if (old.dev(), old.ino()) != (new.dev(), new.ino()) { + return Err(Error::Changed); + } + } + if current._parents.len() != self._parents.len() { + return Err(Error::Changed); + } + Ok(()) + } + fn insert(&mut self, path: &Path, state: State) -> Result<(), Error> { + if let Some(previous) = self.records.get(path) { + return if previous == &state { + Ok(()) + } else { + Err(Error::Changed) + }; + } + let bytes = path.as_os_str().as_bytes().len(); + if bytes > MAX_PATH + || self.records.len() >= MAX_ENTRIES + || self.path_bytes + bytes > MAX_PATH_BYTES + { + return Err(Error::Limit); + } + self.path_bytes += bytes; + self.records.insert(path.into(), state); + Ok(()) + } + fn visit( + &mut self, + parent: &Dir, + name: &Path, + path: &Path, + depth: usize, + ) -> Result<(), Error> { + if depth > MAX_DEPTH { + return Err(Error::Limit); + } + let state = State::read(&parent.symlink_metadata(name).map_err(|_| Error::Io)?)?; + let is_directory = state.directory; + let identity = state.identity; + self.insert(path, state)?; + if is_directory { + let child = parent.open_dir_nofollow(name).map_err(|_| Error::Io)?; + let opened = State::read(&child.dir_metadata().map_err(|_| Error::Io)?)?; + if opened.identity != identity || self.records.get(path) != Some(&opened) { + return Err(Error::Changed); + } + for entry in child.entries().map_err(|_| Error::Io)? { + let name = entry.map_err(|_| Error::Io)?.file_name(); + self.visit(&child, Path::new(&name), &path.join(&name), depth + 1)?; + } + let settled = State::read(&child.dir_metadata().map_err(|_| Error::Io)?)?; + if self.records.get(path) != Some(&settled) { + return Err(Error::Changed); + } + } + let settled = State::read(&parent.symlink_metadata(name).map_err(|_| Error::Io)?)?; + if self.records.get(path) != Some(&settled) { + return Err(Error::Changed); + } + Ok(()) + } + } + fn validate(path: &Path) -> Result<(), Error> { + if !path.is_absolute() + || path.as_os_str().as_bytes().len() > MAX_PATH + || path + .components() + .any(|part| !matches!(part, Component::RootDir | Component::Normal(_))) + { + return Err(Error::Request); + } + Ok(()) + } + fn directory(path: &Path) -> Result { + let mut current = + Dir::open_ambient_dir("/", cap_std::ambient_authority()).map_err(|_| Error::Io)?; + for part in path.components() { + match part { + Component::RootDir => {} + Component::Normal(name) => { + current = current.open_dir_nofollow(name).map_err(|_| Error::Io)? + } + _ => return Err(Error::Request), + } + } + Ok(current) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs b/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs new file mode 100644 index 00000000..9217a820 --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs @@ -0,0 +1,167 @@ +use super::*; + +#[cfg(unix)] +use std::path::Path; + +#[cfg(unix)] +fn fixture() -> (tempfile::TempDir, PathBuf) { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().canonicalize().unwrap().join("parent/private"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join("credential"), b"fixture-private").unwrap(); + (temp, root) +} + +#[cfg(unix)] +fn capture(root: &Path) -> Inventory { + Inventory::capture(&[root.into()]).unwrap() +} + +#[cfg(unix)] +#[test] +fn private_inventory_accepts_nested_roots_and_detects_changed_bytes_names_and_modes() { + use std::os::unix::fs::PermissionsExt; + for change in 0..4 { + let (_temp, root) = fixture(); + std::fs::create_dir(root.join("nested")).unwrap(); + let file = root.join("nested/key"); + std::fs::write(&file, b"fixture-only").unwrap(); + let inventory = Inventory::capture(&[root.clone(), file.clone()]).unwrap(); + inventory.recheck().unwrap(); + match change { + 0 => std::fs::write(&file, b"changed-fixture").unwrap(), + 1 => std::fs::rename(&file, root.join("renamed")).unwrap(), + 2 => std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o400)).unwrap(), + _ => std::fs::write(root.join("new"), b"new-fixture").unwrap(), + } + assert!(inventory.recheck().is_err()); + } +} + +#[cfg(unix)] +#[test] +fn private_inventory_refuses_external_and_internal_hard_links_before_and_after_capture() { + for external in [false, true] { + let (temp, root) = fixture(); + let inventory = capture(&root); + let alias = if external { + temp.path().join("alias") + } else { + root.join("alias") + }; + std::fs::hard_link(root.join("credential"), &alias).unwrap(); + assert_eq!( + Inventory::capture(std::slice::from_ref(&root)).unwrap_err(), + Error::Unsafe + ); + assert_eq!(inventory.recheck(), Err(Error::Unsafe)); + std::fs::remove_file(alias).unwrap(); + // Restoring a link count does not restore the captured ctime incarnation. + assert_eq!(inventory.recheck(), Err(Error::Changed)); + capture(&root).recheck().unwrap(); + } +} + +#[cfg(unix)] +#[test] +fn private_inventory_never_follows_symlinks_or_opens_special_objects() { + let (temp, root) = fixture(); + let alias = temp.path().join("alias"); + std::os::unix::fs::symlink(&root, &alias).unwrap(); + assert!(Inventory::capture(&[alias.join("credential")]).is_err()); + std::os::unix::fs::symlink(root.join("credential"), root.join("link")).unwrap(); + assert_eq!( + Inventory::capture(std::slice::from_ref(&root)).unwrap_err(), + Error::Unsafe + ); + std::fs::remove_file(root.join("link")).unwrap(); + let socket = std::os::unix::net::UnixListener::bind(root.join("socket")).unwrap(); + assert_eq!(Inventory::capture(&[root]).unwrap_err(), Error::Unsafe); + drop(socket); +} + +#[cfg(unix)] +#[test] +fn private_inventory_rejects_replaced_roots_and_ancestor_bindings() { + for replace_parent in [false, true] { + let (temp, root) = fixture(); + let inventory = capture(&root); + if replace_parent { + let parent = root.parent().unwrap(); + let old = temp.path().join("old-parent"); + std::fs::rename(parent, &old).unwrap(); + std::os::unix::fs::symlink(&old, parent).unwrap(); + } else { + std::fs::rename(&root, temp.path().join("old")).unwrap(); + std::fs::create_dir(&root).unwrap(); + std::fs::write(root.join("credential"), b"fixture-private").unwrap(); + } + assert!(inventory.recheck().is_err()); + } +} + +#[cfg(unix)] +#[test] +fn private_inventory_request_and_depth_limits_are_enforced() { + assert_eq!(Inventory::capture(&[]).unwrap_err(), Error::Request); + assert_eq!( + Inventory::capture(&vec![PathBuf::from("/"); MAX_ROOTS + 1]).unwrap_err(), + Error::Request + ); + assert_eq!( + Inventory::capture(&["relative".into()]).unwrap_err(), + Error::Request + ); + let (_temp, root) = fixture(); + assert_eq!( + Inventory::capture(&[root.join("../private")]).unwrap_err(), + Error::Request + ); + let mut current = root.clone(); + for _ in 0..=MAX_DEPTH { + current = current.join("d"); + std::fs::create_dir(¤t).unwrap(); + } + assert_eq!(Inventory::capture(&[root]).unwrap_err(), Error::Limit); +} + +#[cfg(not(unix))] +#[test] +fn private_inventory_refuses_unsupported_platforms() { + assert_eq!( + Inventory::capture(&["fixture".into()]).unwrap_err(), + Error::Unavailable + ); +} + +#[cfg(unix)] +#[test] +fn private_inventory_enforces_unique_entry_limit_with_real_directory_entries() { + let (_temp, root) = fixture(); + // Root and credential occupy two of the available entries. + for i in 0..MAX_ENTRIES - 2 { + std::fs::write(root.join(format!("entry-{i}")), b"").unwrap(); + } + capture(&root).recheck().unwrap(); + std::fs::write(root.join("one-too-many"), b"").unwrap(); + assert_eq!(Inventory::capture(&[root]).unwrap_err(), Error::Limit); +} + +#[cfg(unix)] +#[test] +fn private_inventory_enforces_path_byte_budget_before_entry_limit() { + use std::os::unix::ffi::OsStrExt; + let (_temp, root) = fixture(); + let mut nested = root.clone(); + for _ in 0..3 { + nested = nested.join("d".repeat(180)); + std::fs::create_dir(&nested).unwrap(); + } + let width = nested.as_os_str().as_bytes().len() + 181; + let files = MAX_PATH_BYTES / width + 1; + assert!(files + 5 < MAX_ENTRIES); + for i in 0..files { + std::fs::write(nested.join(format!("{i:05}{}", "f".repeat(175))), b"").unwrap(); + } + assert_eq!(Inventory::capture(&[root]).unwrap_err(), Error::Limit); +} diff --git a/crates/cyber-sandbox/tests/plugin_copy.rs b/crates/cyber-sandbox/tests/plugin_copy.rs index a1187128..5d83ad66 100644 --- a/crates/cyber-sandbox/tests/plugin_copy.rs +++ b/crates/cyber-sandbox/tests/plugin_copy.rs @@ -216,6 +216,20 @@ fn worker_source_io_preserves_owner_sqlite_locks() { ); }; check(); + // Metadata-only private inventory must not open/close this locked database. + let inventory = cyber_sandbox::plugin_image::private_roots::Inventory::capture( + std::slice::from_ref(&source), + ) + .unwrap(); + inventory.recheck().unwrap(); + check(); + drop(inventory); + check(); + assert!( + cyber_sandbox::plugin_image::private_roots::Inventory::capture(&[path.join("child")]) + .is_err() + ); + check(); let result = Command::new(std::env::current_exe().unwrap()) .args(["--exact", "source_digest_probe", "--ignored", "--nocapture"]) .env("CYBER_PLUGIN_COPY_SOURCE", &source) diff --git a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md index deedb029..f72542f5 100644 --- a/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md +++ b/openspec/changes/complete-plugin-hook-runtime/packaged-linux-entry.md @@ -138,3 +138,31 @@ merge with parents `d90f4c16` and `758b86f8`, verified through the commit API. These source/binary bindings accompany actual passing tests, rather than being substituted for them. macOS hosted packaged matrix execution and the remaining production admission/integration gates above remain outstanding. + + +## Complete hosted packaged matrix at `bc850900` + +Run [40872011749](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40872011749) +passed all five platform jobs. Its retained checkout `cc6422cd` has parents +`d90f4c16` and `bc850900`, independently verified from the fetched commit. All four +Unix artifacts were downloaded and inspected. Their native-entry source manifests +independently recompute to +`19761f2a0dc8b8f107ff0a8b75b93abbbaca4a34b5f50bbbd793676e4f1fe7a7` +at that checkout; each runtime's probe manifest also recomputes from the exact +commit. Every runtime's input-helper hash matches its platform's retained +built-helper hash, including separate executed hashes after fresh macOS signing. + +The macOS arm64 and Intel jobs each pass nine rows for Node, Python 3.13, system +Python, Bun, Deno and native: 54 cases per architecture. Both Linux jobs pass nine +rows for Node, Python, Bun, Deno and native: 45 per architecture. All 198 rows have +zero unsupported cases. These reports come from the driver's strict positive +startup/tool and capability assertions, including self-exec; they are not +inferred from provenance hashes. The independent native descriptor regression +remains the descriptor proof, since direct Deno already closes the dummy FD. +Windows passes explicit unavailable behavior only. + +This closes the previously pending hosted packaged-fixture runtime receipts for +that source. It does not prove the later production package configuration path, +private inventory, immutable runtime/helper admission, namespace/alias protection, +network/secret intersections or production launch integration. The explicit +support/design review gate and parent native-enforcement tasks remain open. diff --git a/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md b/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md new file mode 100644 index 00000000..c3e8394c --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md @@ -0,0 +1,89 @@ +# Private-root alias inventory + +This implements a metadata-only admission component within task 2.2, following +[the observed pre-existing alias bypass](native-proof.md#pre-existing-aliases-require-a-separate-admission-boundary). +An OS denial for the private pathname cannot prevent reading the same inode +through a hard link that already exists in a permitted package or workspace. + +`plugin_image::private_roots::Inventory` captures explicitly supplied absolute +roots using only directory handles, directory enumeration and non-following +metadata. Regular private files must have exactly one link, including when two +links would both be inside protected roots. Symlinks and non-file/non-directory +objects refuse capture. Every path component used to open directories rejects +symlinks; replacing an expected directory with a regular file never opens that +file. No private regular file is independently opened or closed in the owner. +This matters for SQLite's process-wide POSIX record locks. + +Capture records native object identity, type, link count, length, ordinary/native +mode, owner, modification and change time. It checks directory handles against +the enumerated entry before descent, and checks each entry again afterward. +Recheck captures fresh metadata and compares it with the retained records and +parent directory identities. Replacement, new/deleted entries, permission changes, +content metadata changes and new aliases refuse the old incarnation. Captured +metadata contains paths and native identities; callers must not log it as public +payload. Errors are static and do not expose private contents or pathnames. + +Limits are 64 supplied roots, 8,192 unique entries, 32 nested directory levels, +4,096 native bytes per path and 4 MiB of retained path bytes. Records have fixed +bounded fields in addition to those path bytes. At most one parent descriptor per +supplied root is retained; traversal uses at most one directory per depth. The +inventory does not allocate memory proportional to file contents. Native metadata +IO can still block; callers must own cancellation/settlement rather than infer a +wall-clock deadline from entry limits. Windows returns +an explicit unavailable result; it does not silently skip alias checks. + +## Preconditions and integration still required + +The caller must supply **every** private root, synchronize legitimate owner writes, +and exclude all untrusted writers/renames/mount changes during capture, recheck +and launch. This API does not enumerate the system's private roots, freeze file +metadata, establish immutable ancestors or close the race after a recheck. It +cannot replace native protected-root masks or the generation-bound launch commit. +Sockets, FIFOs and symlinks require a separately reviewed object policy before a +caller can claim to admit an owner namespace containing them; refusal here is +not evidence of complete production support. + +No production host currently uses this inventory. Namespace provisioning, +protected/alias-root composition, helper/runtime admission, native launch wiring, +network/secret intersections and the explicit support/design review remain open. +The primitive is intentionally not represented as a sealed image or launch permit. +Task 2.2 and the full plugin/hook acceptance checklist remain incomplete. + +## Verification + +On local macOS arm64, 22 owner/authority/inventory tests and five actual-helper +integration tests pass. Two ignored subprocess fixture entrypoints are invoked +by the SQLite regression. Seven inventory tests cover nested and file roots, +metadata changes, internal/external hard links before and after capture, +symlinked ancestors, root replacement, special objects, request/depth bounds, +and real-directory entry/path-byte limits. The exact entry limit is accepted; +one extra entry refuses. A separate long-path fixture exceeds the 4 MiB path +budget before the entry limit. + +The independent SQLite connection still observes the owner's exclusive lock +after inventory capture, recheck and drop. Treating the locked database as an +expected directory ancestor also refuses without releasing the lock. Three +behavioral controls execute the named regressions: removing link-count refusal +allows unsafe capture; removing snapshot comparison misses changed data; adding +an owner-side private regular-file open/close releases the SQLite lock. Each +control fails its intended assertion, and restored positive suites pass. + +Scoped Sandbox all-target Clippy passes with Rust warnings denied. The native C +compiler emits two deprecated Seatbelt SDK-call warnings; no origin is assigned +here. Formatting/diff checks pass, the package workflow YAML and all four Bash +blocks parse, strict OpenSpec validation passes 107 items, and spec lint reports +zero errors and 25 warnings. The exact new CI control block also executes locally +and restores the passing 22-test and five-test suites. Hosted platform results +remain pending. + +The code/security review covered every changed source, test, workflow and +implementation note. Directory opens use the existing capability-based no-follow +primitive; regular files have only metadata operations. Requests are validated +before root-path cloning and traversal, and retained paths/entries and directory +handle counts are bounded. Error strings are static. Tests exercise both safe +capture and unsafe-alias refusal, rather than counting refusal alone as admission. +No blocking issue was found within this inventory scope. The review does not +certify complete private-root coverage, writer exclusion, immutable namespace +ownership, cancellation of blocking native IO or production launch admission. The focused package +workflow already runs `cargo test -p cyber-sandbox --lib plugin_image` and the real +`plugin_copy` integration suite, including the SQLite independent lock probe. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index c905a86f..9ec114ed 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; 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; 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. From 729546f20629f53bc9927b8a19f13a6087e92b98 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:25:11 -0400 Subject: [PATCH 12/50] feat(sandbox): compose bounded native macOS plugin policies --- .github/workflows/plugin-native-launcher.yml | 63 ++++ crates/cyber-sandbox/src/lib.rs | 1 + crates/cyber-sandbox/src/plugin_policy.rs | 270 +++++++++++++++++ .../cyber-sandbox/src/plugin_policy/tests.rs | 85 ++++++ crates/cyber-sandbox/tests/native_exec.rs | 8 + .../tests/support/native_policy_macos.rs | 282 ++++++++++++++++++ .../native-policy-composition.md | 94 ++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- scripts/probes/plugin-native-entry/report.mts | 2 + 9 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 crates/cyber-sandbox/src/plugin_policy.rs create mode 100644 crates/cyber-sandbox/src/plugin_policy/tests.rs create mode 100644 crates/cyber-sandbox/tests/support/native_policy_macos.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/native-policy-composition.md diff --git a/.github/workflows/plugin-native-launcher.yml b/.github/workflows/plugin-native-launcher.yml index e195499b..519f3b51 100644 --- a/.github/workflows/plugin-native-launcher.yml +++ b/.github/workflows/plugin-native-launcher.yml @@ -54,6 +54,8 @@ jobs: with: deno-version: 'v2.9.8' - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Verify native musl C compilation if: runner.os == 'Linux' @@ -70,8 +72,65 @@ jobs: shell: bash run: | set -euo pipefail + rustfmt --edition 2024 --check crates/cyber-sandbox/tests/support/native_policy_macos.rs + cargo test --locked -p cyber-sandbox --lib plugin_policy cargo test --locked -p cyber-sandbox --bin cyber-sandbox-exec native_exec cargo test --locked -p cyber-sandbox --test native_exec + - name: Verify native policy composition and failure controls + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-sandbox/src/plugin_policy.rs + cp "$source" "$RUNNER_TEMP/native-policy-positive.rs" + trap 'cp "$RUNNER_TEMP/native-policy-positive.rs" "$source"' EXIT + for control in ancestor immutable private exec; do + cp "$RUNNER_TEMP/native-policy-positive.rs" "$source" + python3 - "$source" "$control" <<'PY' + from pathlib import Path + import sys + p=Path(sys.argv[1]); text=p.read_text(); control=sys.argv[2] + if control=='ancestor': + start=text.index(' for ancestor in ancestors {') + end=text.index(' if profile.shell {',start) + text=text[:start]+' let _ = ancestors;\n'+text[end:] + elif control=='immutable': + start=text.index(' for root in immutable(profile) {') + end=text.index(' for root in profile.read_only {',start) + text=text[:start]+text[end:] + elif control=='private': + start=text.index(' for root in profile.private {') + end=text.index(' let roots = immutable(profile)',start) + text=text[:start]+text[end:] + else: + before=' if profile.shell {' + assert text.count(before)==1 + text=text.replace(before,' if true {') + p.write_text(text) + PY + if [[ "$control" == ancestor || "$control" == immutable ]]; then + name=rust_plugin_policy_blocks_private_package_and_runtime_ancestor_mutation + else + name=rust_plugin_policy_preserves_native_startup_and_independent_capability_boundaries + fi + if cargo test --locked -p cyber-sandbox --test native_exec "macos::policy::$name" -- --exact > "$RUNNER_TEMP/native-policy-$control-control.log" 2>&1; then + cat "$RUNNER_TEMP/native-policy-$control-control.log" + echo "Missing-$control native policy control unexpectedly passed" >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/native-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 content write:','private':'failed: protected_write:','exec':'failed: shell:'} + assert expected[sys.argv[3]] in text, text + print('Missing',sys.argv[3],'fails intended native assertion') + PY + done + 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 packaged entry across supported runtimes if: runner.os != 'Windows' shell: bash @@ -154,5 +213,9 @@ jobs: ${{ runner.temp }}/native-entry-provenance.json ${{ runner.temp }}/packaged-runtime/ ${{ runner.temp }}/native-fd-control.log + ${{ runner.temp }}/native-policy-ancestor-control.log + ${{ runner.temp }}/native-policy-immutable-control.log + ${{ runner.temp }}/native-policy-private-control.log + ${{ runner.temp }}/native-policy-exec-control.log if-no-files-found: error retention-days: 14 diff --git a/crates/cyber-sandbox/src/lib.rs b/crates/cyber-sandbox/src/lib.rs index 40ef675f..cb9c7f6b 100644 --- a/crates/cyber-sandbox/src/lib.rs +++ b/crates/cyber-sandbox/src/lib.rs @@ -8,6 +8,7 @@ mod isolation; #[cfg(target_os = "linux")] mod linux; pub mod plugin_image; +pub mod plugin_policy; mod policy; mod profiles; pub mod proxy; diff --git a/crates/cyber-sandbox/src/plugin_policy.rs b/crates/cyber-sandbox/src/plugin_policy.rs new file mode 100644 index 00000000..5882f6ea --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_policy.rs @@ -0,0 +1,270 @@ +//! Plugin-specific native policy composition. This is not execution admission: +//! callers own immutable runtime/helper/package provenance, complete private-root +//! inventory, namespace protection and generation-bound process lifetime. +use std::path::Path; + +#[derive(Debug, Clone, Copy)] +pub enum ReadPath<'a> { + File(&'a Path), + Directory(&'a Path), +} +#[cfg(target_os = "macos")] +impl<'a> ReadPath<'a> { + fn path(&self) -> &'a Path { + match self { + Self::File(path) | Self::Directory(path) => path, + } + } +} + +/// Explicit owner-selected canonical bound paths; none are discovered from the +/// project or PATH. Validation is syntactic and cannot establish native bindings. +/// Network defaults to denied. Network/proxy/secret integration is separate work. +pub struct MacProfile<'a> { + pub target: &'a Path, + pub helper_identifier: &'a str, + pub package: &'a Path, + pub scratch: &'a Path, + pub location: &'a Path, + pub bootstrap: &'a [ReadPath<'a>], + pub private: &'a [&'a Path], + pub read_only: &'a [&'a Path], + pub fs_read: bool, + pub fs_write: bool, + pub shell: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("native plugin policy composition is unavailable on this platform")] + Unavailable, + #[error("invalid native plugin policy input")] + Input, + #[error("native plugin policy roots overlap unsafely")] + Overlap, + #[error("native plugin policy exceeds its size limit")] + Limit, +} + +impl MacProfile<'_> { + /// A complete deny-default filesystem/process profile with no network grant. + /// The identifier must name a freshly signed admitted helper and differ from + /// every architecture slice of the admitted target. This builder cannot attest + /// those facts and does not open paths or execute verification tools. + pub fn render(&self) -> Result { + #[cfg(target_os = "macos")] + return macos::render(self); + #[cfg(not(target_os = "macos"))] + Err(Error::Unavailable) + } +} + +#[cfg(target_os = "macos")] +mod macos { + use super::*; + use std::collections::BTreeSet; + use std::fmt::Write; + use std::path::Component; + + const MAX_PROFILE: usize = 64 * 1024; + const MAX_ROOTS: usize = 64; + const MAX_PATH_BYTES: usize = 64 * 1024; + + struct Text(String); + impl Write for Text { + fn write_str(&mut self, text: &str) -> std::fmt::Result { + if self.0.len() + text.len() > MAX_PROFILE { + return Err(std::fmt::Error); + } + self.0.push_str(text); + Ok(()) + } + } + 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 quote(text: &str) -> String { + format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\"")) + } + fn overlaps(a: &Path, b: &Path) -> bool { + a.starts_with(b) || b.starts_with(a) + } + fn validate(profile: &MacProfile<'_>) -> Result<(), Error> { + let identifier = profile.helper_identifier; + if identifier.is_empty() + || identifier.len() > 256 + || !identifier + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte)) + { + return Err(Error::Input); + } + if profile + .bootstrap + .len() + .saturating_add(profile.private.len()) + .saturating_add(profile.read_only.len()) + .saturating_add(4) + > MAX_ROOTS + { + return Err(Error::Limit); + } + let mut paths = vec![ + profile.target, + profile.package, + profile.scratch, + profile.location, + ]; + paths.extend(profile.bootstrap.iter().map(ReadPath::path)); + paths.extend(profile.private.iter().copied()); + paths.extend(profile.read_only.iter().copied()); + if paths.len() > MAX_ROOTS { + return Err(Error::Limit); + } + let mut bytes = 0; + for value in paths { + bytes += path(value)?.len(); + if bytes > MAX_PATH_BYTES { + return Err(Error::Limit); + } + } + let immutable = std::iter::once(profile.package) + .chain(std::iter::once(profile.target)) + .chain(profile.bootstrap.iter().map(ReadPath::path)); + for root in immutable { + if overlaps(root, profile.scratch) + || profile + .private + .iter() + .any(|private| overlaps(root, private)) + { + return Err(Error::Overlap); + } + } + if profile + .private + .iter() + .chain(profile.read_only) + .any(|root| overlaps(root, profile.scratch)) + { + return Err(Error::Overlap); + } + Ok(()) + } + fn rule( + out: &mut Text, + action: &str, + operation: &str, + kind: &str, + path: &Path, + ) -> Result<(), Error> { + writeln!( + out, + "({action} {operation} ({kind} {}))", + quote(path.to_str().ok_or(Error::Input)?) + ) + .map_err(|_| Error::Limit) + } + fn immutable<'a>(profile: &'a MacProfile<'a>) -> impl Iterator> { + [ + ReadPath::Directory(profile.package), + ReadPath::File(profile.target), + ] + .into_iter() + .chain(profile.bootstrap.iter().copied()) + } + pub(super) fn render(profile: &MacProfile<'_>) -> Result { + validate(profile)?; + let mut out = Text(String::new()); + out.write_str("(version 1)\n(deny default)\n(allow file-read-metadata)\n(allow sysctl-read)\n(allow signal (target same-sandbox))\n(allow process-info* (target same-sandbox))\n(allow file-read-data (literal \"/\"))\n(allow file-read* file-write* (literal \"/dev/null\"))\n") + .map_err(|_| Error::Limit)?; + for root in immutable(profile).chain(std::iter::once(ReadPath::Directory(profile.scratch))) + { + let kind = if matches!(root, ReadPath::File(_)) { + "literal" + } else { + "subpath" + }; + rule( + &mut out, + "allow", + "file-read* file-map-executable", + kind, + root.path(), + )?; + } + rule(&mut out, "allow", "file-write*", "subpath", profile.scratch)?; + if profile.fs_read { + rule(&mut out, "allow", "file-read*", "subpath", profile.location)?; + } + if profile.fs_write { + rule( + &mut out, + "allow", + "file-write*", + "subpath", + profile.location, + )?; + } + for root in immutable(profile) { + let kind = if matches!(root, ReadPath::File(_)) { + "literal" + } else { + "subpath" + }; + rule(&mut out, "deny", "file-write*", kind, root.path())?; + } + for root in profile.read_only { + rule(&mut out, "deny", "file-write*", "subpath", root)?; + } + for root in profile.private { + rule(&mut out, "deny", "file-read* file-write*", "subpath", root)?; + } + let roots = immutable(profile) + .map(|root| root.path()) + .chain(profile.private.iter().copied()) + .chain(profile.read_only.iter().copied()); + let mut ancestors = BTreeSet::new(); + for root in roots { + for ancestor in root + .ancestors() + .skip(1) + .filter(|path| path.parent().is_some()) + { + ancestors.insert(ancestor); + } + } + ancestors.insert(profile.location); + for ancestor in ancestors { + rule(&mut out, "deny", "file-write-unlink", "literal", ancestor)?; + } + if profile.shell { + out.write_str("(allow process-fork)\n(allow process-exec)\n") + .map_err(|_| Error::Limit)?; + } else { + writeln!( + out, + "(allow process-exec (require-all (signing-identifier {}) (literal {})))", + quote(profile.helper_identifier), + quote(profile.target.to_str().ok_or(Error::Input)?) + ) + .map_err(|_| Error::Limit)?; + } + Ok(out.0) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/cyber-sandbox/src/plugin_policy/tests.rs b/crates/cyber-sandbox/src/plugin_policy/tests.rs new file mode 100644 index 00000000..5170423e --- /dev/null +++ b/crates/cyber-sandbox/src/plugin_policy/tests.rs @@ -0,0 +1,85 @@ +use super::*; + +fn profile<'a>(bootstrap: &'a [ReadPath<'a>], private: &'a [&'a Path]) -> MacProfile<'a> { + MacProfile { + target: Path::new("/runtime/bin/host"), + helper_identifier: "cyber.helper.fresh-fixture", + package: Path::new("/owned/package"), + scratch: Path::new("/owned/scratch"), + location: Path::new("/project"), + bootstrap, + private, + read_only: &[], + fs_read: true, + fs_write: true, + shell: false, + } +} + +#[cfg(target_os = "macos")] +#[test] +fn native_profile_denials_follow_workspace_grants_and_protect_every_ancestor() { + let bootstrap = [ReadPath::Directory(Path::new("/runtime/lib"))]; + let private = [Path::new("/project/nested/private")]; + let text = profile(&bootstrap, &private).render().unwrap(); + let allow = text + .find("(allow file-write* (subpath \"/project\"))") + .unwrap(); + for denial in [ + "(deny file-write* (subpath \"/owned/package\"))", + "(deny file-write* (literal \"/runtime/bin/host\"))", + "(deny file-read* file-write* (subpath \"/project/nested/private\"))", + "(deny file-write-unlink (literal \"/project/nested\"))", + "(deny file-write-unlink (literal \"/runtime/bin\"))", + ] { + assert!(text.find(denial).unwrap() > allow, "{denial}"); + } + assert!(!text.contains("(allow process-fork)")); + assert!(!text.contains("(allow process-exec)\n")); +} + +#[cfg(target_os = "macos")] +#[test] +fn native_profile_refuses_invalid_paths_identifiers_overlap_and_root_budgets() { + let mut plan = profile(&[], &[]); + for path in ["relative", "/", "/runtime/../target", "/target\n(injected)"] { + plan.target = Path::new(path); + assert_eq!(plan.render(), Err(Error::Input)); + } + plan.target = Path::new("/runtime/bin/host"); + plan.helper_identifier = "bad\"identifier"; + assert_eq!(plan.render(), Err(Error::Input)); + plan.helper_identifier = "cyber.helper.fixture"; + plan.scratch = Path::new("/owned/package/scratch"); + assert_eq!(plan.render(), Err(Error::Overlap)); + plan.scratch = Path::new("/owned/scratch"); + let bootstrap = vec![ReadPath::Directory(Path::new("/runtime/lib")); 65]; + plan.bootstrap = &bootstrap; + assert_eq!(plan.render(), Err(Error::Limit)); +} + +#[cfg(not(target_os = "macos"))] +#[test] +fn native_profile_never_silently_supplies_a_policy_for_another_platform() { + assert_eq!(profile(&[], &[]).render(), Err(Error::Unavailable)); +} + +#[cfg(target_os = "macos")] +#[test] +fn native_profile_accepts_root_limit_and_refuses_rendered_policy_overflow() { + let bootstrap = vec![ReadPath::Directory(Path::new("/runtime/lib")); 60]; + let mut plan = profile(&bootstrap, &[]); + assert!(plan.render().is_ok()); + let paths: Vec<_> = (0..16) + .map(|i| { + let mut path = std::path::PathBuf::from(format!("/large-{i}")); + for _ in 0..30 { + path.push("component-with-some-length"); + } + path + }) + .collect(); + let bootstrap: Vec<_> = paths.iter().map(|path| ReadPath::Directory(path)).collect(); + plan.bootstrap = &bootstrap; + assert_eq!(plan.render(), Err(Error::Limit)); +} diff --git a/crates/cyber-sandbox/tests/native_exec.rs b/crates/cyber-sandbox/tests/native_exec.rs index c75b808e..85738ac9 100644 --- a/crates/cyber-sandbox/tests/native_exec.rs +++ b/crates/cyber-sandbox/tests/native_exec.rs @@ -82,6 +82,11 @@ mod macos { "-Werror", "-Wno-error=deprecated-declarations", ]) + .arg("-I") + .arg( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../scripts/probes/plugin-confinement"), + ) .arg(&input) .arg("-o") .arg(&output), @@ -143,6 +148,9 @@ mod macos { command } } + mod policy { + include!("support/native_policy_macos.rs"); + } fn packet(output: std::process::Output) -> Value { assert!( output.status.success(), diff --git a/crates/cyber-sandbox/tests/support/native_policy_macos.rs b/crates/cyber-sandbox/tests/support/native_policy_macos.rs new file mode 100644 index 00000000..07643db3 --- /dev/null +++ b/crates/cyber-sandbox/tests/support/native_policy_macos.rs @@ -0,0 +1,282 @@ +// Actual packaged-helper receipts for the production Rust policy builder. +use super::*; +use cyber_sandbox::plugin_policy::{MacProfile, ReadPath}; +use std::io::Write; +use std::process::Stdio; +use std::time::{Duration, Instant}; + +fn rpc(command: &mut Command) -> Value { + let mut child = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let packet = b"{\"jsonrpc\":\"2.0\",\"id\":\"native-proof\",\"method\":\"tool/execute\",\"params\":{\"name\":\"native-probe\",\"input\":{}}}\n"; + let _ = child.stdin.take().unwrap().write_all(packet); + let deadline = Instant::now() + Duration::from_secs(15); + while child.try_wait().unwrap().is_none() { + if Instant::now() >= deadline { + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + panic!("native policy receipt timed out: {output:?}"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let result = child.wait_with_output().unwrap(); + assert!( + result.status.success(), + "native policy startup failed: {result:?}" + ); + 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() +} + +#[test] +fn rust_plugin_policy_preserves_native_startup_and_independent_capability_boundaries() { + let fixture = Fixture::new(); + let project = fixture.root.join("project \"quoted\" \\ workspace"); + let package = project.join("installed-package"); + let scratch = fixture.root.join("scratch"); + let private = fixture.root.join("private"); + let protected = project.join("nested/owner-state"); + for path in [&package, &scratch, &private, &protected] { + std::fs::create_dir_all(path).unwrap(); + } + for path in [ + package.join("data"), + project.join("data"), + private.join("secret"), + protected.join("secret"), + ] { + std::fs::write(path, b"fixture-only").unwrap(); + } + symlink(private.join("secret"), package.join("alias")).unwrap(); + let compiled = compile( + &fixture.root, + include_str!("../../../../scripts/probes/plugin-confinement/fixture_native.c"), + "policy-native", + ); + let target = package.join("main"); + std::fs::copy(compiled, &target).unwrap(); + sign(&target, "cyber.test.policy.payload"); + let bootstrap = [ + ReadPath::Directory(Path::new("/System")), + ReadPath::Directory(Path::new("/usr/lib")), + ReadPath::Directory(Path::new("/usr/bin")), + ReadPath::Directory(Path::new("/bin")), + ]; + let private_roots = [private.as_path(), protected.as_path()]; + for read in [false, true] { + for write in [false, true] { + for shell in [false, true] { + let plan = MacProfile { + target: &target, + helper_identifier: &fixture.helper_id, + package: &package, + scratch: &scratch, + location: &project, + bootstrap: &bootstrap, + private: &private_roots, + read_only: &[], + fs_read: read, + fs_write: write, + shell, + }; + let policy = fixture.profile("rust-policy.sb", &plan.render().unwrap()); + let mut command = fixture.command(&policy, &target); + command.arg("inspect"); + for path in [ + package.join("data"), + project.join("data"), + private.join("secret"), + scratch.join("roundtrip"), + project.join("written"), + package.join("alias"), + protected.join("secret"), + scratch.clone(), + project.clone(), + ] { + command.arg(path); + } + command + .args(["0", "0", "1"]) + .env("CYBER_PROOF_PRIVATE", private.join("secret")); + let observed = rpc(&mut command); + assert_capabilities(observed, read, write, shell); + let written = project.join("written"); + if written.exists() { + std::fs::remove_file(written).unwrap(); + } + } + } + } +} + +#[test] +fn rust_plugin_policy_blocks_private_package_and_runtime_ancestor_mutation() { + let fixture = Fixture::new(); + let project = fixture.root.join("project"); + let package = project.join("extension-parent/package"); + let runtime = project.join("runtime-parent/lib"); + let private = project.join("private-parent/owner-state"); + let scratch = fixture.root.join("scratch"); + let public = project.join("public-parent"); + for directory in [&package, &runtime, &private, &scratch, &public] { + std::fs::create_dir_all(directory).unwrap(); + } + for root in [&package, &runtime, &private, &public] { + std::fs::write(root.join("data"), b"unchanged-fixture").unwrap(); + } + let target = compile( + &fixture.root, + r#" +#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); + if (fd >= 0) close(fd); + } else return 2; + printf("{\"success\":%s,\"denied\":%s}\n", result == 0 ? "true" : "false", + result < 0 && (errno == EPERM || errno == EACCES) ? "true" : "false"); + return 0; +} +"#, + "mutation-probe", + ); + sign(&target, "cyber.test.policy.mutation"); + let bootstrap = [ + ReadPath::Directory(Path::new("/System")), + ReadPath::Directory(Path::new("/usr/lib")), + ReadPath::Directory(&runtime), + ]; + let private_roots = [private.as_path()]; + let plan = MacProfile { + target: &target, + helper_identifier: &fixture.helper_id, + package: &package, + scratch: &scratch, + location: &project, + bootstrap: &bootstrap, + private: &private_roots, + read_only: &[], + fs_read: true, + fs_write: true, + shell: false, + }; + let profile = fixture.profile("mutation-policy.sb", &plan.render().unwrap()); + for root in [&private, &package, &runtime] { + let parent = root.parent().unwrap(); + let result = packet(successful( + fixture + .command(&profile, &target) + .arg("rename") + .arg(parent) + .arg(project.join("moved")), + )); + assert_eq!( + result, + json!({"success":false,"denied":true}), + "protected ancestor rename: {parent:?}" + ); + assert_eq!( + std::fs::read(root.join("data")).unwrap(), + b"unchanged-fixture" + ); + } + for root in [&package, &runtime] { + let result = packet(successful( + fixture + .command(&profile, &target) + .arg("write") + .arg(root.join("data")) + .arg("unused"), + )); + assert_eq!( + result, + json!({"success":false,"denied":true}), + "immutable content write: {root:?}" + ); + assert_eq!( + std::fs::read(root.join("data")).unwrap(), + b"unchanged-fixture" + ); + } + let moved = project.join("public-moved"); + let result = packet(successful( + fixture + .command(&profile, &target) + .arg("rename") + .arg(&public) + .arg(&moved), + )); + assert_eq!(result, json!({"success":true,"denied":false})); + assert_eq!( + std::fs::read(moved.join("data")).unwrap(), + b"unchanged-fixture" + ); +} + +fn assert_capabilities(observed: Value, read: bool, write: bool, shell: bool) { + for key in [ + "stdio_tool", + "package_read", + "thread", + "scratch_write", + "scratch_read", + "cannot_relax", + ] { + assert_eq!(observed[key], true, "{key}: {observed}"); + } + 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!(observed[key], false, "{key}: {observed}"); + } + assert_eq!(observed["project_read"], read, "{observed}"); + assert_eq!(observed["project_write"], write, "{observed}"); + assert_eq!( + observed["written_project_read"], + read && write, + "{observed}" + ); + assert_eq!(observed["thread_reads"], json!([read, false]), "{observed}"); + for key in [ + "shell", + "thread_shell", + "native_fork", + "constructor_fork", + "native_spawn", + ] { + assert_eq!(observed[key], shell, "{key}: {observed}"); + } + let children = if shell { + json!([read, false]) + } else { + Value::Null + }; + assert_eq!(observed["child_reads"], children, "{observed}"); + assert_eq!(observed["native_child_reads"], children, "{observed}"); +} diff --git a/openspec/changes/complete-plugin-hook-runtime/native-policy-composition.md b/openspec/changes/complete-plugin-hook-runtime/native-policy-composition.md new file mode 100644 index 00000000..a8ce879b --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/native-policy-composition.md @@ -0,0 +1,94 @@ +# Native plugin policy composition + +The macOS filesystem/process policy from the confinement experiment now has a +bounded Rust composer in `cyber-sandbox::plugin_policy`. This is a production +library component, not production host activation or a native admission permit. +It keeps the plugin-specific contract separate from ordinary command sandboxing. + +The owner supplies the selected target, fresh helper signing identifier, package, +scratch, Location, typed file/directory bootstrap reads, private roots and +write-protected paths. Nothing is discovered from project configuration or PATH. +Inputs must be canonical, bound native paths; validation here is syntactic only. +The caller still attests immutable runtime/helper/package provenance, complete +private/alias inventory, protected ancestors, all-architecture signing identity +separation and generation-bound start/lifetime before executing the profile. + +Profiles deny by default. Bootstrap/package/scratch reads and executable mappings +are explicit. Scratch remains readable/writable. Location reads and writes are +independent flags. All immutable bootstrap/package/target write denials follow +Location grants, so an enclosing writable Location cannot make them writable. +Private roots deny reads and writes. Every ancestor of immutable/private and +write-protected paths denies unlink/rename, including ancestors inside Location. +Other public directory renames remain possible with workspace writes enabled. + +Without shell capability, only the exact target may exec while the subject has +the freshly signed helper identity; process fork remains denied and threads are +allowed by the native mechanism. With shell capability, fork/exec are permitted +under the same inherited filesystem profile. Matching target/helper signing +identity remains a known bypass unless the caller establishes separation before +launch; composing text cannot establish it. + +The component supplies no network grant. Native network/proxy/secret intersections +must be integrated and tested separately before activation. Linux needs its own +Landlock plus private mount/PID/network composition; this Mac profile is never +silently supplied for Linux or Windows. No platform support gate is closed. + +## Bounds and validation + +Inputs have at most 64 paths in total, 64 KiB of path text, 4,096 bytes per path, +and 64 path components. Relative/root/parent-traversing/control-character paths +and unsafe scratch/private/bootstrap overlaps refuse. The helper identifier is +bounded to 256 ASCII letters, numbers, dots, underscores and hyphens. Paths quote +backslashes and double quotes for the native profile syntax. Rendering checks the +64 KiB output ceiling on each write rather than allocating an unbounded profile. +No filesystem contents, metadata or verification subprocesses are opened here. + +## Verification and integration status + +Real packaged-helper tests use the Rust composer, independent native payloads, +fresh all-slice-verified helper/target identities and bounded process receipts. +One payload exercises all eight fs.read/fs.write/shell combinations, including +threads, constructor-time reads/forks, child/shell inheritance, private symlink +and hard-link attempts, package alias writes and real JSON-RPC tool execution. +Another tests protected ancestor rename, direct package/runtime writes and a +positive public directory rename. The capability matrix uses a workspace name +containing spaces, double quotes and a backslash to verify native quoting. + +On local macOS arm64, all three composer unit tests and all eight packaged native +entry tests pass. The two new native tests include eight capability combinations +and the mutation/positive-rename cases. Twenty-two owner/authority/inventory +tests and five actual-helper integration tests also pass; two ignored fixture +entrypoints are explicitly invoked by the SQLite regression. + +Four controlled mutations each fail a named native assertion: missing ancestor +unlink denials permit protected parent rename; missing immutable write denials +permit package data modification; missing private-root denials permit protected +writes; unrestricted fork/exec permits a shell without shell capability. The +correct source is restored, and all three unit/eight native tests pass again. +The workflow retains these logs and verifies their precise assertion reasons, +rather than accepting an arbitrary failed compilation as a successful control. + +Scoped Sandbox all-target Clippy passes with Rust warnings denied. The C compiler +reports the two deprecated Seatbelt SDK calls; no origin is assigned here. +Formatting, standalone formatting of the included native test helper, diff +checks, TypeScript syntax for the source-provenance script, workflow YAML/all six +Bash blocks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) +pass. Hosted macOS arm64/Intel execution and other-platform explicit-unavailable +results remain required; local execution is not their substitute. + +The scoped code/security review covered every changed builder, unit/native test, +workflow, provenance script and implementation note. The composer stays in the +filesystem/process adapter and performs no owner IO. Inputs are bounded before +collection; output is bounded incrementally. It distinguishes exact-file grants +from directory grants, quotes native paths, applies immutable/private denials +after broad Location grants and protects ancestor bindings. Tests retain actual +positive startup, threads, permitted child execution and public writes/renames. +No blocking issue was found within this composition scope. Syntactic path checks +and caller-supplied identity strings do not attest immutable native provenance; +that obligation and the activation gates below remain open. + +The production host still uses its legacy sandbox. Namespace provisioning, +runtime/helper admission and resolver, prepared-image integration, revocation and +process settlement, Linux composition, native network/secret intersection and +explicit native proof/design review remain required. The full approved task 2 +and all later plugin/hook, Daily coding and binary editor obligations stay open. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 9ec114ed..0a070618 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; 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; 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 e5a41bb3..3542aebb 100644 --- a/scripts/probes/plugin-native-entry/report.mts +++ b/scripts/probes/plugin-native-entry/report.mts @@ -13,6 +13,8 @@ const sources = [ "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/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 de2c91ecfe554c368157a7e00b623cdd3c90639c Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:28:16 -0400 Subject: [PATCH 13/50] test(sandbox): avoid timestamp precision assumptions in alias checks --- crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs | 5 +++-- .../complete-plugin-hook-runtime/private-root-inventory.md | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs b/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs index 9217a820..74881621 100644 --- a/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs +++ b/crates/cyber-sandbox/src/plugin_image/private_roots/tests.rs @@ -56,8 +56,9 @@ fn private_inventory_refuses_external_and_internal_hard_links_before_and_after_c ); assert_eq!(inventory.recheck(), Err(Error::Unsafe)); std::fs::remove_file(alias).unwrap(); - // Restoring a link count does not restore the captured ctime incarnation. - assert_eq!(inventory.recheck(), Err(Error::Changed)); + // A fresh capture may admit the now-unlinked object. Do not require the + // filesystem clock to distinguish this rapid link/unlink round trip. + // The live-alias refusals above are the invariant, not timestamp precision. capture(&root).recheck().unwrap(); } } diff --git a/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md b/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md index c3e8394c..b8658980 100644 --- a/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md +++ b/openspec/changes/complete-plugin-hook-runtime/private-root-inventory.md @@ -61,7 +61,9 @@ one extra entry refuses. A separate long-path fixture exceeds the 4 MiB path budget before the entry limit. The independent SQLite connection still observes the owner's exclusive lock -after inventory capture, recheck and drop. Treating the locked database as an +after inventory capture, recheck and drop. The hard-link regression verifies +refusal while the alias exists and safe fresh capture after unlink; it does not +assume timestamp precision can distinguish a rapid link/unlink round trip. Treating the locked database as an expected directory ancestor also refuses without releasing the lock. Three behavioral controls execute the named regressions: removing link-count refusal allows unsafe capture; removing snapshot comparison misses changed data; adding From 483066c7712515783a4f71fd51543a8b4ad527eb Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:31:52 -0400 Subject: [PATCH 14/50] fix(ci): allow Intel bootstrap acceptance to finish compiling --- .../workflows/plugin-bootstrap-environment.yml | 2 +- .../runtime-startup.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index 30b4b8bc..8d8a002a 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -29,7 +29,7 @@ jobs: bootstrap: name: Host bootstrap (${{ matrix.os }}) runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: diff --git a/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md b/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md index 8d1feb4e..3e12340b 100644 --- a/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md +++ b/openspec/changes/complete-plugin-hook-runtime/runtime-startup.md @@ -114,3 +114,20 @@ initial runtime lookup still uses owner PATH; production launch is still the legacy sandbox; prepared-image native integration and protected/alias-root composition remain unimplemented. Windows fixture receipts prove CLI behavior only. These are open parent tasks, not claims of completed capability enforcement. + + +## Intel hosted job budget + +At source `9e802ec1`, [Intel job 123660939230](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40934385871/job/123660939230) +was cancelled by its 30-minute job limit. GitHub's annotation explicitly records +that maximum execution time. Helper build, environment construction, bundled +runtime startup, missing-filter controls and the prepared workspace boundary +step passed. The job was still compiling Rust dependencies for the production App +plugin tests when cancelled. Those App tests and the later overlay control are +not passing results for that job. + +The workflow job budget is now 60 minutes to accommodate that compilation plus +the required regressions. Runtime receipt deadlines, control assertions, worker +limits and source restoration are unchanged. Fresh hosted Intel completion is +still required; neither the cancellation nor increasing the build allowance +establishes acceptance. From 0cf48fc309197c178900340f44defaa787626279 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:46:46 -0400 Subject: [PATCH 15/50] test(sandbox): prove Linux protected mount ancestry and file masks --- .github/workflows/plugin-linux-mounts.yml | 52 ++++++ .../linux-mount-protection.md | 55 ++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- .../plugin-native-entry/linux_mounts.mts | 168 ++++++++++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/plugin-linux-mounts.yml create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md create mode 100644 scripts/probes/plugin-native-entry/linux_mounts.mts diff --git a/.github/workflows/plugin-linux-mounts.yml b/.github/workflows/plugin-linux-mounts.yml new file mode 100644 index 00000000..0bf198fc --- /dev/null +++ b/.github/workflows/plugin-linux-mounts.yml @@ -0,0 +1,52 @@ +name: Linux plugin mount composition +on: + workflow_dispatch: + pull_request: + paths: + - scripts/probes/plugin-native-entry/linux_mounts.mts + - .github/workflows/plugin-linux-mounts.yml + push: + branches: [main] + paths: + - scripts/probes/plugin-native-entry/linux_mounts.mts + - .github/workflows/plugin-linux-mounts.yml +permissions: + contents: read +concurrency: + group: plugin-linux-mounts-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + mounts: + name: Mount composition (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Prepare namespace prerequisites + shell: bash + run: | + set -euo pipefail + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* + sudo apt-get update + sudo apt-get install -y bubblewrap python3 + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + - name: Verify native controls and ancestor anchors + shell: bash + run: | + set -euo pipefail + node --experimental-strip-types scripts/probes/plugin-native-entry/linux_mounts.mts --report "$RUNNER_TEMP/linux-mounts.json" + - name: Retain native mount receipts + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-mounts-${{ matrix.os }} + path: ${{ runner.temp }}/linux-mounts.json + if-no-files-found: warn + retention-days: 14 diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md new file mode 100644 index 00000000..4c2b9486 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md @@ -0,0 +1,55 @@ +# Linux protected mount ancestry experiment + +This continues native proof task 2.1 and the policy-composition work in task 2.2. +It does not activate production plugin confinement. The existing Landlock entry +requires a private mount/PID/network namespace and protected subtree masks. +[Landlock's kernel documentation](https://docs.kernel.org/userspace-api/landlock.html#layers-of-file-path-access-rights) +explains that a grant from a path ancestor can allow operations on descendants +within a policy layer; a child grant is not a subtractive denial. + +The new experiment tests whether masking a private child or mounting an immutable +package read-only is sufficient to prevent moving its ancestor. Its explicit +unanchored writable control requires such parent/grandparent renames to succeed. +The candidate adds same-path bind mounts for every protected ancestor strictly +inside Location, in shallow-to-deep order, before mounting immutable objects and +masking private objects. Writable anchors must keep public sibling writes usable; +readonly Location uses readonly anchors. A protected ancestor should itself be a +busy mountpoint rather than relying on the child's read/write mode to protect it. + +A separate character-device mask control tests whether binding /dev/null over a +private file can still report a successful write. The candidate instead uses an +owner-created, empty, mode-000 regular file on a readonly bind mount. Both views +must leave the owner's actual private bytes intact. This file is new owned +metadata; no private regular file is opened in an owner holding SQLite locks. + +The driver runs system Python inside bubblewrap with cleared environment, dropped +capabilities, disabled nested user namespaces and private PID/network/IPC/UTS +namespaces. It exchanges a fixed JSON-RPC-shaped receipt, checks direct private +reads/writes, symlink reads, immutable writes, protected parent/grandparent renames, +ordinary public writes/renames, scratch writes and unchanged owner bytes. Paths +include spaces, quotes and a backslash; arguments never pass through a shell. +Reports bind the driver/workflow sources and checkout, record observed rows and +explicit pass/fail outcome, and are retained even if a later assertion fails. + +Five cases are required: writable unanchored control, writable anchored candidate, +readonly unanchored/anchored cases, and the writable anchored character-mask +counterexample. No failed bootstrap can count as a denial. The actor has a +15-second subprocess timeout and bounded output; each temporary fixture is owned +by the driver and removed after its actor settles. + +## Current verification and scope + +Local macOS checks pass TypeScript syntax, the actor's Python AST syntax, portable +ancestor ordering/sibling-prefix checks, workflow YAML and both Bash blocks, and +diff checks. Linux execution is unavailable locally. The new lightweight workflow +requires native Ubuntu x64 and arm64 receipts before the candidate can be treated +as proved. Expected control/candidate outcomes here remain hypotheses until those +jobs finish; no Linux native acceptance is claimed by these local checks. + +This is a mount primitive experiment, without Landlock or the packaged native +entry. It does not prove fs.read/shell/network/secret intersections, immutable +package/runtime/helper admission, pre-existing alias inventory, owner namespace +provisioning, generation lifetime or actual App/server enforcement. A successful +experiment must be followed by production Linux composition and real-helper +regressions. The approved support/design gates and all parent acceptance tasks +remain open. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 0a070618..273a437d 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; 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) tests parent binding and regular-file mask primitives before Linux composition; 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/linux_mounts.mts b/scripts/probes/plugin-native-entry/linux_mounts.mts new file mode 100644 index 00000000..4f94f629 --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_mounts.mts @@ -0,0 +1,168 @@ +/** Native Linux mount-composition experiment; not plugin launch admission. */ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { spawnSync } from "node:child_process"; +import { parseArgs } from "node:util"; + +const actor = String.raw` +import errno, json, os, sys +from pathlib import Path +p = json.loads(sys.argv[1]) +def attempt(action): + try: + action() + return {"ok": True, "errno": None} + except OSError as error: + return {"ok": False, "errno": error.errno} +def rename_back(source, destination): + os.rename(source, destination) + os.rename(destination, source) +def read_kind(path): + try: + value = Path(path).read_bytes() + return "empty" if value == b"" else "data" + except OSError: + return "denied" +renames = [] +siblings = [] +for label, root in p["roots"].items(): + parent = Path(root).parent + for level in range(2): + source = parent if level == 0 else parent.parent + destination = source.with_name(source.name + "-moved") + renames.append({"root": label, "level": level, + "result": attempt(lambda: rename_back(source, destination))}) + siblings.append(attempt(lambda: (source / "public-sibling").write_bytes(b"fixture-write"))) +immutable = [attempt(lambda path=path: Path(path).write_bytes(b"must-not-write")) + for path in p["immutable_data"]] +public = Path(p["project"]) / "public" +result = {"started": True, "renames": renames, "siblings": siblings, + "immutable_writes": immutable, + "private_directory_read": read_kind(p["private_data"]), + "private_file_read": read_kind(p["roots"]["private_file"]), + "private_writes": [attempt(lambda path=path: Path(path).write_bytes(b"must-not-write")) + for path in [p["private_data"], p["roots"]["private_file"]]], + "symlink_private_read": read_kind(p["alias"]), + "workspace_write": attempt(lambda: (Path(p["project"]) / "written").write_bytes(b"fixture-write")), + "public_rename": attempt(lambda: rename_back(public, public.with_name("public-moved"))), + "scratch_write": attempt(lambda: (Path(p["scratch"]) / "written").write_bytes(b"fixture-write"))} +print(json.dumps({"jsonrpc": "2.0", "id": "mount-proof", "result": result})) +`; + +type Layout = { + project: string; scratch: string; empty_mask: string; write: boolean; + roots: Record; immutable_data: string[]; + private_data: string; alias: string; +}; +function beneath(path: string, parent: string): boolean { + const rest = relative(parent, path); + return rest !== "" && rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest); +} +function anchors(layout: Layout): string[] { + const paths = new Set(); + for (const root of Object.values(layout.roots)) { + for (let parent = dirname(root); beneath(parent, layout.project); parent = dirname(parent)) paths.add(parent); + } + return [...paths].sort((a, b) => a.split(sep).length - b.split(sep).length || a.localeCompare(b)); +} +function fixture(root: string, write: boolean): Layout { + const project = join(root, 'project "quote" \\ workspace'); + const scratch = join(root, "scratch"); + const empty_mask = join(root, "owned-empty-mask"); + writeFileSync(empty_mask, "", { mode: 0o000 }); + const roots = { package: join(project, "extensions/level/package"), + runtime_file: join(project, "toolchain/level/runtime-file"), + private_directory: join(project, ".owner/level/state"), + private_file: join(project, ".secrets/level/key") }; + for (const path of [roots.package, dirname(roots.runtime_file), roots.private_directory, + dirname(roots.private_file), scratch, join(project, "public")]) mkdirSync(path, { recursive: true, mode: 0o700 }); + const immutable_data = [join(roots.package, "data"), roots.runtime_file]; + const private_data = join(roots.private_directory, "data"); + for (const path of [...immutable_data, private_data, roots.private_file]) writeFileSync(path, "owned-fixture-only", { mode: 0o600 }); + // The symlink itself is created by the owner before the read-only package mount. + const alias = join(roots.package, "alias"); + return { project, scratch, empty_mask, write, roots, immutable_data, private_data, alias }; +} +function argumentsFor(layout: Layout, anchored: boolean, deviceMask: boolean): string[] { + const args = ["--die-with-parent", "--unshare-user", "--unshare-pid", "--unshare-net", + "--unshare-ipc", "--unshare-uts", "--disable-userns", "--cap-drop", "ALL", "--clearenv", + "--setenv", "PATH", "/usr/bin:/bin", "--ro-bind", "/", "/", "--proc", "/proc", "--dev", "/dev", + layout.write ? "--bind" : "--ro-bind", layout.project, layout.project]; + if (anchored) for (const parent of anchors(layout)) args.push(layout.write ? "--bind" : "--ro-bind", parent, parent); + for (const path of [layout.roots.package, layout.roots.runtime_file]) args.push("--ro-bind", path, path); + args.push("--perms", "0000", "--tmpfs", layout.roots.private_directory, "--remount-ro", layout.roots.private_directory, + "--ro-bind", deviceMask ? "/dev/null" : layout.empty_mask, layout.roots.private_file, + "--bind", layout.scratch, layout.scratch, "--chdir", layout.scratch, + "--", "/usr/bin/python3", "-I", "-S", "-c", actor, JSON.stringify(layout)); + return args; +} +function verify(observed: any, write: boolean, anchored: boolean, deviceMask: boolean): void { + assert.equal(observed.started, true, "a failed bootstrap cannot prove a mount denial"); + assert.equal(observed.renames.length, 8); + for (const entry of observed.renames) { + assert.equal(entry.result.ok, write && !anchored, `ancestor binding: ${JSON.stringify(entry)}`); + if (write && anchored) assert.equal(entry.result.errno, 16, "protected ancestors must be busy mountpoints"); + } + for (const entry of observed.siblings) assert.equal(entry.ok, write, "ancestor anchors must preserve sibling writes"); + for (const entry of observed.immutable_writes) assert.deepEqual(entry, { ok: false, errno: 30 }, "immutable data must be on read-only mounts"); + assert.equal(observed.private_directory_read, "denied"); + assert.equal(observed.private_file_read, deviceMask ? "empty" : "denied"); + assert.equal(observed.private_writes[0].ok, false); + assert.equal(observed.private_writes[1].ok, deviceMask, "a device mask is not a read-only regular file"); + assert.equal(observed.symlink_private_read, "denied"); + assert.equal(observed.workspace_write.ok, write); + assert.equal(observed.public_rename.ok, write, "ordinary public renames must remain usable"); + assert.equal(observed.scratch_write.ok, true); +} +function selfTest(): void { + const layout = { project: "/project", roots: { a: "/project/first/deep/private", b: "/project/second/file" } } as Layout; + assert.deepEqual(anchors(layout), ["/project/first", "/project/second", "/project/first/deep"]); + assert.equal(beneath("/project-other/file", "/project"), false); + const syntax = spawnSync("/usr/bin/python3", ["-I", "-S", "-c", "import ast, sys; ast.parse(sys.stdin.read())"], + { input: actor, encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin" } }); + assert.equal(syntax.status, 0, syntax.stderr); +} + +const { values } = parseArgs({ options: { report: { type: "string" }, "self-test": { type: "boolean" } } }); +selfTest(); +if (!values["self-test"]) { + assert.equal(process.platform, "linux", "native mount receipts require Linux"); + assert(values.report, "--report is required for native receipts"); + const rows = []; + let outcome = "failed"; + try { + for (const [write, anchored, deviceMask] of [[true, false, false], [true, true, false], + [false, false, false], [false, true, false], [true, true, true]]) { + const root = realpathSync(mkdtempSync(join(tmpdir(), "cyber-linux-mounts-"))); + try { + const layout = fixture(root, write); + symlinkSync(layout.private_data, layout.alias); + const result = spawnSync("/usr/bin/bwrap", argumentsFor(layout, anchored, deviceMask), + { encoding: "utf8", timeout: 15_000, maxBuffer: 64 * 1024, env: { PATH: "/usr/bin:/bin" } }); + assert.equal(result.status, 0, `mount bootstrap failed: ${result.error ?? result.stderr}`); + const packet = JSON.parse(result.stdout); + assert.equal(packet.jsonrpc, "2.0"); + assert.equal(packet.id, "mount-proof"); + const ownerDataUnchanged = [...layout.immutable_data, layout.private_data, layout.roots.private_file] + .every(path => readFileSync(path, "utf8") === "owned-fixture-only"); + rows.push({ write, anchored, deviceMask, observations: packet.result, owner_data_unchanged: ownerDataUnchanged }); + verify(packet.result, write, anchored, deviceMask); + assert.equal(ownerDataUnchanged, true, "owner data changed"); + } finally { rmSync(root, { recursive: true, force: true }); } + } + outcome = "passed"; + } finally { + const sources = ["scripts/probes/plugin-native-entry/linux_mounts.mts", ".github/workflows/plugin-linux-mounts.yml"]; + const hash = createHash("sha256"); + for (const path of sources) hash.update(path + "\0").update(readFileSync(path)); + const report = { platform: process.platform, architecture: process.arch, checkout: process.env.GITHUB_SHA ?? null, + run_id: process.env.GITHUB_RUN_ID ?? null, outcome, sources, source_sha256: hash.digest("hex"), rows, + scope: "Mount primitive experiment only. No Landlock, native helper/runtime admission, network capability, Windows or production activation proof." }; + mkdirSync(dirname(resolve(values.report)), { recursive: true }); + writeFileSync(values.report, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 }); + } + console.log(`Verified ${rows.length} mount cases with positive startup, public writes/renames and owner-data checks`); +} From ee7268c669b49171e8d886586e80771d87c1b507 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:48:12 -0400 Subject: [PATCH 16/50] test(sandbox): record kernel and mount runtime versions --- scripts/probes/plugin-native-entry/linux_mounts.mts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/probes/plugin-native-entry/linux_mounts.mts b/scripts/probes/plugin-native-entry/linux_mounts.mts index 4f94f629..9bfe89e9 100644 --- a/scripts/probes/plugin-native-entry/linux_mounts.mts +++ b/scripts/probes/plugin-native-entry/linux_mounts.mts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { release, tmpdir } from "node:os"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import { parseArgs } from "node:util"; @@ -131,6 +131,12 @@ selfTest(); if (!values["self-test"]) { assert.equal(process.platform, "linux", "native mount receipts require Linux"); assert(values.report, "--report is required for native receipts"); + const bwrap = spawnSync("/usr/bin/bwrap", ["--version"], + { encoding: "utf8", timeout: 10_000, maxBuffer: 4096, env: { PATH: "/usr/bin:/bin" } }); + assert.equal(bwrap.status, 0, "bubblewrap version query failed"); + const python = spawnSync("/usr/bin/python3", ["-I", "-S", "--version"], + { encoding: "utf8", timeout: 10_000, maxBuffer: 4096, env: { PATH: "/usr/bin:/bin" } }); + assert.equal(python.status, 0, "Python version query failed"); const rows = []; let outcome = "failed"; try { @@ -158,7 +164,8 @@ if (!values["self-test"]) { const sources = ["scripts/probes/plugin-native-entry/linux_mounts.mts", ".github/workflows/plugin-linux-mounts.yml"]; const hash = createHash("sha256"); for (const path of sources) hash.update(path + "\0").update(readFileSync(path)); - const report = { platform: process.platform, architecture: process.arch, checkout: process.env.GITHUB_SHA ?? null, + const report = { platform: process.platform, architecture: process.arch, kernel_release: release(), + bubblewrap_version: bwrap.stdout.trim(), python_version: python.stdout.trim(), checkout: process.env.GITHUB_SHA ?? null, run_id: process.env.GITHUB_RUN_ID ?? null, outcome, sources, source_sha256: hash.digest("hex"), rows, scope: "Mount primitive experiment only. No Landlock, native helper/runtime admission, network capability, Windows or production activation proof." }; mkdirSync(dirname(resolve(values.report)), { recursive: true }); From d4fcf3ee8e0dd1838bae1818a150d5ee19d6fb6c Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:51:33 -0400 Subject: [PATCH 17/50] test(sandbox): retain observed Linux device mask denials --- .../linux-mount-protection.md | 26 ++++++++++++++++--- .../plugin-native-entry/linux_mounts.mts | 17 +++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md index 4c2b9486..81a41d8d 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md @@ -16,8 +16,8 @@ masking private objects. Writable anchors must keep public sibling writes usable readonly Location uses readonly anchors. A protected ancestor should itself be a busy mountpoint rather than relying on the child's read/write mode to protect it. -A separate character-device mask control tests whether binding /dev/null over a -private file can still report a successful write. The candidate instead uses an +A separate character-device mask comparison records binding /dev/null over a +private file. The candidate uses an owner-created, empty, mode-000 regular file on a readonly bind mount. Both views must leave the owner's actual private bytes intact. This file is new owned metadata; no private regular file is opened in an owner holding SQLite locks. @@ -33,7 +33,7 @@ explicit pass/fail outcome, and are retained even if a later assertion fails. Five cases are required: writable unanchored control, writable anchored candidate, readonly unanchored/anchored cases, and the writable anchored character-mask -counterexample. No failed bootstrap can count as a denial. The actor has a +comparison. No failed bootstrap can count as a denial. The actor has a 15-second subprocess timeout and bounded output; each temporary fixture is owned by the driver and removed after its actor settles. @@ -53,3 +53,23 @@ provisioning, generation lifetime or actual App/server enforcement. A successful experiment must be followed by production Linux composition and real-helper regressions. The approved support/design gates and all parent acceptance tasks remain open. + + +## First native observations at `ee7268c6` + +Run [40943429793](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40943429793) +failed both Linux jobs on the diagnostic's expected empty device read: both +observed a denial instead. The retained x64 report contains all five rows. Its +first four cases satisfy their expected mount assertions: all eight unanchored +writable ancestor renames succeed; all eight anchored writable attempts return +EBUSY; sibling/public writes and public renames remain usable; readonly cases +refuse workspace writes; regular-file masks refuse private reads/writes; all +owner bytes remain unchanged. This partial evidence does not turn a failed job +into acceptance. + +The expected successful device write/empty read was an unproved hypothesis and +is not claimed as an observed bypass. The comparison now requires the observed +read/write denial and records file kind, mode and nodev mount flag to explain the +actual view without assigning a cause merely from EACCES. Candidate regular-file +requirements and the successful unanchored rename counterexample are unchanged. +Fresh complete native runs are required after this diagnostic correction. diff --git a/scripts/probes/plugin-native-entry/linux_mounts.mts b/scripts/probes/plugin-native-entry/linux_mounts.mts index 9bfe89e9..7d1b6493 100644 --- a/scripts/probes/plugin-native-entry/linux_mounts.mts +++ b/scripts/probes/plugin-native-entry/linux_mounts.mts @@ -8,7 +8,7 @@ import { spawnSync } from "node:child_process"; import { parseArgs } from "node:util"; const actor = String.raw` -import errno, json, os, sys +import errno, json, os, stat, sys from pathlib import Path p = json.loads(sys.argv[1]) def attempt(action): @@ -39,7 +39,13 @@ for label, root in p["roots"].items(): immutable = [attempt(lambda path=path: Path(path).write_bytes(b"must-not-write")) for path in p["immutable_data"]] public = Path(p["project"]) / "public" -result = {"started": True, "renames": renames, "siblings": siblings, +mask_metadata = os.stat(p["roots"]["private_file"]) +mask_flags = os.statvfs(p["roots"]["private_file"]).f_flag +result = {"started": True, + "mask_info": {"character": stat.S_ISCHR(mask_metadata.st_mode), + "regular": stat.S_ISREG(mask_metadata.st_mode), + "mode": stat.S_IMODE(mask_metadata.st_mode), + "nodev": bool(mask_flags & os.ST_NODEV)}, "renames": renames, "siblings": siblings, "immutable_writes": immutable, "private_directory_read": read_kind(p["private_data"]), "private_file_read": read_kind(p["roots"]["private_file"]), @@ -109,9 +115,12 @@ function verify(observed: any, write: boolean, anchored: boolean, deviceMask: bo for (const entry of observed.siblings) assert.equal(entry.ok, write, "ancestor anchors must preserve sibling writes"); for (const entry of observed.immutable_writes) assert.deepEqual(entry, { ok: false, errno: 30 }, "immutable data must be on read-only mounts"); assert.equal(observed.private_directory_read, "denied"); - assert.equal(observed.private_file_read, deviceMask ? "empty" : "denied"); + assert.equal(observed.private_file_read, "denied"); + assert.equal(observed.mask_info.character, deviceMask); + assert.equal(observed.mask_info.regular, !deviceMask); assert.equal(observed.private_writes[0].ok, false); - assert.equal(observed.private_writes[1].ok, deviceMask, "a device mask is not a read-only regular file"); + assert.equal(observed.private_writes[1].ok, false); + if (deviceMask) assert.equal(observed.private_writes[1].errno, 13, "retain the observed device-mask denial"); assert.equal(observed.symlink_private_read, "denied"); assert.equal(observed.workspace_write.ok, write); assert.equal(observed.public_rename.ok, write, "ordinary public renames must remain usable"); From 2548fdeaf68fdfe6e13f8ecfa87e77ae41e5bcd7 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 15:58:12 -0400 Subject: [PATCH 18/50] docs(sandbox): record Linux protected mount primitive receipts --- .../linux-mount-protection.md | 38 +- .../complete-plugin-hook-runtime/tasks.md | 2 +- .../linux-mounts-d4fcf3ee/linux-arm64.json | 768 ++++++++++++++++++ .../linux-x64-initial-failed.json | 738 +++++++++++++++++ .../linux-mounts-d4fcf3ee/linux-x64.json | 768 ++++++++++++++++++ 5 files changed, 2310 insertions(+), 4 deletions(-) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-arm64.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64-initial-failed.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md index 81a41d8d..67da2b68 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-mount-protection.md @@ -42,9 +42,8 @@ by the driver and removed after its actor settles. Local macOS checks pass TypeScript syntax, the actor's Python AST syntax, portable ancestor ordering/sibling-prefix checks, workflow YAML and both Bash blocks, and diff checks. Linux execution is unavailable locally. The new lightweight workflow -requires native Ubuntu x64 and arm64 receipts before the candidate can be treated -as proved. Expected control/candidate outcomes here remain hypotheses until those -jobs finish; no Linux native acceptance is claimed by these local checks. +requires native Ubuntu x64 and arm64 receipts. Those mount-only receipts are now +recorded below; local checks alone did not establish them. This is a mount primitive experiment, without Landlock or the packaged native entry. It does not prove fs.read/shell/network/secret intersections, immutable @@ -73,3 +72,36 @@ read/write denial and records file kind, mode and nodev mount flag to explain th actual view without assigning a cause merely from EACCES. Candidate regular-file requirements and the successful unanchored rename counterexample are unchanged. Fresh complete native runs are required after this diagnostic correction. + + +## Verified mount-only receipts at `d4fcf3ee` + +The corrected [x64 job](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40943857344/job/123693935943) +and [arm64 job](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40943857344/job/123693935579) +both pass all five cases. Both report Linux `7.0.0-1017-azure`, bubblewrap `0.9.0` +and system Python `3.12.3`. The exact passing reports are preserved at +[the evidence directory](../../../scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/), +with the initial x64 failed report retained separately and explicitly marked failed. + +Both source manifests independently recompute to +`fc98524c490a23bc18e373783cdea32b55ceb400dee2d22a9b78c3d3f32660ed` +from fetched checkout `d6afa580`. Its verified parents are `7261e888` and +`d4fcf3ee`. The retained initial failed report also matches its own exact source +checkout and is not counted as passing acceptance. + +Every passing report retains positive actor startup and unchanged owner bytes. +Unanchored writable cases perform all eight protected ancestor moves. Anchored +writable cases refuse all eight with EBUSY while permitting all sibling writes, +public writes/renames and scratch writes. Readonly cases reject workspace writes +but keep scratch usable. Immutable writes refuse with EROFS; private reads and +writes are refused. The /dev/null comparison records a mode-0666 character device, +nodev mount flag and EACCES write result, rather than the originally hypothesized +successful device write. The regular-file candidate remains independent of that +device-specific outcome. + +The scoped review covered driver/actor separation, direct argument passing, +cleared child environment, namespace/capability flags, shallow-first anchor order, +positive controls, exact errors and unchanged owner data, bounded receipts and +source-bound failure reporting. No blocking issue was found within this mount +experiment. These receipts prove the primitive only: production Linux composition, +Landlock/helper integration and complete owner admission remain unimplemented. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 273a437d..58cc8213 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) tests parent binding and regular-file mask primitives before 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 production Linux composition; 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-mounts-d4fcf3ee/linux-arm64.json b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-arm64.json new file mode 100644 index 00000000..64a47516 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-arm64.json @@ -0,0 +1,768 @@ +{ + "platform": "linux", + "architecture": "arm64", + "kernel_release": "7.0.0-1017-azure", + "bubblewrap_version": "bubblewrap 0.9.0", + "python_version": "Python 3.12.3", + "checkout": "d6afa580f51a2a6ec1ad0fec823a8fcc8200e75b", + "run_id": "40943857344", + "outcome": "passed", + "sources": [ + "scripts/probes/plugin-native-entry/linux_mounts.mts", + ".github/workflows/plugin-linux-mounts.yml" + ], + "source_sha256": "fc98524c490a23bc18e373783cdea32b55ceb400dee2d22a9b78c3d3f32660ed", + "rows": [ + { + "write": true, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": true, + "observations": { + "started": true, + "mask_info": { + "character": true, + "regular": false, + "mode": 438, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 13 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + } + ], + "scope": "Mount primitive experiment only. No Landlock, native helper/runtime admission, network capability, Windows or production activation proof." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64-initial-failed.json b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64-initial-failed.json new file mode 100644 index 00000000..21341bb4 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64-initial-failed.json @@ -0,0 +1,738 @@ +{ + "platform": "linux", + "architecture": "x64", + "kernel_release": "7.0.0-1017-azure", + "bubblewrap_version": "bubblewrap 0.9.0", + "python_version": "Python 3.12.3", + "checkout": "a2dfc61265c3b8c19a28d928e1724b9f47c5ef72", + "run_id": "40943429793", + "outcome": "failed", + "sources": [ + "scripts/probes/plugin-native-entry/linux_mounts.mts", + ".github/workflows/plugin-linux-mounts.yml" + ], + "source_sha256": "586261b69ef979a45fde591e17716ee312778fcced8705a5243506a922dec9c6", + "rows": [ + { + "write": true, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": true, + "observations": { + "started": true, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 13 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + } + ], + "scope": "Mount primitive experiment only. No Landlock, native helper/runtime admission, network capability, Windows or production activation proof." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64.json b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64.json new file mode 100644 index 00000000..526f045c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-mounts-d4fcf3ee/linux-x64.json @@ -0,0 +1,768 @@ +{ + "platform": "linux", + "architecture": "x64", + "kernel_release": "7.0.0-1017-azure", + "bubblewrap_version": "bubblewrap 0.9.0", + "python_version": "Python 3.12.3", + "checkout": "d6afa580f51a2a6ec1ad0fec823a8fcc8200e75b", + "run_id": "40943857344", + "outcome": "passed", + "sources": [ + "scripts/probes/plugin-native-entry/linux_mounts.mts", + ".github/workflows/plugin-linux-mounts.yml" + ], + "source_sha256": "fc98524c490a23bc18e373783cdea32b55ceb400dee2d22a9b78c3d3f32660ed", + "rows": [ + { + "write": true, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": true, + "errno": null + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": true, + "errno": null + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": false, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": false, + "anchored": true, + "deviceMask": false, + "observations": { + "started": true, + "mask_info": { + "character": false, + "regular": true, + "mode": 0, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 30 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 30 + } + } + ], + "siblings": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 30 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": false, + "errno": 30 + }, + "public_rename": { + "ok": false, + "errno": 30 + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + }, + { + "write": true, + "anchored": true, + "deviceMask": true, + "observations": { + "started": true, + "mask_info": { + "character": true, + "regular": false, + "mode": 438, + "nodev": true + }, + "renames": [ + { + "root": "package", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "package", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "runtime_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_directory", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 0, + "result": { + "ok": false, + "errno": 16 + } + }, + { + "root": "private_file", + "level": 1, + "result": { + "ok": false, + "errno": 16 + } + } + ], + "siblings": [ + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + }, + { + "ok": true, + "errno": null + } + ], + "immutable_writes": [ + { + "ok": false, + "errno": 30 + }, + { + "ok": false, + "errno": 30 + } + ], + "private_directory_read": "denied", + "private_file_read": "denied", + "private_writes": [ + { + "ok": false, + "errno": 13 + }, + { + "ok": false, + "errno": 13 + } + ], + "symlink_private_read": "denied", + "workspace_write": { + "ok": true, + "errno": null + }, + "public_rename": { + "ok": true, + "errno": null + }, + "scratch_write": { + "ok": true, + "errno": null + } + }, + "owner_data_unchanged": true + } + ], + "scope": "Mount primitive experiment only. No Landlock, native helper/runtime admission, network capability, Windows or production activation proof." +} 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 19/50] 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 20/50] 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 21/50] 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 22/50] 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." +} From 505537ff0cfe3ff2999f3335d58df17292c9bac9 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:14:56 -0400 Subject: [PATCH 23/50] test(plugins): probe native Linux host socket reachability --- .github/workflows/plugin-linux-ipc.yml | 51 ++++ .../examples/plugin_unix_socket_probe.rs | 218 ++++++++++++++++++ .../tests/fixtures/plugin_unix_socket.c | 35 +++ .../linux-ipc-proof.md | 45 ++++ .../complete-plugin-hook-runtime/tasks.md | 4 +- 5 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/plugin-linux-ipc.yml create mode 100644 crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml new file mode 100644 index 00000000..936b9347 --- /dev/null +++ b/.github/workflows/plugin-linux-ipc.yml @@ -0,0 +1,51 @@ +name: Linux plugin IPC diagnostic +on: + workflow_dispatch: + pull_request: + paths: + - crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs + - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c + - .github/workflows/plugin-linux-ipc.yml +permissions: + contents: read +concurrency: + group: plugin-linux-ipc-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + ipc: + name: UNIX socket diagnostic (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + env: + CARGO_INCREMENTAL: "0" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Prepare namespace prerequisites + shell: bash + run: | + set -euo pipefail + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* + sudo apt-get update + sudo apt-get install -y bubblewrap build-essential pkg-config cmake + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + - name: Build production helper and diagnostic + run: cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe + - name: Observe fixture-only host socket reachability + shell: bash + run: | + set -euo pipefail + target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" + - name: Retain diagnostic observations + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-ipc-${{ matrix.os }} + path: ${{ runner.temp }}/linux-ipc.json + if-no-files-found: warn + retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs new file mode 100644 index 00000000..17ff2bdf --- /dev/null +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -0,0 +1,218 @@ +//! Diagnostic evidence, not capability acceptance. Uses only owned test sockets. +#[cfg(unix)] +mod probe { + use cyber_sandbox::plugin_policy::{LinuxPolicy, MaskPath}; + use serde_json::{Value, json}; + use sha2::{Digest, Sha256}; + use std::io::Read; + use std::os::unix::{fs::PermissionsExt, fs::symlink, net::UnixListener}; + use std::path::{Path, PathBuf}; + use std::process::{Command, Output, Stdio}; + use std::time::{Duration, Instant}; + + fn run(command: &mut Command) -> Output { + let mut child = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(15); + loop { + if child.try_wait().unwrap().is_some() { + let output = child.wait_with_output().unwrap(); + assert!( + output.status.success(), + "fixture bootstrap failed: {output:?}" + ); + return output; + } + if Instant::now() >= deadline { + let _ = child.kill(); + let _ = child.wait(); + panic!("fixture did not settle"); + } + std::thread::sleep(Duration::from_millis(10)); + } + } + + fn receipt(listener: &UnixListener, observed: &Value) -> bool { + assert_eq!(observed["started"], true); + match listener.accept() { + Ok((mut stream, _)) => { + assert_eq!(observed["connected"], true); + assert_eq!(observed["sent"], true); + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let mut token = [0; 12]; + stream.read_exact(&mut token).unwrap(); + assert_eq!(&token, b"fixture-only"); + true + } + Err(error) => { + assert_eq!(error.kind(), std::io::ErrorKind::WouldBlock); + assert_eq!(observed["connected"], false); + assert_eq!(observed["sent"], false); + false + } + } + } + + pub fn main() { + assert_eq!( + std::env::consts::OS, + "linux", + "native evidence requires Linux" + ); + let args: Vec<_> = std::env::args_os().skip(1).collect(); + assert_eq!( + args.len(), + 2, + "expected explicit helper path and report path" + ); + let helper = std::fs::canonicalize(&args[0]).unwrap(); + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path().canonicalize().unwrap(); + let project = root.join("project"); + let package = project.join("package"); + let private = project.join("private"); + let scratch = root.join("scratch"); + for directory in [&project, &package, &private, &scratch] { + std::fs::create_dir_all(directory).unwrap(); + } + let data = project.join("data"); + std::fs::write(&data, b"fixture-only").unwrap(); + let target = package.join("socket-client"); + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + run(Command::new("cc") + .env_clear() + .env("PATH", "/usr/bin:/bin") + .args(["-Wall", "-Wextra", "-Werror"]) + .arg(workspace.join("crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c")) + .arg("-o") + .arg(&target)); + let mask = root.join("empty-mask"); + std::fs::write(&mask, []).unwrap(); + std::fs::set_permissions(&mask, std::fs::Permissions::from_mode(0o000)).unwrap(); + let profile = root.join("profile"); + let bootstrap: Vec = ["/usr", "/lib", "/lib64", "/bin", "/etc/ld.so.cache"] + .into_iter() + .filter(|path| Path::new(path).exists()) + .map(|path| std::fs::canonicalize(path).unwrap()) + .collect(); + let bootstrap: Vec<_> = bootstrap.iter().map(PathBuf::as_path).collect(); + let outside = root.join("host.sock"); + let public = project.join("public.sock"); + let hidden = private.join("broker.sock"); + let alias = package.join("socket-alias"); + symlink(&outside, &alias).unwrap(); + let listeners: Vec<_> = [&outside, &public, &hidden] + .into_iter() + .map(|path| { + let listener = UnixListener::bind(path).unwrap(); + listener.set_nonblocking(true).unwrap(); + listener + }) + .collect(); + let paths = [ + (&outside, 0, "outside"), + (&public, 1, "public"), + (&hidden, 2, "private-mask"), + (&alias, 0, "symlink-outside"), + ]; + let mut rows = Vec::new(); + for (socket, index, label) in paths { + let direct: Value = serde_json::from_slice( + &run(Command::new(&target).arg(socket).arg(&data).env_clear()).stdout, + ) + .unwrap(); + assert!( + receipt(&listeners[index], &direct), + "direct socket control: {label}" + ); + assert_eq!(direct["project_read"], true); + for (read, write) in [(false, false), (false, true), (true, false), (true, true)] { + let target_args = [ + socket.to_str().unwrap().into(), + data.to_str().unwrap().into(), + ]; + let private_paths = [MaskPath::Directory(&private)]; + let plan = LinuxPolicy { + helper: &helper, + target: &target, + profile: &profile, + package: &package, + scratch: &scratch, + location: &project, + mask_file: &mask, + bootstrap: &bootstrap, + private: &private_paths, + read_only: &[], + target_args: &target_args, + fs_read: read, + fs_write: write, + shell: false, + namespace_proc: false, + } + .compose() + .unwrap(); + std::fs::write(&profile, plan.landlock).unwrap(); + let observed: Value = serde_json::from_slice( + &run(Command::new("/usr/bin/bwrap") + .args(plan.arguments) + .env_clear()) + .stdout, + ) + .unwrap(); + assert_eq!(observed["project_read"], read, "native read control"); + let delivered = receipt(&listeners[index], &observed); + if label == "private-mask" { + assert!(!delivered, "opaque private directory must hide its socket"); + } + rows.push(json!({"socket": label, "fs_read": read, "fs_write": write, + "shell": false, "host_received_fixture": delivered, "observed": observed})); + } + } + let sources = [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + ]; + let mut hash = Sha256::new(); + for path in sources { + hash.update(path.as_bytes()); + hash.update([0]); + hash.update(std::fs::read(workspace.join(path)).unwrap()); + } + let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, + "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), + "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), + "rows": rows, "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance."}); + std::fs::write( + &args[1], + format!("{}\n", serde_json::to_string_pretty(&report).unwrap()), + ) + .unwrap(); + } +} + +#[cfg(unix)] +fn main() { + probe::main(); +} +#[cfg(not(unix))] +fn main() { + panic!("native evidence requires Linux"); +} diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c new file mode 100644 index 00000000..3b20c7d4 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -0,0 +1,35 @@ +/* Fixture-only connection probe. It never contacts a non-fixture service. */ +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc != 3) return 2; + struct sockaddr_un address = { .sun_family = AF_UNIX }; + size_t length = strlen(argv[1]); + if (length == 0 || length >= sizeof(address.sun_path)) return 2; + memcpy(address.sun_path, argv[1], length + 1); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd >= 0 && fcntl(fd, F_SETFD, FD_CLOEXEC) != 0) { + close(fd); + fd = -1; + } + int connected = fd >= 0 && connect(fd, (struct sockaddr *)&address, + (socklen_t)(offsetof(struct sockaddr_un, sun_path) + length + 1)) == 0; + int error = connected ? 0 : errno; + int sent = connected && send(fd, "fixture-only", 12, MSG_NOSIGNAL) == 12; + if (fd >= 0) close(fd); + int data = open(argv[2], O_RDONLY | O_CLOEXEC); + char byte; + int readable = data >= 0 && read(data, &byte, 1) == 1; + if (data >= 0) close(data); + printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"errno\":%d,\"project_read\":%s}\n", + connected ? "true" : "false", sent ? "true" : "false", error, + readable ? "true" : "false"); + return 0; +} diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md new file mode 100644 index 00000000..11cddc70 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md @@ -0,0 +1,45 @@ +# Linux host UNIX-socket diagnostic + +This experiment addresses the outstanding native IPC intersection in tasks +2.1–2.3. It invokes the production `LinuxPolicy` composer and packaged native +helper, with shell disabled and every read/write combination. It does not change +the helper ABI floor, production confinement, support decisions or host activation. + +The owner creates three disposable pathname UNIX listeners: outside Location, +inside public Location and under a masked private directory. A package symlink +also targets the outside listener. A compiled native client attempts to connect +and send the fixed `fixture-only` token. The host accepts the connection and +verifies those exact bytes; a client-reported successful connect alone is not +sufficient evidence. Each route has an unrestricted direct positive control. +Project-file reads must independently match the read capability. The private +directory mount must hide its socket. Every process/receive has a deadline; +only owned fixture endpoints are contacted and the temporary tree is removed. + +The public/outside/alias observations are diagnostic, not assertions that host +access is acceptable. In particular a successful diagnostic job cannot certify +network isolation. No real broker request, secret access or filesystem/shell +escalation is performed. The JSON records the precise source hash, helper hash, +checkout and run plus each observation. Source provenance can be independently +recomputed from the checkout; binaries are not retained by this workflow. + +Native x64/arm64 CI execution is required. Local macOS compilation and C syntax +checks cannot prove the Linux behavior. If the fixture proves host reachability, +the resulting support/design decision and a runtime-compatible enforcement +mechanism remain required before production activation; the existing ABI-3 +filesystem rights and private IP namespace must not be credited with a UNIX +socket denial they do not demonstrate. + +Local verification: the example passes Rust Clippy with warnings denied, the C +fixture passes `-Wall -Wextra -Werror` compilation and a direct native macOS +connection/token-receipt check, and workflow YAML plus its three shell blocks +parse. Formatting, diff checks, all 107 strict OpenSpec items and spec lint +(zero errors, 25 warnings) pass. The C build reports two deprecated Seatbelt SDK +calls; their origin is not assigned. Linux execution remains pending. + +The scoped code/security review inspected the example, native fixture, workflow +and task/docs changes. The client accepts only an explicit pathname and project +file from the owner; the experiment contacts owned ephemeral listeners only. +The private directory negative control and independent project-read assertion +prevent a failed bootstrap or broad read from being credited as an IPC result. +This review finds no additional source issue in the diagnostic; it does not +approve the unresolved production IPC/network boundary. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 10a1fe2c..648f3114 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -8,8 +8,8 @@ ## 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 [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.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 supplies bounded arguments/profile and actual-helper regressions passing on Linux x64/arm64 at `546641f9`; 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. [Linux IPC diagnostic](linux-ipc-proof.md) tests fixture-owned host socket reachability through the composed production boundary; it does not certify the network capability or close this task. - [ ] 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. ## 3. Host contribution lifecycle From 18791fbe0089c95708956da0f3c4a47e3035c31d Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:18:33 -0400 Subject: [PATCH 24/50] docs(plugins): record native UNIX socket counterexample --- .../linux-ipc-proof.md | 21 ++ .../evidence/linux-ipc-505537ff/arm64.json | 250 ++++++++++++++++++ .../evidence/linux-ipc-505537ff/x64.json | 250 ++++++++++++++++++ 3 files changed, 521 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/arm64.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/x64.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md index 11cddc70..371bffd2 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-proof.md @@ -43,3 +43,24 @@ The private directory negative control and independent project-read assertion prevent a failed bootstrap or broad read from being credited as an IPC result. This review finds no additional source issue in the diagnostic; it does not approve the unresolved production IPC/network boundary. + +## Observed native counterexample + +At PR head `505537ff0cfe3ff2999f3335d58df17292c9bac9`, both diagnostic jobs +complete successfully: [x64](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40956201103/job/123736839891) +and [arm64](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40956201103/job/123736839593). +All 12 public/outside/symlink rows per architecture deliver the fixed token to +the host owner, including read/write/shell all disabled. All four private-mask +rows per architecture deny connection. Independent project reads match the read +flag. This proves host pathname-socket reachability through this component; +it does not demonstrate a real broker escalation or private-file read. + +[Retained observations](../../../scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/) +record merge checkout `f43bd778adc2a5849d512e4a6fe4f4863cf57eb4`, with parents +`856a70f05aea8f43390f8823122402a9c4b16a32` and +`505537ff0cfe3ff2999f3335d58df17292c9bac9`. Independent recomputation from every +manifest path, NUL and exact checkout bytes matches source SHA-256 +`0045db0a0dde19c015947db3a9c029572c39ad87a3f27718a96b05b258c2c289`. +Helper hashes match the prior composed-policy jobs; these remain job-reported +hashes, since no binary download was retained. A green diagnostic job records a +confinement gap here, not network acceptance. Production activation remains gated. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/arm64.json b/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/arm64.json new file mode 100644 index 00000000..b4e3c893 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/arm64.json @@ -0,0 +1,250 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "f43bd778adc2a5849d512e4a6fe4f4863cf57eb4", + "run_id": "40956201103", + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml" + ], + "source_sha256": "0045db0a0dde19c015947db3a9c029572c39ad87a3f27718a96b05b258c2c289", + "helper_sha256": "effbfd2d592bd9bdadbce618f9821a040b80949d0dc8069c8128e577dcd49d28", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": true + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": true + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + } + ], + "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/x64.json b/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/x64.json new file mode 100644 index 00000000..b7d5d08f --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-ipc-505537ff/x64.json @@ -0,0 +1,250 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "f43bd778adc2a5849d512e4a6fe4f4863cf57eb4", + "run_id": "40956201103", + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml" + ], + "source_sha256": "0045db0a0dde19c015947db3a9c029572c39ad87a3f27718a96b05b258c2c289", + "helper_sha256": "22ad1c8ddc2828a2ad618dbe1289d30b91b996272cb4b1e279b38f86fb55cbff", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": true + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 13, + "project_read": true + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": true, + "observed": { + "started": true, + "connected": true, + "sent": true, + "errno": 0, + "project_read": true + } + } + ], + "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance." +} From ccde59d4c8559c3b483711d0d3b906c214246200 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:24:04 -0400 Subject: [PATCH 25/50] test(plugins): evaluate Linux socket-family confinement --- .github/workflows/plugin-linux-ipc.yml | 48 ++++++++++- .../examples/plugin_unix_socket_probe.rs | 83 ++++++++++++++++--- .../tests/fixtures/plugin_unix_socket.c | 57 ++++++++++++- .../linux-ipc-candidate.md | 56 +++++++++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- .../build_socket_candidate.py | 30 +++++++ .../linux_socket_candidate.h | 25 ++++++ 7 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md create mode 100644 scripts/probes/plugin-native-entry/build_socket_candidate.py create mode 100644 scripts/probes/plugin-native-entry/linux_socket_candidate.h diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 936b9347..84878cd1 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -6,6 +6,8 @@ on: - crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c - .github/workflows/plugin-linux-ipc.yml + - scripts/probes/plugin-native-entry/linux_socket_candidate.h + - scripts/probes/plugin-native-entry/build_socket_candidate.py permissions: contents: read concurrency: @@ -24,6 +26,18 @@ jobs: CARGO_INCREMENTAL: "0" steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.4.4' + - uses: denoland/setup-deno@v2 + with: + deno-version: 'v2.9.8' - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Prepare namespace prerequisites @@ -32,20 +46,48 @@ jobs: set -euo pipefail sudo rm -f /etc/apt/sources.list.d/microsoft-prod.* sudo apt-get update - sudo apt-get install -y bubblewrap build-essential pkg-config cmake + sudo apt-get install -y bubblewrap build-essential pkg-config cmake strace sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true - name: Build production helper and diagnostic - run: cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe + id: build + shell: bash + run: | + set -euo pipefail + cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-candidate" - name: Observe fixture-only host socket reachability shell: bash run: | set -euo pipefail target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate + - name: Verify candidate runtime compatibility + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/socket-runtime" + status=0 + for runtime in node python bun deno native; do + args=(--runtime "$runtime" --initial-exec --packaged-helper "$RUNNER_TEMP/socket-candidate" --report "$RUNNER_TEMP/socket-runtime/$runtime.json" --trace "$RUNNER_TEMP/socket-runtime/trace-$runtime") + if [[ "$runtime" == python ]]; then + args+=(--python "$(command -v python3)") + elif [[ "$runtime" == bun || "$runtime" == deno ]]; then + args+=(--executable "$(command -v "$runtime")") + fi + if ! node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/socket-runtime/$runtime.log" 2>&1; then + cat "$RUNNER_TEMP/socket-runtime/$runtime.log" + status=1 + fi + done + exit "$status" - name: Retain diagnostic observations if: always() uses: actions/upload-artifact@v4 with: name: linux-ipc-${{ matrix.os }} - path: ${{ runner.temp }}/linux-ipc.json + path: | + ${{ runner.temp }}/linux-ipc*.json + ${{ runner.temp }}/socket-runtime/ if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index 17ff2bdf..587bea87 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -59,19 +59,72 @@ mod probe { } } - pub fn main() { + fn pair_row(policy: LinuxPolicy<'_>, listener: &UnixListener, label: &str) -> Value { + let pair_args = [ + policy.target_args[0].clone(), + policy.target_args[1].clone(), + "pair".into(), + ]; + let pair_policy = LinuxPolicy { + target_args: &pair_args, + ..policy + }; + let plan = pair_policy.compose().unwrap(); + std::fs::write(pair_policy.profile, plan.landlock).unwrap(); + let observed: Value = serde_json::from_slice( + &run(Command::new("/usr/bin/bwrap") + .args(plan.arguments) + .env_clear()) + .stdout, + ) + .unwrap(); + assert_eq!( + observed["project_read"], pair_policy.fs_read, + "native pair read control: {observed}" + ); + assert_eq!( + observed["pair_roundtrip"], true, + "private pair communication: {observed}" + ); + for key in [ + "connected", + "pair_reconnect", + "pair_listen", + "datagram_pair", + "seqpacket_pair", + ] { + assert_eq!( + observed[key], false, + "private pair escape: {key}: {observed}" + ); + } + let delivered = receipt(listener, &observed); + assert!(!delivered, "pair reached host: {observed}"); + json!({"socket": label, "fs_read": pair_policy.fs_read, "fs_write": pair_policy.fs_write, + "shell": false, "mode": "pair", "host_received_fixture": delivered, "observed": observed}) + } + + fn options() -> (PathBuf, PathBuf, bool) { assert_eq!( std::env::consts::OS, "linux", "native evidence requires Linux" ); let args: Vec<_> = std::env::args_os().skip(1).collect(); - assert_eq!( - args.len(), - 2, - "expected explicit helper path and report path" + assert!( + matches!(args.len(), 2 | 3), + "expected helper, report and optional candidate flag" ); + let candidate = args.len() == 3; + if candidate { + assert_eq!(args[2], "--candidate"); + } let helper = std::fs::canonicalize(&args[0]).unwrap(); + (helper, PathBuf::from(&args[1]), candidate) + } + + pub fn main() { + let (helper, report_path, candidate) = options(); let temporary = tempfile::tempdir().unwrap(); let root = temporary.path().canonicalize().unwrap(); let project = root.join("project"); @@ -138,7 +191,7 @@ mod probe { data.to_str().unwrap().into(), ]; let private_paths = [MaskPath::Directory(&private)]; - let plan = LinuxPolicy { + let policy = LinuxPolicy { helper: &helper, target: &target, profile: &profile, @@ -154,9 +207,8 @@ mod probe { fs_write: write, shell: false, namespace_proc: false, - } - .compose() - .unwrap(); + }; + let plan = policy.compose().unwrap(); std::fs::write(&profile, plan.landlock).unwrap(); let observed: Value = serde_json::from_slice( &run(Command::new("/usr/bin/bwrap") @@ -167,11 +219,14 @@ mod probe { .unwrap(); assert_eq!(observed["project_read"], read, "native read control"); let delivered = receipt(&listeners[index], &observed); - if label == "private-mask" { - assert!(!delivered, "opaque private directory must hide its socket"); + if candidate || label == "private-mask" { + assert!(!delivered, "socket denial: {label}: {observed}"); } rows.push(json!({"socket": label, "fs_read": read, "fs_write": write, "shell": false, "host_received_fixture": delivered, "observed": observed})); + if candidate { + rows.push(pair_row(policy, &listeners[index], label)); + } } } let sources = [ @@ -188,6 +243,8 @@ mod probe { "crates/cyber-sandbox/Cargo.toml", "Cargo.lock", ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py", ]; let mut hash = Sha256::new(); for path in sources { @@ -197,11 +254,11 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), "rows": rows, "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance."}); std::fs::write( - &args[1], + report_path, format!("{}\n", serde_json::to_string_pretty(&report).unwrap()), ) .unwrap(); diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 3b20c7d4..0ff4fada 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -6,14 +6,69 @@ #include #include #include +#include #include +static int pair_probe(const struct sockaddr_un *address, socklen_t length, + const char *data_path) { + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) != 0) return 3; + char token[12]; + int roundtrip = send(pair[0], "fixture-only", 12, MSG_NOSIGNAL) == 12 && + recv(pair[1], token, sizeof(token), MSG_WAITALL) == 12 && + memcmp(token, "fixture-only", 12) == 0; + int connected = connect(pair[0], (const struct sockaddr *)address, length) == 0; + struct sockaddr unspecified = { .sa_family = AF_UNSPEC }; + int disconnected = connect(pair[0], &unspecified, sizeof(unspecified)) == 0; + int reconnected = connect(pair[0], (const struct sockaddr *)address, length) == 0; + int sent = (connected || reconnected) && send(pair[0], "fixture-only", 12, MSG_NOSIGNAL) == 12; + struct sockaddr_un local = *address; + size_t name_length = strlen(local.sun_path); + if (name_length + 5 >= sizeof(local.sun_path)) return 2; + memcpy(local.sun_path + name_length, "-pair", 6); + int bound = bind(pair[0], (struct sockaddr *)&local, + (socklen_t)(offsetof(struct sockaddr_un, sun_path) + name_length + 6)) == 0; + int listening = listen(pair[0], 1) == 0; + int sent_to = sendto(pair[0], "fixture-only", 12, MSG_NOSIGNAL, + (const struct sockaddr *)address, length) == 12; + struct iovec body = { .iov_base = (void *)"fixture-only", .iov_len = 12 }; + struct msghdr message = { .msg_name = (void *)address, .msg_namelen = length, + .msg_iov = &body, .msg_iovlen = 1 }; + int sent_message = sendmsg(pair[0], &message, MSG_NOSIGNAL) == 12; + close(pair[0]); close(pair[1]); + int dgram[2], seqpacket[2]; + int datagram = socketpair(AF_UNIX, SOCK_DGRAM, 0, dgram) == 0; + int sequence = socketpair(AF_UNIX, SOCK_SEQPACKET, 0, seqpacket) == 0; + if (datagram) { close(dgram[0]); close(dgram[1]); } + if (sequence) { close(seqpacket[0]); close(seqpacket[1]); } + int data = open(data_path, O_RDONLY | O_CLOEXEC); + char byte; + int readable = data >= 0 && read(data, &byte, 1) == 1; + if (data >= 0) close(data); + printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"project_read\":%s," + "\"pair_roundtrip\":%s,\"pair_disconnect\":%s,\"pair_reconnect\":%s," + "\"pair_bind\":%s,\"pair_listen\":%s,\"pair_sendto\":%s,\"pair_sendmsg\":%s," + "\"datagram_pair\":%s,\"seqpacket_pair\":%s}\n", + (connected || reconnected) ? "true" : "false", sent ? "true" : "false", + readable ? "true" : "false", roundtrip ? "true" : "false", + disconnected ? "true" : "false", reconnected ? "true" : "false", + bound ? "true" : "false", listening ? "true" : "false", + sent_to ? "true" : "false", sent_message ? "true" : "false", + datagram ? "true" : "false", sequence ? "true" : "false"); + return 0; +} + int main(int argc, char **argv) { - if (argc != 3) return 2; + if (argc != 3 && argc != 4) return 2; struct sockaddr_un address = { .sun_family = AF_UNIX }; size_t length = strlen(argv[1]); if (length == 0 || length >= sizeof(address.sun_path)) return 2; memcpy(address.sun_path, argv[1], length + 1); + if (argc == 4) { + if (strcmp(argv[3], "pair") != 0) return 2; + return pair_probe(&address, + (socklen_t)(offsetof(struct sockaddr_un, sun_path) + length + 1), argv[2]); + } int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd >= 0 && fcntl(fd, F_SETFD, FD_CLOEXEC) != 0) { close(fd); diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md new file mode 100644 index 00000000..cfcc699d --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md @@ -0,0 +1,56 @@ +# Linux socket-family candidate (experiment only) + +The [native counterexample](linux-ipc-proof.md) proves that private IP namespaces +and ABI-3 filesystem rules leave host pathname UNIX sockets reachable. This +candidate evaluates a compatible seccomp restriction before any production or +support/design decision. It does not change production native sources or the +helper's ABI floor. + +The workflow copies the current native entry and headers into an owned temporary +directory and adds a socket-family filter to that copy. The initial-exec listener +channel is created before the filter and closed before guest execution. A small +C wrapper invokes the copied entry with the same argument shape. This wrapper +is an experimental helper, not the admitted production Rust binary. + +The candidate permits `socket` only for INET/INET6, still within a mandatory +private network namespace. It permits `socketpair` only for UNIX stream pairs, +including close-on-exec/nonblocking flags. It denies ambient UNIX sockets, other +families (including host vsock/netlink endpoints), and datagram/seqpacket pairs. +All existing initial-exec, architecture and escape denials remain in place. + +The diagnostic tests all four read/write combinations with shell disabled. In +addition to denying the three exposed host socket routes and private socket, +it requires connected stream-pair token exchange to work, then attempts direct +connect, AF_UNSPEC disconnect/reconnect, bind/listen, addressed sendto/sendmsg, +and creation of datagram/seqpacket pairs. Host receipt remains the decisive +reachability observation. Project reads must still match their independent flag. +Node, Python, Bun, Deno and native runtime fixtures must separately retain their +existing nine-case behavior per Linux architecture with the candidate helper. + +These checks are configured, not yet native verified. This experiment does not +certify all socket APIs, child IPC, real plugin-runtime compatibility, host +descriptor/stdio admission, network allowlist proxy transport or secret handling. +The permitted stream pair must not be assumed incapable of ambient connection +until the native reconnect/addressed-message controls run. Production integration +requires the native proof and explicit support/design review gate: this candidate +restricts plugin-created UNIX endpoints, which may affect legitimate runtime or +plugin IPC. No weaker feature contract is silently selected. + +Local checks: example Clippy passes with Rust warnings denied and the repository's +cognitive-complexity ceiling of 15; option parsing and pair verification are +separate functions. The skill's analyzer does not support Rust, so this is the +actual Rust Clippy check. The portable C fixture passes warnings-denied compilation +and its direct stream-pair token exchange on macOS. YAML and four shell blocks +parse; the Python builder passes syntax plus an isolated-copy/injection/compiler +argument check without invoking a Linux compiler. Formatting/diff checks, all 107 +strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. Two deprecated +Seatbelt SDK warnings are observed without assigning their origin. Native Linux +candidate compilation, denial/positive tests and runtime execution remain pending. + +The code/security review covers the BPF branches and offsets, family/type/flag +checks and restored syscall-number loads, temporary source ownership, unchanged +production sources, bounded fixed-fixture IO, native pair attempts, workflow and +report bindings. It finds no further source issue within the experiment. The +test setup is partial: local portable tests execute; Linux kernel behavior cannot +run on this macOS host. Review is not production security acceptance, and the +explicit support/design gate remains open. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 648f3114..19998e3d 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -9,7 +9,7 @@ - [ ] 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 [Rust Linux composition](linux-policy-composition.md), which supplies bounded arguments/profile and actual-helper regressions passing on Linux x64/arm64 at `546641f9`; 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. [Linux IPC diagnostic](linux-ipc-proof.md) tests fixture-owned host socket reachability through the composed production boundary; it does not certify the network capability or close this task. +- [ ] 2.3 Enforce `fs.read`, `fs.write`, `shell`, network and secret intersections; refuse unsupported confinement with actionable plugin diagnostics. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) evaluates enforcement and runtime compatibility without production activation or a revised support decision. Neither closes this task. - [ ] 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. ## 3. Host contribution lifecycle diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py new file mode 100644 index 00000000..25e07233 --- /dev/null +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -0,0 +1,30 @@ +"""Build an isolated C experiment; never mutate production helper sources.""" +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +root = Path(__file__).resolve().parents[3] +output = Path(sys.argv[1]).resolve() +with tempfile.TemporaryDirectory(prefix="cyber-socket-candidate-") as temporary: + target = Path(temporary) + for name in ["linux_plugin_exec.c", "linux_filter.h", "linux_initial_exec.h"]: + shutil.copyfile(root / "crates/cyber-sandbox/native" / name, target / name) + shutil.copyfile(Path(__file__).with_name("linux_socket_candidate.h"), target / "socket_candidate.h") + header = target / "linux_filter.h" + text = header.read_text() + before = "#define CYBER_ESCAPE_DENIALS \\\n" + assert text.count(before) == 1 + header.write_text(text.replace(before, '#include "socket_candidate.h"\n' + before + " CYBER_SOCKET_FAMILIES, \\\n")) + wrapper = target / "main.c" + wrapper.write_text('''#include +int cyber_plugin_exec(int, const char *const []); +int main(int argc, char **argv) { + if (argc < 2 || strcmp(argv[1], "--plugin-native-exec") != 0) return 2; + return cyber_plugin_exec(argc - 1, (const char *const *)(argv + 1)); +} +''') + subprocess.run(["cc", "-Wall", "-Wextra", "-Werror", str(wrapper), + str(target / "linux_plugin_exec.c"), "-o", str(output)], + check=True, timeout=30) diff --git a/scripts/probes/plugin-native-entry/linux_socket_candidate.h b/scripts/probes/plugin-native-entry/linux_socket_candidate.h new file mode 100644 index 00000000..596b6c96 --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_socket_candidate.h @@ -0,0 +1,25 @@ +/* Experimental only: copied into a temporary helper build by the workflow. + * Keep private, connected stream socketpairs usable, with no ambient endpoints. + * INET/INET6 remain subject to the caller's private network namespace. + * This does not implement approved network/proxy support. */ +#ifndef CYBER_SOCKET_CANDIDATE_H +#define CYBER_SOCKET_CANDIDATE_H +#include + +#define CYBER_SOCKET_FAMILIES \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_socket, 0, 5), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 2, 0), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_socketpair, 0, 8), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_UNIX, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])), \ + BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~(SOCK_CLOEXEC | SOCK_NONBLOCK)), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SOCK_STREAM, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)) +#endif From 15e1ef85b1fbff552c980b50c7d22e4b35251608 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:27:44 -0400 Subject: [PATCH 26/50] test(plugins): reject pair rebinding in IPC candidate --- .../examples/plugin_unix_socket_probe.rs | 1 + .../linux-ipc-candidate.md | 20 +++++++++++++++++++ .../linux_socket_candidate.h | 2 ++ 3 files changed, 23 insertions(+) diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index 587bea87..ef1aae94 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -89,6 +89,7 @@ mod probe { for key in [ "connected", "pair_reconnect", + "pair_bind", "pair_listen", "datagram_pair", "seqpacket_pair", diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md index cfcc699d..a89889a0 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md @@ -17,6 +17,9 @@ private network namespace. It permits `socketpair` only for UNIX stream pairs, including close-on-exec/nonblocking flags. It denies ambient UNIX sockets, other families (including host vsock/netlink endpoints), and datagram/seqpacket pairs. All existing initial-exec, architecture and escape denials remain in place. +The revised experiment also denies `bind` and `listen` for every family. This +is an explicit candidate limitation: IP server sockets and explicit client +binding are unavailable. It is not a selected production network contract. The diagnostic tests all four read/write combinations with shell disabled. In addition to denying the three exposed host socket routes and private socket, @@ -54,3 +57,20 @@ report bindings. It finds no further source issue within the experiment. The test setup is partial: local portable tests execute; Linux kernel behavior cannot run on this macOS host. Review is not production security acceptance, and the explicit support/design gate remains open. + +## First native rejection + +At `ccde59d4`, both Linux jobs reject the candidate's pair assertion. The x64 +[job log](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40957529698/job/123741379932) +shows internal roundtrip succeeds, direct/disconnect/reconnect attempts fail, +datagram/seqpacket creation fails, but both `pair_bind` and `pair_listen` succeed +with project reads disabled. This refutes the assumption that a connected stream +pair cannot become an ambient listener. No host service or descriptor escalation +was attempted, and no failed candidate row is counted as isolation acceptance. + +The revised copy adds unconditional bind/listen denials and requires pair bind +to fail too. Both native denial and runtime suites must run again. This candidate +still needs explicit design/support review because those syscall denials also +restrict legitimate IP servers and explicit client binds. A narrower production +policy, stronger primitive or reviewed support revision remains necessary; green +fixture tests alone cannot select this restriction as the final feature contract. diff --git a/scripts/probes/plugin-native-entry/linux_socket_candidate.h b/scripts/probes/plugin-native-entry/linux_socket_candidate.h index 596b6c96..87aa791e 100644 --- a/scripts/probes/plugin-native-entry/linux_socket_candidate.h +++ b/scripts/probes/plugin-native-entry/linux_socket_candidate.h @@ -7,6 +7,8 @@ #include #define CYBER_SOCKET_FAMILIES \ + CYBER_DENY(SYS_bind), \ + CYBER_DENY(SYS_listen), \ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_socket, 0, 5), \ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), \ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 2, 0), \ From 3c2cf7e687e8cf2a3ebd6c01a156c5af35174151 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:31:20 -0400 Subject: [PATCH 27/50] docs: constrain goal delivery to one active PR --- docs/goal-delivery.md | 40 +++++++++++++++++++ .../complete-plugin-hook-runtime/tasks.md | 6 +++ 2 files changed, 46 insertions(+) create mode 100644 docs/goal-delivery.md diff --git a/docs/goal-delivery.md b/docs/goal-delivery.md new file mode 100644 index 00000000..df392078 --- /dev/null +++ b/docs/goal-delivery.md @@ -0,0 +1,40 @@ +# Active implementation goal and delivery rules + +The full approved scope from PRs #53/#54 remains active, in this order: + +1. [Plugins and hooks](../openspec/changes/complete-plugin-hook-runtime/tasks.md): + production native capability enforcement, complete host protocol operations, + scoped prompt callbacks and missing hook-event producers. +2. [Daily coding](../openspec/changes/complete-daily-coding-surfaces/tasks.md): + rewind/undo/redo, non-Git snapshots, path-scoped rules and context inspection, + root structured output and streaming JSONL input. +3. [Binary hex editor](../openspec/changes/add-binary-hex-editor/tasks.md): + the full reviewed CLI/TUI editor, owner/SDK/model operations and integrations. + +## Delivery constraint approved on 2026-10-29 + +The user requested fewer simultaneously open PRs. Keep normally **one open goal +PR**, with a **maximum of two** only when an independent change genuinely needs +separate review. This changes delivery granularity, not feature scope or acceptance. + +- Finish and verify a coherent production increment before opening the next PR. +- Keep related experiments, evidence, implementation fixes and CI fixes in the + active PR. Do not open a PR for each probe or supporting component. +- Consolidate the existing #70–#76 stack into draft #76 against `main`, retaining + the contained commits, source branches and evidence. Close #70–#75 as superseded + after verifying their heads are contained in the consolidated branch. +- Use the active PR description to distinguish production behavior, experimental + candidates, verified results and remaining requirements. Keep it draft while + its delivery increment is incomplete. +- Do not open the next implementation PR until the current increment is merged, + superseded or deliberately split for independent review within the cap. +- Preserve native proof and explicit support/design revision gates. Passing probes + are not production feature acceptance. Never weaken the agreed contract merely + to finish a PR, and never merge automatically. + +As of consolidation, native confinement foundations and regressions exist, but +production enforcement and the remaining plugin/hook scope are incomplete. The +Linux UNIX-socket diagnostic found a real host-reachability gap. Its experimental +filter and runtime checks are under investigation; no support revision or host +activation is accepted. Daily coding and binary editor implementation have not +started. The linked task lists remain authoritative for completion. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 19998e3d..b4942a26 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -1,5 +1,11 @@ ## 1. Review and baseline +Delivery rule updated by the user on 2026-10-29: normally one open goal PR, +maximum two only for independently reviewable work. Consolidate #70–#76 into +draft #76 against `main`; keep related probes, fixes and evidence in that PR. +The full goal scope, order and acceptance gates remain unchanged. See +[goal delivery rules](../../../docs/goal-delivery.md). + - [x] 1.1 Inspect canonical plugin/hook contracts, SDK protocol, host confinement and explicit unsupported paths at `097b6278`. - [x] 1.2 Draft the proposal, design, supplemental scenarios and ordered implementation tasks. - [x] 1.3 Review proposal/design/tasks with the user before security-sensitive or capability implementation. Record approved scope and platform/runtime decisions. Approved on 2026-10-27: after PRs #53/#54 merged, the user explicitly requested implementation of both merged plans and then instructed goal resume. Native proof, baseline and acceptance gates remain required. From 2f3b3c29b069ce71845de86ac32e93101a088432 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:43:23 -0400 Subject: [PATCH 28/50] test(plugins): evaluate path-scoped UNIX socket confinement --- .github/workflows/plugin-linux-ipc.yml | 36 +- .../examples/plugin_unix_socket_probe.rs | 98 ++- .../tests/fixtures/plugin_unix_socket.c | 66 +- .../linux-ipc-candidate.md | 34 +- .../linux-unix9-candidate.md | 65 ++ .../complete-plugin-hook-runtime/tasks.md | 2 +- scripts/probes/plugin-confinement/run.mts | 10 +- .../build_socket_candidate.py | 25 +- .../arm64/linux-ipc-candidate.json | 621 ++++++++++++++++++ .../arm64/linux-ipc.json | 253 +++++++ .../arm64/socket-runtime/bun.json | 485 ++++++++++++++ .../arm64/socket-runtime/deno.json | 436 ++++++++++++ .../arm64/socket-runtime/native.json | 472 +++++++++++++ .../arm64/socket-runtime/node.json | 436 ++++++++++++ .../arm64/socket-runtime/python.json | 427 ++++++++++++ .../x64/linux-ipc-candidate.json | 621 ++++++++++++++++++ .../x64/linux-ipc.json | 253 +++++++ .../x64/socket-runtime/bun.json | 485 ++++++++++++++ .../x64/socket-runtime/deno.json | 436 ++++++++++++ .../x64/socket-runtime/native.json | 472 +++++++++++++ .../x64/socket-runtime/node.json | 436 ++++++++++++ .../x64/socket-runtime/python.json | 427 ++++++++++++ .../linux_socket_landlock9.h | 20 + .../plugin-native-entry/linux_unix9_policy.h | 70 ++ 24 files changed, 6631 insertions(+), 55 deletions(-) create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/linux-ipc-candidate.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/linux-ipc.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/socket-runtime/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/socket-runtime/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/socket-runtime/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/socket-runtime/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/arm64/socket-runtime/python.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/linux-ipc-candidate.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/linux-ipc.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/socket-runtime/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/socket-runtime/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/socket-runtime/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/socket-runtime/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/x64/socket-runtime/python.json create mode 100644 scripts/probes/plugin-native-entry/linux_socket_landlock9.h create mode 100644 scripts/probes/plugin-native-entry/linux_unix9_policy.h diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 84878cd1..e0388af3 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -8,6 +8,8 @@ on: - .github/workflows/plugin-linux-ipc.yml - scripts/probes/plugin-native-entry/linux_socket_candidate.h - scripts/probes/plugin-native-entry/build_socket_candidate.py + - scripts/probes/plugin-native-entry/linux_socket_landlock9.h + - scripts/probes/plugin-native-entry/linux_unix9_policy.h permissions: contents: read concurrency: @@ -55,12 +57,31 @@ jobs: set -euo pipefail cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-candidate" + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-candidate" landlock9 + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-missing-resolve" landlock9-no-resolve - name: Observe fixture-only host socket reachability shell: bash run: | set -euo pipefail target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/unix9-candidate" "$RUNNER_TEMP/linux-ipc-unix9.json" --landlock9 + - name: Verify pathname policy counterexample control + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/unix9-missing-resolve" "$RUNNER_TEMP/linux-ipc-unix9-control.json" --landlock9 > "$RUNNER_TEMP/unix9-missing-resolve.log" 2>&1; then + cat "$RUNNER_TEMP/unix9-missing-resolve.log" + echo 'Missing pathname policy unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/unix9-missing-resolve.log" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'socket denial: outside:' in text, text + assert '"connected":true' in text and '"sent":true' in text, text + PY - name: Verify candidate runtime compatibility if: ${{ !cancelled() && steps.build.outcome == 'success' }} shell: bash @@ -68,18 +89,24 @@ jobs: set -euo pipefail mkdir -p "$RUNNER_TEMP/socket-runtime" status=0 - for runtime in node python bun deno native; do - args=(--runtime "$runtime" --initial-exec --packaged-helper "$RUNNER_TEMP/socket-candidate" --report "$RUNNER_TEMP/socket-runtime/$runtime.json" --trace "$RUNNER_TEMP/socket-runtime/trace-$runtime") + for policy in seccomp unix9; do + mkdir -p "$RUNNER_TEMP/socket-runtime/$policy" + helper="$RUNNER_TEMP/socket-candidate" + if [[ "$policy" == unix9 ]]; then helper="$RUNNER_TEMP/unix9-candidate"; fi + for runtime in node python bun deno native; do + args=(--runtime "$runtime" --initial-exec --packaged-helper "$helper" --report "$RUNNER_TEMP/socket-runtime/$policy/$runtime.json" --trace "$RUNNER_TEMP/socket-runtime/$policy/trace-$runtime") + if [[ "$policy" == unix9 ]]; then args+=(--unix-scratch-policy); fi if [[ "$runtime" == python ]]; then args+=(--python "$(command -v python3)") elif [[ "$runtime" == bun || "$runtime" == deno ]]; then args+=(--executable "$(command -v "$runtime")") fi - if ! node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/socket-runtime/$runtime.log" 2>&1; then - cat "$RUNNER_TEMP/socket-runtime/$runtime.log" + if ! node --experimental-strip-types scripts/probes/plugin-confinement/run.mts "${args[@]}" > "$RUNNER_TEMP/socket-runtime/$policy/$runtime.log" 2>&1; then + cat "$RUNNER_TEMP/socket-runtime/$policy/$runtime.log" status=1 fi done + done exit "$status" - name: Retain diagnostic observations if: always() @@ -89,5 +116,6 @@ jobs: path: | ${{ runner.temp }}/linux-ipc*.json ${{ runner.temp }}/socket-runtime/ + ${{ runner.temp }}/unix9-missing-resolve.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index ef1aae94..4169aba6 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -59,25 +59,48 @@ mod probe { } } - fn pair_row(policy: LinuxPolicy<'_>, listener: &UnixListener, label: &str) -> Value { - let pair_args = [ + #[derive(Clone, Copy, PartialEq)] + enum Mode { + Baseline, + Seccomp, + Unix9, + } + + fn observe(policy: &LinuxPolicy<'_>, mode: Mode) -> Value { + let mut plan = policy.compose().unwrap(); + if mode == Mode::Unix9 { + plan.landlock + .push_str(&format!("U\t{}\n", policy.scratch.display())); + } + std::fs::write(policy.profile, plan.landlock).unwrap(); + serde_json::from_slice( + &run(Command::new("/usr/bin/bwrap") + .args(plan.arguments) + .env_clear()) + .stdout, + ) + .unwrap() + } + + fn pair_row( + policy: LinuxPolicy<'_>, + listener: &UnixListener, + label: &str, + mode: Mode, + ) -> Value { + let mut pair_args = vec![ policy.target_args[0].clone(), policy.target_args[1].clone(), "pair".into(), ]; + if mode == Mode::Unix9 { + pair_args.push(policy.scratch.join("local.sock").to_str().unwrap().into()); + } let pair_policy = LinuxPolicy { target_args: &pair_args, ..policy }; - let plan = pair_policy.compose().unwrap(); - std::fs::write(pair_policy.profile, plan.landlock).unwrap(); - let observed: Value = serde_json::from_slice( - &run(Command::new("/usr/bin/bwrap") - .args(plan.arguments) - .env_clear()) - .stdout, - ) - .unwrap(); + let observed = observe(&pair_policy, mode); assert_eq!( observed["project_read"], pair_policy.fs_read, "native pair read control: {observed}" @@ -86,26 +109,34 @@ mod probe { observed["pair_roundtrip"], true, "private pair communication: {observed}" ); + for key in ["connected", "pair_reconnect", "pair_bind"] { + assert_eq!( + observed[key], false, + "private pair escape: {key}: {observed}" + ); + } for key in [ - "connected", - "pair_reconnect", - "pair_bind", - "pair_listen", "datagram_pair", "seqpacket_pair", + "tcp_roundtrip", + "scratch_unix_roundtrip", ] { assert_eq!( - observed[key], false, - "private pair escape: {key}: {observed}" + observed[key], + mode == Mode::Unix9, + "pair/local support: {key}: {observed}" ); } + if mode == Mode::Seccomp { + assert_eq!(observed["pair_listen"], false, "pair listener: {observed}"); + } let delivered = receipt(listener, &observed); assert!(!delivered, "pair reached host: {observed}"); json!({"socket": label, "fs_read": pair_policy.fs_read, "fs_write": pair_policy.fs_write, "shell": false, "mode": "pair", "host_received_fixture": delivered, "observed": observed}) } - fn options() -> (PathBuf, PathBuf, bool) { + fn options() -> (PathBuf, PathBuf, Mode) { assert_eq!( std::env::consts::OS, "linux", @@ -116,16 +147,19 @@ mod probe { matches!(args.len(), 2 | 3), "expected helper, report and optional candidate flag" ); - let candidate = args.len() == 3; - if candidate { - assert_eq!(args[2], "--candidate"); - } + let mode = match args.get(2).and_then(|arg| arg.to_str()) { + None => Mode::Baseline, + Some("--candidate") => Mode::Seccomp, + Some("--landlock9") => Mode::Unix9, + _ => panic!("unknown candidate mode"), + }; let helper = std::fs::canonicalize(&args[0]).unwrap(); - (helper, PathBuf::from(&args[1]), candidate) + (helper, PathBuf::from(&args[1]), mode) } pub fn main() { - let (helper, report_path, candidate) = options(); + let (helper, report_path, mode) = options(); + let candidate = mode != Mode::Baseline; let temporary = tempfile::tempdir().unwrap(); let root = temporary.path().canonicalize().unwrap(); let project = root.join("project"); @@ -209,15 +243,7 @@ mod probe { shell: false, namespace_proc: false, }; - let plan = policy.compose().unwrap(); - std::fs::write(&profile, plan.landlock).unwrap(); - let observed: Value = serde_json::from_slice( - &run(Command::new("/usr/bin/bwrap") - .args(plan.arguments) - .env_clear()) - .stdout, - ) - .unwrap(); + let observed = observe(&policy, mode); assert_eq!(observed["project_read"], read, "native read control"); let delivered = receipt(&listeners[index], &observed); if candidate || label == "private-mask" { @@ -226,7 +252,7 @@ mod probe { rows.push(json!({"socket": label, "fs_read": read, "fs_write": write, "shell": false, "host_received_fixture": delivered, "observed": observed})); if candidate { - rows.push(pair_row(policy, &listeners[index], label)); + rows.push(pair_row(policy, &listeners[index], label, mode)); } } } @@ -245,6 +271,8 @@ mod probe { "Cargo.lock", ".github/workflows/plugin-linux-ipc.yml", "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", "scripts/probes/plugin-native-entry/build_socket_candidate.py", ]; let mut hash = Sha256::new(); @@ -255,7 +283,7 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), "rows": rows, "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance."}); std::fs::write( diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 0ff4fada..075e44dd 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -8,9 +8,55 @@ #include #include #include +#include +#ifdef __linux__ +#include +#include +#endif + +static int abi(void) { +#ifdef __linux__ + return (int)syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); +#else + return 0; +#endif +} + +static int local_exchange(int family, const char *path) { + int server = -1, client = -1, peer = -1, ok = 0; + struct sockaddr_storage storage = {0}; + socklen_t length; + if (family == AF_UNIX) { + struct sockaddr_un *address = (struct sockaddr_un *)&storage; + if (strlen(path) >= sizeof(address->sun_path)) return 0; + address->sun_family = AF_UNIX; + memcpy(address->sun_path, path, strlen(path) + 1); + length = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + strlen(path) + 1); + } else { + struct sockaddr_in *address = (struct sockaddr_in *)&storage; + address->sin_family = AF_INET; + address->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + length = sizeof(*address); + } + server = socket(family, SOCK_STREAM, 0); + if (server < 0 || bind(server, (struct sockaddr *)&storage, length) != 0 || + listen(server, 1) != 0 || getsockname(server, (struct sockaddr *)&storage, &length) != 0) goto done; + client = socket(family, SOCK_STREAM, 0); + if (client < 0 || connect(client, (struct sockaddr *)&storage, length) != 0) goto done; + peer = accept(server, NULL, NULL); + char token[12]; + ok = peer >= 0 && send(client, "fixture-only", 12, MSG_NOSIGNAL) == 12 && + recv(peer, token, sizeof(token), MSG_WAITALL) == 12 && memcmp(token, "fixture-only", 12) == 0; +done: + if (peer >= 0) close(peer); + if (client >= 0) close(client); + if (server >= 0) close(server); + if (family == AF_UNIX) unlink(path); + return ok; +} static int pair_probe(const struct sockaddr_un *address, socklen_t length, - const char *data_path) { + const char *data_path, const char *scratch_socket) { int pair[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) != 0) return 3; char token[12]; @@ -45,29 +91,33 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, char byte; int readable = data >= 0 && read(data, &byte, 1) == 1; if (data >= 0) close(data); + int tcp = scratch_socket != NULL && local_exchange(AF_INET, NULL); + int scratch_ipc = scratch_socket != NULL && local_exchange(AF_UNIX, scratch_socket); printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"project_read\":%s," "\"pair_roundtrip\":%s,\"pair_disconnect\":%s,\"pair_reconnect\":%s," "\"pair_bind\":%s,\"pair_listen\":%s,\"pair_sendto\":%s,\"pair_sendmsg\":%s," - "\"datagram_pair\":%s,\"seqpacket_pair\":%s}\n", + "\"datagram_pair\":%s,\"seqpacket_pair\":%s,\"landlock_abi\":%d," + "\"tcp_roundtrip\":%s,\"scratch_unix_roundtrip\":%s}\n", (connected || reconnected) ? "true" : "false", sent ? "true" : "false", readable ? "true" : "false", roundtrip ? "true" : "false", disconnected ? "true" : "false", reconnected ? "true" : "false", bound ? "true" : "false", listening ? "true" : "false", sent_to ? "true" : "false", sent_message ? "true" : "false", - datagram ? "true" : "false", sequence ? "true" : "false"); + datagram ? "true" : "false", sequence ? "true" : "false", abi(), + tcp ? "true" : "false", scratch_ipc ? "true" : "false"); return 0; } int main(int argc, char **argv) { - if (argc != 3 && argc != 4) return 2; + if (argc != 3 && argc != 4 && argc != 5) return 2; struct sockaddr_un address = { .sun_family = AF_UNIX }; size_t length = strlen(argv[1]); if (length == 0 || length >= sizeof(address.sun_path)) return 2; memcpy(address.sun_path, argv[1], length + 1); - if (argc == 4) { + if (argc >= 4) { if (strcmp(argv[3], "pair") != 0) return 2; return pair_probe(&address, - (socklen_t)(offsetof(struct sockaddr_un, sun_path) + length + 1), argv[2]); + (socklen_t)(offsetof(struct sockaddr_un, sun_path) + length + 1), argv[2], argc == 5 ? argv[4] : NULL); } int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd >= 0 && fcntl(fd, F_SETFD, FD_CLOEXEC) != 0) { @@ -83,8 +133,8 @@ int main(int argc, char **argv) { char byte; int readable = data >= 0 && read(data, &byte, 1) == 1; if (data >= 0) close(data); - printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"errno\":%d,\"project_read\":%s}\n", + printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"errno\":%d,\"project_read\":%s,\"landlock_abi\":%d}\n", connected ? "true" : "false", sent ? "true" : "false", error, - readable ? "true" : "false"); + readable ? "true" : "false", abi()); return 0; } diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md index a89889a0..0c165dd3 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md @@ -65,8 +65,9 @@ At `ccde59d4`, both Linux jobs reject the candidate's pair assertion. The x64 shows internal roundtrip succeeds, direct/disconnect/reconnect attempts fail, datagram/seqpacket creation fails, but both `pair_bind` and `pair_listen` succeed with project reads disabled. This refutes the assumption that a connected stream -pair cannot become an ambient listener. No host service or descriptor escalation -was attempted, and no failed candidate row is counted as isolation acceptance. +pair must reject bind/listen. Host reachability of that newly listening pair was +not tested; no actual incoming host connection or descriptor escalation is +claimed. No failed candidate row is counted as isolation acceptance. The revised copy adds unconditional bind/listen denials and requires pair bind to fail too. Both native denial and runtime suites must run again. This candidate @@ -74,3 +75,32 @@ still needs explicit design/support review because those syscall denials also restrict legitimate IP servers and explicit client binds. A narrower production policy, stronger primitive or reviewed support revision remains necessary; green fixture tests alone cannot select this restriction as the final feature contract. + +## Revised native component results + +At `15e1ef85b1fbff552c980b50c7d22e4b35251608`, both +[x64 job 123743126666](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40958015885/job/123743126666) +and [arm64 job 123743126191](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40958015885/job/123743126191) +pass. Per architecture, all 16 ordinary host-socket attempts deny, all 16 stream +pair cases retain internal token exchange while rejecting bind/listen/reconnect, +and project reads independently match their capability. The unmodified production +helper still delivers the 12 public/outside/symlink tokens per architecture, +preserving the counterexample rather than concealing it behind candidate results. + +The five runtime drivers each pass nine cases per architecture (90 cases total), +with no unsupported cases. Every runtime's input and executed helper SHA-256 +matches its architecture's candidate helper hash. This proves the fixture matrix, +not all runtime applications, IP server support or production network acceptance. + +[Retained original reports](../../../scripts/probes/plugin-native-entry/evidence/linux-ipc-candidate-15e1ef85/) +refer to merge checkout `a4ce695bcb8d591bb2b4f39fe55df1c9e6ede048`, whose parents +are `18791fbe0089c95708956da0f3c4a47e3035c31d` and +`15e1ef85b1fbff552c980b50c7d22e4b35251608`. Independent recomputation of each +manifest path, NUL and exact checkout bytes matches diagnostic source hash +`38213834f9fbd04afb435af7f5b173ed00ba3ab042fede482c48c30c7bd61f09` and each +runtime source report (runtime paths are relative to `plugin-confinement`). The +job-reported candidate helper hashes are +`7792832e9f12b58942eba0b0e2797b1089beb38dd03fed4b9e7bb6edfbbf58c1` (x64) +and `909a09be86ab2173c489e75a0a1b3bd4b75a0902ea511d4598518490535f3766` +(arm64). Binaries were not retained for independent rehashing. The support/design +gate and the explicit bind/listen limitation remain open despite these passes. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md new file mode 100644 index 00000000..76c76412 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md @@ -0,0 +1,65 @@ +# Path-scoped UNIX socket alternative (experiment only) + +The ABI-3 seccomp candidate's unconditional bind/listen denials are not selected +as the production contract. This alternative tests a stronger kernel primitive +to preserve real private networking and local IPC. It is built only from temporary +copies of the native entry; production sources and ABI requirements are unchanged. + +The copied helper installs an additional Landlock layer, then its existing +filesystem policy, both before any runtime instruction. This layer requires ABI 9 and +handles pathname UNIX resolution and socket-node creation. It grants those actions +only beneath explicit owned scratch, never Location/bootstrap roots merely because +they have read/write grants. Abstract UNIX connections are scoped to the new +domain. The layer also handles REFER and grants REFER at root so it does not +silently add an unconditional cross-directory rename prohibition to the existing +filesystem policy. No other filesystem rights are granted at root by this layer. + +The additional layer reads one bounded `U` scratch entry from the owned experimental +profile. This syntax is only in the copied C helper/probe, not the production +composer or SDK. A socket-family filter permits UNIX, INET and INET6 and UNIX +pairs, while denying unrelated ambient families. It leaves bind/listen available. +The existing namespace, descriptor and initial-exec mechanisms are retained. + +The diagnostic must deny the host pathname socket routes under every independent +read/write choice, while retaining stream/datagram/seqpacket pairs, real TCP +loopback bind/listen/connect/accept/token exchange and private scratch UNIX +bind/listen/connect/accept/token exchange. Pair reconnect and binding outside +scratch remain denial assertions. A successful listen on an unnamed pair alone +is not counted as host reachability. The existing five-runtime/nine-case matrix +also runs with an explicit experimental scratch policy flag. +A separate copied-helper control removes pathname resolution from the handled +rights and must fail the exact outside-socket assertion after an actual host token +receipt. Startup/compilation failure cannot satisfy that control. + +These checks are configured, not native verified. ABI 9 changes the potential +Linux support floor; this is not approved or integrated. If the alternative passes, +present its exact native evidence and the support tradeoff for explicit review. +Do not silently fall back to fewer capabilities, weaker IPC protection or global +bind/listen denial. Older kernels, network proxy transport, complete secret/stdio +intersections, owner admission, actual plugins and production acceptance remain +open. This experiment does not prove incoming host access to scratch impossible. + +The ABI constants and struct layout follow the kernel's +[UAPI header](https://github.com/torvalds/linux/blob/master/include/uapi/linux/landlock.h) +and [Landlock documentation](https://docs.kernel.org/userspace-api/landlock.html). +Native results, once available, supersede assumptions about what these primitives +enforce. No parent capability or support checkbox is closed. + +Local verification: example Clippy with Rust warnings denied passes; the native +C fixture compiles with `-Wall -Wextra -Werror` and its stream-pair, TCP and UNIX +token exchange controls pass on macOS. Python syntax plus all three temporary +source-copy/injection/compiler-argument variants pass without claiming Linux C +compilation. TypeScript syntax, workflow YAML/five shell blocks, Rust formatting, +diff checks, 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) +pass. The C build reports two deprecated Seatbelt SDK warnings; their origin is +not assigned. Native Linux execution remains required. + +The scoped code/security review checked all changed probe, copied-helper, +workflow and documentation inputs. It caught the need to install the UNIX layer +before the existing filesystem layer, since the latter may already deny reopening +the profile. Both now install after a single-threaded check and before runtime +instructions. Profile IO is bounded and no-follow; the scratch entry is explicit +and unique. Root receives REFER only. Family filters restore the syscall-number +register before later checks, and positive internal IPC/IP controls are required +alongside exact missing-policy failure. These checks are experiment review, not +production native admission or acceptance of a new support floor. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index b4942a26..7aae83ec 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -15,7 +15,7 @@ The full goal scope, order and acceptance gates remain unchanged. See - [ ] 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 [Rust Linux composition](linux-policy-composition.md), which supplies bounded arguments/profile and actual-helper regressions passing on Linux x64/arm64 at `546641f9`; 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. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) evaluates enforcement and runtime compatibility without production activation or a revised support decision. Neither closes this task. +- [ ] 2.3 Enforce `fs.read`, `fs.write`, `shell`, network and secret intersections; refuse unsupported confinement with actionable plugin diagnostics. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) passes its fixture matrix but restricts bind/listen, while the [path-scoped ABI-9 alternative](linux-unix9-candidate.md) tests preserving private IP/UNIX functionality without production activation or an approved support-floor change. None closes this task. - [ ] 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. ## 3. Host contribution lifecycle diff --git a/scripts/probes/plugin-confinement/run.mts b/scripts/probes/plugin-confinement/run.mts index b0a24edb..ae0ce9a8 100644 --- a/scripts/probes/plugin-confinement/run.mts +++ b/scripts/probes/plugin-confinement/run.mts @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; type Runtime = { kind: "python" | "node" | "bun" | "deno" | "native"; executable: string; prefix: string; include: string; libdir: string; library?: string; version: string }; -type Artifacts = { executable: string; addon?: string; landlock?: string; initialExec: boolean; stagedPlatform?: boolean; signingIdentifier?: string; trace?: string; packaged?: boolean }; +type Artifacts = { executable: string; addon?: string; landlock?: string; initialExec: boolean; stagedPlatform?: boolean; signingIdentifier?: string; trace?: string; packaged?: boolean; unixScratch?: boolean }; type Flags = { read: boolean; write: boolean; shell: boolean; unrestricted?: boolean }; type Observation = Record; const source = dirname(fileURLToPath(import.meta.url)); @@ -180,7 +180,7 @@ function fixture(root: string): Record { function linuxLaunch(artifacts: Artifacts, runtime: Runtime, paths: Record, profile: string, flags: Flags, args: string[]) { const grants: string[] = []; - const grant = (right: "R" | "W", path: string) => { + const grant = (right: "R" | "W" | "U", path: string) => { assert(path.startsWith("/") && path !== "/" && !/[\t\n\r\0]/.test(path), "bounded absolute native proof root required"); if (existsSync(path)) grants.push(`${right}\t${path}`); }; @@ -199,6 +199,7 @@ function linuxLaunch(artifacts: Artifacts, runtime: Runtime, paths: Record + +#define CYBER_SOCKET_FAMILIES \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_socket, 0, 6), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_UNIX, 3, 0), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 2, 0), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_socketpair, 0, 4), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])), \ + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_UNIX, 1, 0), \ + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), \ + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)) +#endif diff --git a/scripts/probes/plugin-native-entry/linux_unix9_policy.h b/scripts/probes/plugin-native-entry/linux_unix9_policy.h new file mode 100644 index 00000000..49215628 --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_unix9_policy.h @@ -0,0 +1,70 @@ +/* Isolated native experiment only; not compiled into the production helper. */ +#ifndef CYBER_UNIX9_POLICY_H +#define CYBER_UNIX9_POLICY_H +/* ABI-9 UAPI value, even when building against an older distro's headers. */ +#define CYBER_RESOLVE_UNIX (1ULL << 16) +#define CYBER_ABSTRACT_UNIX 1ULL + +static int unix9_add(int ruleset, const char *path, uint64_t rights) { + int fd = open(path, O_PATH | O_CLOEXEC | O_NOFOLLOW); + struct stat info; + if (fd < 0 || fstat(fd, &info) != 0 || !S_ISDIR(info.st_mode)) { + if (fd >= 0) close(fd); + return -1; + } + struct landlock_path_beneath_attr rule = { .parent_fd = fd, .allowed_access = rights }; + int result = syscall(SYS_landlock_add_rule, ruleset, LANDLOCK_RULE_PATH_BENEATH, &rule, 0); + close(fd); + return result; +} + +static int confine_unix9(const char *policy) { + int abi = syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 9) { + fprintf(stderr, "experimental pathname UNIX policy requires Landlock ABI >= 9 (observed %d)\n", abi); + return -1; + } + int fd = open(policy, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); + struct stat info; + if (fd < 0 || fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || + info.st_nlink != 1 || info.st_size <= 0 || info.st_size > 65536) { + if (fd >= 0) close(fd); + return -1; + } + size_t size = (size_t)info.st_size, offset = 0; + char *text = calloc(size + 1, 1); + if (text == NULL) { close(fd); return -1; } + while (offset < size) { + ssize_t count = read(fd, text + offset, size - offset); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) { free(text); close(fd); return -1; } + offset += (size_t)count; + } + close(fd); + int result = -1, ruleset = -1; + char *scratch = NULL, *state = NULL; + if (strlen(text) != size) goto done; + for (char *line = strtok_r(text, "\n", &state); line != NULL; line = strtok_r(NULL, "\n", &state)) { + if (line[0] != 'U') continue; + if (scratch != NULL || line[1] != '\t' || line[2] != '/' || + strcmp(line + 2, "/") == 0 || strlen(line + 2) > 4096 || + strchr(line + 2, '\t') != NULL) goto done; + scratch = line + 2; + } + if (scratch == NULL) goto done; + struct { uint64_t fs, net, scoped; } attributes = { + .fs = CYBER_RESOLVE_UNIX | LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_REFER, + .scoped = CYBER_ABSTRACT_UNIX, + }; + ruleset = syscall(SYS_landlock_create_ruleset, &attributes, sizeof(attributes), 0); + if (ruleset < 0 || + unix9_add(ruleset, "/", LANDLOCK_ACCESS_FS_REFER) != 0 || + unix9_add(ruleset, scratch, attributes.fs) != 0) goto done; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) goto done; + result = syscall(SYS_landlock_restrict_self, ruleset, 0); +done: + if (ruleset >= 0) close(ruleset); + free(text); + return result; +} +#endif From 0855777644e3093704040f954268adc250c5d118 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 17:50:06 -0400 Subject: [PATCH 29/50] test(plugins): record unavailable UNIX policy kernel support --- .github/workflows/plugin-linux-ipc.yml | 53 +++++++++++++++++++ .../linux-unix9-candidate.md | 19 +++++++ 2 files changed, 72 insertions(+) diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index e0388af3..cc0d622d 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -65,8 +65,56 @@ jobs: set -euo pipefail target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate + - name: Record pathname policy kernel availability + id: abi + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + import json, os + source = json.loads((Path(os.environ['RUNNER_TEMP']) / 'linux-ipc.json').read_text()) + observed = {row['observed']['landlock_abi'] for row in source['rows']} + assert len(observed) == 1 + abi = observed.pop() + assert isinstance(abi, int) and abi >= 3 + supported = abi >= 9 + report = {'required_abi': 9, 'observed_abi': abi, 'available': supported, + 'acceptance': 'pending native checks' if supported else 'unverified; kernel unsupported', + 'checkout': source['checkout'], 'run_id': source['run_id'], + 'source_sha256': source['source_sha256'], + 'scope': 'Kernel availability only; refusal on an older kernel is not confinement acceptance.'} + (Path(os.environ['RUNNER_TEMP']) / 'linux-ipc-unix9-availability.json').write_text(json.dumps(report, indent=2) + '\n') + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + output.write('supported=' + str(supported).lower() + '\n') + print(json.dumps(report)) + PY + - name: Verify pathname policy on a capable kernel + if: steps.abi.outputs.supported == 'true' + shell: bash + run: | + set -euo pipefail target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/unix9-candidate" "$RUNNER_TEMP/linux-ipc-unix9.json" --landlock9 + - name: Verify explicit refusal on an older kernel + if: steps.abi.outputs.supported == 'false' + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/unix9-candidate" "$RUNNER_TEMP/linux-ipc-unix9.json" --landlock9 > "$RUNNER_TEMP/unix9-unavailable.log" 2>&1; then + echo 'Unsupported kernel unexpectedly admitted the ABI-9 candidate' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/unix9-unavailable.log" "$RUNNER_TEMP/linux-ipc-unix9-availability.json" <<'PY' + from pathlib import Path + import json, sys + text = Path(sys.argv[1]).read_text() + abi = json.loads(Path(sys.argv[2]).read_text())['observed_abi'] + assert f'requires Landlock ABI >= 9 (observed {abi})' in text, text + assert 'fixture bootstrap failed' in text, text + print('UNVERIFIED: ABI-9 confinement and runtime acceptance require a capable kernel.') + PY - name: Verify pathname policy counterexample control + if: steps.abi.outputs.supported == 'true' shell: bash run: | set -euo pipefail @@ -90,6 +138,10 @@ jobs: mkdir -p "$RUNNER_TEMP/socket-runtime" status=0 for policy in seccomp unix9; do + if [[ "$policy" == unix9 && '${{ steps.abi.outputs.supported }}' != true ]]; then + echo 'UNVERIFIED: skipping ABI-9 runtime matrix on this unsupported kernel.' + continue + fi mkdir -p "$RUNNER_TEMP/socket-runtime/$policy" helper="$RUNNER_TEMP/socket-candidate" if [[ "$policy" == unix9 ]]; then helper="$RUNNER_TEMP/unix9-candidate"; fi @@ -117,5 +169,6 @@ jobs: ${{ runner.temp }}/linux-ipc*.json ${{ runner.temp }}/socket-runtime/ ${{ runner.temp }}/unix9-missing-resolve.log + ${{ runner.temp }}/unix9-unavailable.log if-no-files-found: warn retention-days: 14 diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md index 76c76412..2873a18f 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-unix9-candidate.md @@ -63,3 +63,22 @@ and unique. Root receives REFER only. Family filters restore the syscall-number register before later checks, and positive internal IPC/IP controls are required alongside exact missing-policy failure. These checks are experiment review, not production native admission or acceptance of a new support floor. + +## Hosted kernel limit at `2f3b3c29` + +Both [x64 job 123750657048](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40960197456/job/123750657048) +and [arm64 job 123750656386](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40960197456/job/123750656386) +compile the candidates but report Landlock ABI 8. The ABI-9 helper refuses before +target output, with `requires Landlock ABI >= 9 (observed 8)`. Consequently none +of its confinement/local-IP/local-UNIX/runtime acceptance passes on these hosts; +the missing-resolution control does not run. This is a kernel availability result, +not a proof that the alternative works or that a newer kernel is sufficient. + +The workflow now records the actual ABI in a source-bound availability JSON. On +older hosts it verifies the exact refusal, explicitly records ABI-9 acceptance as +unverified, and skips its kernel-specific control/runtime matrix. The separate +ABI-3 socket-family experiment still runs normally. On a capable kernel, all +ABI-9 positives, denials, exact missing-policy control and runtime checks remain +mandatory. There is no best-effort downgrade of the policy and no production +support change. These skips do not close any acceptance gate; a compatible +mechanism or authoritative execution on a capable host remains required. From 19cf223caf654586e2ec5f87b52ede543009f0f1 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 18:01:27 -0400 Subject: [PATCH 30/50] fix(plugins): exclude project paths from runtime lookup --- .../plugin-bootstrap-environment.yml | 37 ++++ crates/cyber-tools/src/plugin_host/launch.rs | 40 ++--- crates/cyber-tools/src/plugin_host/mod.rs | 1 + .../src/plugin_host/runtime_search.rs | 61 +++++++ .../src/plugin_host/runtime_search/tests.rs | 166 ++++++++++++++++++ .../src/plugin_host/startup_tests.rs | 2 +- .../runtime-search.md | 58 ++++++ .../specs/plugins-marketplace/spec.md | 5 + .../complete-plugin-hook-runtime/tasks.md | 5 + sdk/plugin/README.md | 5 + 10 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 crates/cyber-tools/src/plugin_host/runtime_search.rs create mode 100644 crates/cyber-tools/src/plugin_host/runtime_search/tests.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/runtime-search.md diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index 8d8a002a..99b1e107 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -64,6 +64,42 @@ jobs: - name: Verify explicit environment construction shell: bash run: cargo test --locked -p cyber-tools --lib plugin_host::environment + - name: Verify runtime search exclusions + shell: bash + run: cargo test --locked -p cyber-tools --lib plugin_host::runtime_search + - name: Verify missing runtime search guards are detected + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-tools/src/plugin_host/runtime_search.rs + cp "$source" "$RUNNER_TEMP/runtime-search-positive.rs" + trap 'cp "$RUNNER_TEMP/runtime-search-positive.rs" "$source"' EXIT + python3 - "$source" <<'PY' + from pathlib import Path + import sys + path = Path(sys.argv[1]) + text = path.read_text() + for before in ['excluded.iter().any(|root| directory.starts_with(root))', + 'excluded.iter().any(|root| candidate.starts_with(root))']: + assert text.count(before) == 1 + text = text.replace(before, 'false') + path.write_text(text) + PY + name=project_and_package_path_entries_cannot_shadow_the_owner_runtime + if cargo test --locked -p cyber-tools --lib "plugin_host::runtime_search::tests::$name" -- --exact > "$RUNNER_TEMP/runtime-search-control.log" 2>&1; then + echo 'Missing runtime search guards unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/runtime-search-control.log" "$name" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert f'{sys.argv[2]} ... FAILED' in text, text + assert 'runtime selection receipt' in text, text + PY + cp "$RUNNER_TEMP/runtime-search-positive.rs" "$source" + cargo test --locked -p cyber-tools --lib plugin_host::runtime_search - name: Verify bundled runtime startup receipts shell: bash run: | @@ -192,5 +228,6 @@ jobs: ${{ runner.temp }}/runtime-bun-control.log ${{ runner.temp }}/runtime-deno-control.log ${{ runner.temp }}/workspace-boundary-control.log + ${{ runner.temp }}/runtime-search-control.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-tools/src/plugin_host/launch.rs b/crates/cyber-tools/src/plugin_host/launch.rs index 099b36ef..8eeaef6c 100644 --- a/crates/cyber-tools/src/plugin_host/launch.rs +++ b/crates/cyber-tools/src/plugin_host/launch.rs @@ -53,7 +53,7 @@ impl LaunchContext<'_> { plugin: &PluginLaunch<'_>, denials: &Denials, ) -> Result { - let (program, args) = command(plugin)?; + let (program, args) = command(plugin, self.location)?; let mut config = SandboxConfig::resolve( &self.resolved.value, &self.resolved.sources, @@ -178,7 +178,7 @@ fn confinement( } /// The program and arguments that run `main` with its runtime. -fn command(plugin: &PluginLaunch<'_>) -> Result<(String, Vec), String> { +fn command(plugin: &PluginLaunch<'_>, location: &Path) -> Result<(String, Vec), String> { let relative = cyber_core::plugins::manifest_path(&plugin.host.main) .ok_or_else(|| format!("plugin {}: main is outside the plugin", plugin.name))?; let main = plugin.root.join(relative); @@ -197,12 +197,10 @@ fn command(plugin: &PluginLaunch<'_>) -> Result<(String, Vec), String> { Runtime::Deno => (&["deno"], deno_arguments(plugin, main_arg)?), Runtime::Python => (&["python3", "python"], vec!["-u".into(), main_arg]), }; - let program = candidates - .iter() - .find_map(|name| find_program(name)) + let program = super::runtime_search::find(candidates, location, plugin.root) .ok_or_else(|| { format!( - "plugin {}: runtime {} is not installed", + "plugin {}: runtime {} needs an executable on an absolute PATH outside the Location and package", plugin.name, plugin.host.runtime.as_str() ) @@ -266,18 +264,6 @@ fn runtime_config(plugin: &PluginLaunch<'_>, names: &[&str]) -> Result Option { - let path = std::env::var_os("PATH")?; - let names: Vec = if cfg!(windows) { - vec![format!("{name}.exe"), format!("{name}.cmd")] - } else { - vec![name.to_owned()] - }; - std::env::split_paths(&path) - .flat_map(|dir| names.iter().map(move |name| dir.join(name))) - .find(|candidate| candidate.is_file()) -} - /// A host's private temporary directory, removed when the host stops. pub(crate) struct Scratch { pub path: PathBuf, @@ -321,12 +307,15 @@ mod tests { runtime, }; let capabilities = Capabilities::default(); - command(&PluginLaunch { - name: "kit", + command( + &PluginLaunch { + name: "kit", + root, + host: &host, + capabilities: &capabilities, + }, root, - host: &host, - capabilities: &capabilities, - }) + ) } /// Regression: the Windows AppContainer plan of a host held only its scratch @@ -443,7 +432,10 @@ mod tests { } Err(error) => assert_eq!( error, - format!("plugin kit: runtime {} is not installed", runtime.as_str()) + format!( + "plugin kit: runtime {} needs an executable on an absolute PATH outside the Location and package", + runtime.as_str() + ) ), } } diff --git a/crates/cyber-tools/src/plugin_host/mod.rs b/crates/cyber-tools/src/plugin_host/mod.rs index 6ea6bb00..f93fe206 100644 --- a/crates/cyber-tools/src/plugin_host/mod.rs +++ b/crates/cyber-tools/src/plugin_host/mod.rs @@ -16,6 +16,7 @@ mod host; mod launch; pub(crate) mod network; mod restart; +mod runtime_search; pub mod secrets; use std::collections::{BTreeMap, BTreeSet, HashMap}; diff --git a/crates/cyber-tools/src/plugin_host/runtime_search.rs b/crates/cyber-tools/src/plugin_host/runtime_search.rs new file mode 100644 index 00000000..3a5c8529 --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/runtime_search.rs @@ -0,0 +1,61 @@ +//! Prevent Location/package PATH entries from choosing the initial runtime. +//! This is a search guard, not immutable image or dependency admission. +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +pub(super) fn find(names: &[&str], location: &Path, package: &Path) -> Option { + let path = std::env::var_os("PATH")?; + search(names, &path, location, package) +} + +fn search(names: &[&str], path: &OsStr, location: &Path, package: &Path) -> Option { + let excluded = [location.canonicalize().ok()?, package.canonicalize().ok()?]; + find_in(names, path, &excluded) +} + +fn find_in(names: &[&str], path: &OsStr, excluded: &[PathBuf]) -> Option { + // Preserve runtime-name preference (python3 before python), then PATH order. + names.iter().find_map(|name| { + std::env::split_paths(path) + .filter(|directory| directory.is_absolute()) + .filter_map(|directory| directory.canonicalize().ok()) + .filter(|directory| !excluded.iter().any(|root| directory.starts_with(root))) + .find_map(|directory| find_at(name, &directory, excluded)) + }) +} + +fn find_at(name: &str, directory: &Path, excluded: &[PathBuf]) -> Option { + #[cfg(windows)] + let names = [format!("{name}.exe"), format!("{name}.cmd")]; + #[cfg(not(windows))] + let names = [name.to_owned()]; + names.iter().find_map(|name| { + let candidate = directory.join(name).canonicalize().ok()?; + if candidate.to_str().is_none() + || excluded.iter().any(|root| candidate.starts_with(root)) + || !executable(&candidate) + { + return None; + } + Some(candidate) + }) +} + +fn executable(path: &Path) -> bool { + let Ok(metadata) = path.metadata() else { + return false; + }; + if !metadata.is_file() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + metadata.permissions().mode() & 0o111 != 0 + } + #[cfg(not(unix))] + true +} + +#[cfg(test)] +mod tests; diff --git a/crates/cyber-tools/src/plugin_host/runtime_search/tests.rs b/crates/cyber-tools/src/plugin_host/runtime_search/tests.rs new file mode 100644 index 00000000..464a6d42 --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/runtime_search/tests.rs @@ -0,0 +1,166 @@ +use super::*; + +struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, + location: PathBuf, + package: PathBuf, + owner: PathBuf, +} +impl Fixture { + fn new() -> Self { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().canonicalize().unwrap(); + let location = root.join("project"); + let package = root.join("package"); + let owner = root.join("owner-bin"); + for path in [&location, &package, &owner] { + std::fs::create_dir(path).unwrap(); + } + Self { + _temp: temp, + root, + location, + package, + owner, + } + } + fn script(&self, directory: &Path, name: &str, token: &str) -> PathBuf { + let path = directory.join(if cfg!(windows) { + format!("{name}.exe") + } else { + name.into() + }); + std::fs::write(&path, format!("#!/bin/sh\nprintf '%s' '{token}'\n")).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + path + } + fn select(&self, names: &[&str], directories: &[&Path]) -> Option { + let path = std::env::join_paths(directories).unwrap(); + search(names, &path, &self.location, &self.package) + } +} + +#[test] +fn project_and_package_path_entries_cannot_shadow_the_owner_runtime() { + let f = Fixture::new(); + f.script(&f.location, "node", "project"); + f.script(&f.package, "node", "package"); + let owner = f.script(&f.owner, "node", "owner"); + let selected = f + .select(&["node"], &[&f.location, &f.package, &f.owner]) + .unwrap(); + #[cfg(unix)] + { + let result = std::process::Command::new(&selected).output().unwrap(); + assert!(result.status.success()); + assert_eq!(result.stdout, b"owner", "runtime selection receipt"); + } + assert_eq!(selected, owner); + assert!(f.select(&["node"], &[&f.location, &f.package]).is_none()); +} + +#[test] +fn runtime_name_preference_is_retained_before_path_directory_order() { + let f = Fixture::new(); + f.script(&f.owner, "python", "fallback"); + let other = f.root.join("second"); + std::fs::create_dir(&other).unwrap(); + let preferred = f.script(&other, "python3", "preferred"); + assert_eq!( + f.select(&["python3", "python"], &[&f.owner, &other]), + Some(preferred) + ); +} + +#[test] +fn an_unresolved_location_or_package_cannot_disable_the_search_exclusion() { + let f = Fixture::new(); + f.script(&f.owner, "node", "owner"); + let path = std::env::join_paths([&f.owner]).unwrap(); + assert!(search(&["node"], &path, &f.root.join("missing"), &f.package).is_none()); + assert!(search(&["node"], &path, &f.location, &f.root.join("missing")).is_none()); +} + +#[cfg(windows)] +#[test] +fn windows_case_variants_still_exclude_project_path_entries() { + let f = Fixture::new(); + f.script(&f.location, "node", "project"); + let upper = PathBuf::from(f.location.to_str().unwrap().to_uppercase()); + let path = std::env::join_paths([&upper]).unwrap(); + assert!(search(&["node"], &path, &f.location, &f.package).is_none()); +} + +#[cfg(unix)] +#[test] +fn directory_and_file_symlinks_cannot_reintroduce_project_runtime_code() { + use std::os::unix::fs::symlink; + let f = Fixture::new(); + let project = f.script(&f.location, "node", "project"); + let alias = f.root.join("project-alias"); + symlink(&f.location, &alias).unwrap(); + let shims = f.root.join("shims"); + std::fs::create_dir(&shims).unwrap(); + symlink(&project, shims.join("node")).unwrap(); + assert!(f.select(&["node"], &[&alias, &shims]).is_none()); + let owner = f.script(&f.owner, "node", "owner"); + assert_eq!( + f.select(&["node"], &[&alias, &shims, &f.owner]), + Some(owner) + ); + let path = std::env::join_paths([&alias]).unwrap(); + assert!(search(&["node"], &path, &alias, &f.package).is_none()); +} + +#[cfg(unix)] +#[test] +fn even_a_safe_target_link_cannot_make_a_project_path_entry_an_owner_runtime() { + let f = Fixture::new(); + let owner = f.script(&f.owner, "node", "owner"); + std::os::unix::fs::symlink(owner, f.location.join("node")).unwrap(); + assert!(f.select(&["node"], &[&f.location]).is_none()); +} + +#[cfg(unix)] +#[test] +fn empty_and_existing_relative_path_entries_are_ignored() { + let f = Fixture::new(); + let relative_bin = f.root.join("relative-bin"); + std::fs::create_dir(&relative_bin).unwrap(); + f.script(&relative_bin, "node", "relative"); + let cwd = std::env::current_dir().unwrap(); + let mut relative = PathBuf::new(); + for _ in cwd.ancestors().skip(1) { + relative.push(".."); + } + relative.push(relative_bin.strip_prefix("/").unwrap()); + assert!(relative.join("node").is_file()); + let owner = f.script(&f.owner, "node", "owner"); + assert_eq!( + f.select(&["node"], &[Path::new(""), &relative, &f.owner]), + Some(owner) + ); +} + +#[cfg(unix)] +#[test] +fn non_executable_files_and_directories_do_not_hide_a_later_runtime() { + use std::os::unix::fs::PermissionsExt; + let f = Fixture::new(); + let first = f.root.join("first"); + let second = f.root.join("second"); + std::fs::create_dir(&first).unwrap(); + std::fs::create_dir_all(second.join("node")).unwrap(); + let file = f.script(&first, "node", "not-executable"); + std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o600)).unwrap(); + let owner = f.script(&f.owner, "node", "owner"); + assert_eq!( + f.select(&["node"], &[&first, &second, &f.owner]), + Some(owner) + ); +} diff --git a/crates/cyber-tools/src/plugin_host/startup_tests.rs b/crates/cyber-tools/src/plugin_host/startup_tests.rs index cf71566f..c87b6175 100644 --- a/crates/cyber-tools/src/plugin_host/startup_tests.rs +++ b/crates/cyber-tools/src/plugin_host/startup_tests.rs @@ -273,7 +273,7 @@ fn verify(runtime: Runtime, config_name: Option<&str>) { capabilities: &capabilities, }; let (program, args) = - command(&plugin).expect("the focused runtime workflow must install this runtime"); + command(&plugin, &project).expect("the focused runtime workflow must install this runtime"); let environment = crate::plugin_host::environment::build(plugin.name, package, &scratch, &Default::default()); let result = receipt( diff --git a/openspec/changes/complete-plugin-hook-runtime/runtime-search.md b/openspec/changes/complete-plugin-hook-runtime/runtime-search.md new file mode 100644 index 00000000..bf931e4e --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/runtime-search.md @@ -0,0 +1,58 @@ +# Production runtime search guard + +The host's initial runtime lookup now ignores relative/empty PATH entries and +canonical Location/package directories. It also rejects candidates whose resolved +target is inside either excluded tree, including an outside shim symlink pointing +back into a project. Selection returns the canonical absolute path rather than +the original mutable directory alias. Missing/unresolvable scope roots refuse +selection. Unix candidates must be executable regular files; Python3 preference +and owner PATH ordering remain intact. + +This guard is used by the real production launch command, before wrapping and +spawning the host. A missing safe runtime produces a plugin-named diagnostic +asking for an executable on an absolute PATH entry outside Location/package. +The source package's declared native entry remains its own explicit entry; it is +not selected from PATH. Ordinary command-tool lookup is unchanged. + +This is not complete reviewed runtime admission. Outside PATH entries are still +owner environment input. Mutable outside binaries/dependencies, hard-link aliases, +directory replacement races, fixed runtime provenance and generation-bound image +ownership still require the native admission work. Canonicalization is a lookup +guard, not exclusion against concurrent writers. Windows legacy `.cmd` selection +is retained without claiming that such a shim is a native-admitted runtime. + +Regressions use explicit fixture PATH values rather than mutating the process +environment. They cover Location/package shadows, outside file/directory symlink +aliases, an otherwise safe target exposed through a project PATH entry, real +relative/empty entries, non-executable files/directories, unresolved scope roots +and runtime-name preference. On Unix, the selected fixture executable actually +returns its owner token. A missing-exclusions control must execute the project +fixture instead and fail that exact token-receipt assertion; a compiler/bootstrap +failure cannot count. Windows additionally checks a case variant of Location. + +The existing host-bootstrap workflow runs the search suite on Linux x64/arm64, +macOS arm64/Intel and Windows, with actual receipt controls on Unix and restored +search tests afterward. Complete runtime startup/production host regressions +remain in that workflow. Hosted execution of this new guard remains required. + +## Local verification and review + +On macOS arm64, all seven search regressions pass. Removing both scope exclusions +makes the real selected fixture return `project` instead of `owner`, failing the +exact receipt assertion. After restoring the source, the broader plugin-host suite +passes 25 tests; the two configured Bun/Deno startup receipts remain ignored. +Neither runtime is installed on this host, so those checks are not locally +verified. The focused hosted workflow installs them and explicitly runs them. +Tools library/test Clippy passes with Rust warnings denied; two deprecated C +Seatbelt SDK warnings are observed without assigning their origin. Rust formatting, +workflow YAML/ten shell blocks, diff checks, all 107 strict OpenSpec items and +spec lint (zero errors, 25 warnings) pass. Windows case/alias behavior and the +new hosted receipt controls still require CI evidence. + +The scoped code/security review covers the selector, production wiring, launch +tests, SDK guidance, OpenSpec and workflow. It confirms that both directory and +resolved file target exclusions are necessary, preserves name ordering, keeps +environment injection out of the tests, and verifies a restored suite after the +specific failing receipt. No additional source finding remains within the lookup +guard. The unresolved immutable identity, hard-link and writer-race obligations +above are not claimed fixed by this increment, and no parent task is closed. diff --git a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md index 21223eae..f97853b7 100644 --- a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md +++ b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md @@ -22,6 +22,11 @@ - **AND** permitted bundled preloads, imports and frozen package locks remain usable without rewriting the approved package - **AND** prepared Deno packages retain an owner-created boundary against ancestor workspace discovery while the workspace Location remains in the initialize envelope +#### Scenario: Project PATH entries cannot choose the initial runtime +- **WHEN** PATH includes a Location/package directory, a relative entry or a shim resolving into either excluded tree +- **THEN** host runtime lookup ignores that entry and selects a suitable outside executable or refuses with a plugin-named diagnostic +- **AND** this search guard does not substitute for immutable reviewed runtime and dependency admission + ### Requirement: Runtime contribution generation ownership (P1) Runtime hooks, providers and catalog transforms SHALL be validated and owned by plugin identity, digest, Location and host generation. Revocation SHALL remove only that generation before later dispatch, and stale results SHALL NOT restore it. Runtime hooks SHALL follow ordinary plugin hook scope and managed-only policy. Provider streams SHALL use normal usage/cancellation paths and SHALL NOT supply trusted reserved bounds by declaration alone. Catalog transforms SHALL be bounded and ordered, preserve policy/configuration ceilings and leave the previous valid view intact on failure. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 7aae83ec..b992550e 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -18,6 +18,11 @@ The full goal scope, order and acceptance gates remain unchanged. See - [ ] 2.3 Enforce `fs.read`, `fs.write`, `shell`, network and secret intersections; refuse unsupported confinement with actionable plugin diagnostics. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) passes its fixture matrix but restricts bind/listen, while the [path-scoped ABI-9 alternative](linux-unix9-candidate.md) tests preserving private IP/UNIX functionality without production activation or an approved support-floor change. None closes this task. - [ ] 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. +Runtime-selection follow-up for task 2.2: the [production PATH guard](runtime-search.md) +now excludes project/package directories, relative entries and symlink targets +back into either tree. It is not fixed reviewed runtime/image admission; the +parent native enforcement and support gates remain open. + ## 3. Host contribution lifecycle - [ ] 3.1 Validate initialize contributions and add plugin/digest/Location/generation ownership, collision handling, atomic replacement and late-result fences. diff --git a/sdk/plugin/README.md b/sdk/plugin/README.md index 5579c597..f6632965 100644 --- a/sdk/plugin/README.md +++ b/sdk/plugin/README.md @@ -77,3 +77,8 @@ Prepared Deno images also need their retained private workspace boundary; select an absolute config path alone does not prevent ancestor workspace discovery. Production launch still uses the source package; prepared-image integration and complete native admission remain outstanding. + +Interpreter runtimes must be executable files on absolute PATH entries outside +the Location and plugin package. Empty/relative entries and project/package +symlink targets cannot select the initial interpreter. This search guard does +not establish immutable runtime/dependency admission or complete native confinement. From f3e761bd98c5e55563ea9c5950b590ff2e308c6f Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 18:54:10 -0400 Subject: [PATCH 31/50] fix(plugins): retain host group identity until child cleanup --- .github/workflows/ci.yml | 4 + .../plugin-bootstrap-environment.yml | 41 ++++- crates/cyber-tools/src/plugin_host/mod.rs | 11 +- .../src/plugin_host/settlement_tests.rs | 149 ++++++++++++++++++ docs/goal-delivery.md | 3 + .../host-process-lifetime.md | 49 ++++++ .../runtime-search.md | 10 ++ .../complete-plugin-hook-runtime/tasks.md | 5 + 8 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 crates/cyber-tools/src/plugin_host/settlement_tests.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e56bd4c9..3ba11fc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: branches: [main] pull_request: +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: # Commits that only touch root Markdown, docs/ or openspec/ skip the build and test # jobs; OpenSpec validation still runs for them. diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index 99b1e107..14c80683 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -100,6 +100,42 @@ jobs: PY cp "$RUNNER_TEMP/runtime-search-positive.rs" "$source" cargo test --locked -p cyber-tools --lib plugin_host::runtime_search + - name: Verify same-group plugin child lifetime + if: runner.os != 'Windows' + shell: bash + run: cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests + - name: Verify leader-only wait controls fail child lifetime checks + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + source=crates/cyber-tools/src/plugin_host/mod.rs + cp "$source" "$RUNNER_TEMP/plugin-settlement-positive.rs" + trap 'cp "$RUNNER_TEMP/plugin-settlement-positive.rs" "$source"' EXIT + python3 - "$source" <<'PY' + from pathlib import Path + import sys + path = Path(sys.argv[1]) + text = path.read_text() + before = 'started.process.wait_tree()' + assert text.count(before) == 4 + path.write_text(text.replace(before, 'started.process.wait()')) + PY + if cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests > "$RUNNER_TEMP/plugin-settlement-control.log" 2>&1; then + echo 'Leader-only waits unexpectedly passed descendant checks' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/plugin-settlement-control.log" <<'PY' + from pathlib import Path + import re, sys + text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) + for name in ['crashed_host_stops_its_same_group_child_and_releases_scratch', + 'acknowledged_shutdown_stops_a_same_group_child_after_leader_exit']: + assert name + ' ... FAILED' in text, text + assert text.count('plugin descendant retained stdout after host settlement') == 2, text + PY + cp "$RUNNER_TEMP/plugin-settlement-positive.rs" "$source" + cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests - name: Verify bundled runtime startup receipts shell: bash run: | @@ -121,8 +157,8 @@ jobs: from pathlib import Path path = Path('crates/cyber-tools/src/plugin_host/launch.rs') text = path.read_text() - start = text.index('fn command(') - end = text.index('\nfn find_program(', start) + start = text.index('fn bun_arguments(' if sys.argv[1] == 'bun' else 'fn deno_arguments(') + end = text.index('\nfn deno_arguments(' if sys.argv[1] == 'bun' else '\nfn runtime_config(', start) body = text[start:end] flag, replacement = ('"--no-env-file".into(),', '') if sys.argv[1] == 'bun' else ('vec!["--no-config".into()]', 'Vec::new()') assert body.count(flag) == 1 @@ -229,5 +265,6 @@ jobs: ${{ runner.temp }}/runtime-deno-control.log ${{ runner.temp }}/workspace-boundary-control.log ${{ runner.temp }}/runtime-search-control.log + ${{ runner.temp }}/plugin-settlement-control.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-tools/src/plugin_host/mod.rs b/crates/cyber-tools/src/plugin_host/mod.rs index f93fe206..bb24f0e6 100644 --- a/crates/cyber-tools/src/plugin_host/mod.rs +++ b/crates/cyber-tools/src/plugin_host/mod.rs @@ -267,7 +267,7 @@ async fn supervise( entry.set(State::Ready(Arc::clone(&live))); let stopped = tokio::select! { _ = live.connection.closed.cancelled() => false, - _ = started.process.wait() => false, + _ = started.process.wait_tree() => false, _ = entry.stop.cancelled() => true, }; if stopped { @@ -275,7 +275,7 @@ async fn supervise( return None; } started.process.terminate(); - let _ = started.process.wait().await; + let _ = started.process.wait_tree().await; if let Some(proxy) = started.proxy.take() { let _ = proxy.shutdown().await; } @@ -337,12 +337,12 @@ async fn shutdown(live: &Live, mut started: launch::Started) { Some(SHUTDOWN_GRACE), ) .await; - if tokio::time::timeout(SHUTDOWN_GRACE, started.process.wait()) + if tokio::time::timeout(SHUTDOWN_GRACE, started.process.wait_tree()) .await .is_err() { started.process.terminate(); - let _ = started.process.wait().await; + let _ = started.process.wait_tree().await; } live.connection.closed.cancel(); if let Some(proxy) = started.proxy.take() { @@ -711,3 +711,6 @@ async fn log_stderr(name: String, stderr: crate::tools::process::ReadStream) { ); } } + +#[cfg(all(test, unix))] +mod settlement_tests; diff --git a/crates/cyber-tools/src/plugin_host/settlement_tests.rs b/crates/cyber-tools/src/plugin_host/settlement_tests.rs new file mode 100644 index 00000000..62a886d9 --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/settlement_tests.rs @@ -0,0 +1,149 @@ +//! Actual child-held pipe lifetime through the production host supervisors. +use super::*; +use crate::tools::process::{Process, ReadStream, WriteStream}; +use std::process::Stdio; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; + +struct Fixture { + _root: tempfile::TempDir, + started: launch::Started, + input: WriteStream, + output: BufReader, + live: Arc, + host_input: ReadStream, + host_output: WriteStream, +} + +impl Fixture { + async fn new() -> Self { + let root = tempfile::tempdir().unwrap(); + let scratch = root.path().join("scratch"); + std::fs::create_dir(&scratch).unwrap(); + // The leader exits. Its same-group child holds stdout open and waits for + // the owner's stdin EOF, providing deterministic cleanup even in controls. + let script = "import os\nif os.fork() == 0:\n os.write(1,b'child-ready\\n')\n os.read(0,1)\n os._exit(0)\nos._exit(0)\n"; + let mut process = Process::spawn_with_stdin( + "/usr/bin/python3", + &["-c".into(), script.into()], + None, + |command| { + command + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + }, + ) + .await + .unwrap(); + let input = process.stdin().unwrap(); + let mut output = BufReader::new(process.stdout().unwrap()); + let mut ready = String::new(); + tokio::time::timeout(Duration::from_secs(3), output.read_line(&mut ready)) + .await + .unwrap() + .unwrap(); + assert_eq!(ready, "child-ready\n"); + let (core_out, host_in) = tokio::io::duplex(4096); + let (host_out, core_in) = tokio::io::duplex(4096); + let handler: Handler = Arc::new(|_, _| Box::pin(async { Ok(Value::Null) })); + let live = Arc::new(Live { + connection: Connection::start(Box::new(core_out), Box::new(core_in), handler), + tools: Vec::new(), + started: Instant::now(), + }); + Self { + _root: root, + started: launch::Started { + process, + _scratch: launch::Scratch { path: scratch }, + proxy: None, + }, + input, + output, + live, + host_input: Box::new(host_in), + host_output: Box::new(host_out), + } + } +} + +fn entry(location: PathBuf) -> Entry { + Entry { + name: "settlement-fixture".into(), + digest: "fixture".into(), + location, + state: Mutex::new(State::Starting), + stop: CancellationToken::new(), + denials: Default::default(), + crashes: Default::default(), + settings: Mutex::default(), + calls: Mutex::default(), + task: Mutex::default(), + } +} + +/// Always close the controlled child's input before asserting so a missing-fix +/// test cannot leave a daemon behind. No PID inspection or reused-PID kill. +async fn assert_child_closed(mut output: BufReader, input: WriteStream) { + let mut remaining = Vec::new(); + let ended = + tokio::time::timeout(Duration::from_secs(2), output.read_to_end(&mut remaining)).await; + drop(input); + if ended.is_err() { + tokio::time::timeout(Duration::from_secs(3), output.read_to_end(&mut remaining)) + .await + .unwrap() + .unwrap(); + } + assert!( + matches!(ended, Ok(Ok(_))), + "plugin descendant retained stdout after host settlement" + ); +} + +#[tokio::test] +async fn crashed_host_stops_its_same_group_child_and_releases_scratch() { + let f = Fixture::new().await; + let scratch = f.started._scratch.path.clone(); + let e = entry(f._root.path().to_path_buf()); + let outcome = tokio::time::timeout( + Duration::from_secs(3), + supervise(&e, Arc::clone(&f.live), f.started), + ) + .await + .unwrap(); + assert_child_closed(f.output, f.input).await; + f.live.connection.closed.cancel(); + assert!(outcome.is_some()); + assert!(!scratch.exists()); + // Keep the protocol pipes open until after supervision, so EOF on that + // independent connection cannot select another cleanup branch. + drop((f.host_input, f.host_output)); +} + +#[tokio::test] +async fn acknowledged_shutdown_stops_a_same_group_child_after_leader_exit() { + let f = Fixture::new().await; + let scratch = f.started._scratch.path.clone(); + let peer = tokio::spawn(async move { + let mut input = BufReader::new(f.host_input); + let mut request = String::new(); + input.read_line(&mut request).await.unwrap(); + let request: Value = serde_json::from_str(&request).unwrap(); + assert_eq!(request["method"], "shutdown"); + let response = json!({"jsonrpc":"2.0", "id":request["id"], "result":null}); + let mut output = f.host_output; + output + .write_all(format!("{response}\n").as_bytes()) + .await + .unwrap(); + }); + tokio::time::timeout(Duration::from_secs(6), shutdown(&f.live, f.started)) + .await + .unwrap(); + peer.await.unwrap(); + assert_child_closed(f.output, f.input).await; + assert!(!scratch.exists()); +} diff --git a/docs/goal-delivery.md b/docs/goal-delivery.md index df392078..cd20b660 100644 --- a/docs/goal-delivery.md +++ b/docs/goal-delivery.md @@ -20,6 +20,9 @@ separate review. This changes delivery granularity, not feature scope or accepta - Finish and verify a coherent production increment before opening the next PR. - Keep related experiments, evidence, implementation fixes and CI fixes in the active PR. Do not open a PR for each probe or supporting component. +- Cancel superseded PR/revision CI runs after verifying their source is retained. + Main CI, like the focused workflows, cancels older runs for the same PR/ref; + completed evidence remains associated with its actual tested revision. - Consolidate the existing #70–#76 stack into draft #76 against `main`, retaining the contained commits, source branches and evidence. Close #70–#75 as superseded after verifying their heads are contained in the consolidated branch. diff --git a/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md b/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md new file mode 100644 index 00000000..b7f936f0 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md @@ -0,0 +1,49 @@ +# Same-group plugin child cleanup + +The production host supervisor now uses the existing `Process::wait_tree()` for +leader exit and graceful shutdown instead of reaping through `wait()` first. On +Unix this observes leader exit with WNOWAIT, preserving its PID/process-group +identity, requests group termination, then reaps the leader. Reaping first lost +the identity used by `terminate()`, leaving a same-group child alive when its +host leader exited. The connection-close and timeout paths use the same tree-aware +wait after their termination request. Windows retains the backend's existing +Job Object wait behavior. + +Two Unix regressions exercise the real production `supervise` and `shutdown` +functions with an actual Python leader and forked child. The leader exits while +the child holds stdout open, waiting for owner-controlled stdin EOF. Independent +JSON-RPC pipes keep connection closure from selecting a different cleanup branch; +the graceful case receives a shutdown response. The owner requires EOF on the +child-held output and verifies scratch removal after supervision. A missing-fix +control restores the four leader-only waits and must fail both named child-held +pipe assertions. Every test releases the child's input and drains its output +before asserting, so even the failing controls do not leave a daemon behind. + +This fixes same-group cleanup through those host paths. It does not establish +complete native descendant containment or prove that all resources survive every +unknown settlement. A child that changes group/session, startup-error/cancelled +future cleanup, process-wait errors, native namespace/platform admission and +generation-bound resource retention still require their own implementation and +proof. Sending a termination signal is not an assertion that every possible +descendant is reaped. No parent native admission or lifetime task is closed here. + +## Verification + +On local macOS arm64 the two live-child regressions pass. Both fail their exact +child-held pipe assertion with leader-only waits restored, then the restored +broader plugin-host suite passes 27 tests. Two Bun/Deno startup receipts remain +ignored locally because those runtimes are absent; the focused workflow installs +and explicitly runs them. Linux/native backend and hosted platform execution +remain required. The same workflow now runs these Unix tests and controls and +retains their logs, followed by the restored suite. Tools library/test Clippy passes +with Rust warnings denied; two deprecated C Seatbelt SDK warnings are observed +without assigning their origin. Formatting, the host workflow's YAML/12 shell +blocks, main CI concurrency syntax, both corrected runtime mutation scripts, +diff checks, 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) +pass locally. + +The scoped code/security review checks all four waits, cancellation behavior of +the existing Process API, pipe ownership, guaranteed fixture-child release, +independent connection lifetime and the limits above. The change reuses process +ownership infrastructure rather than inventing PID discovery or killing a PID +whose leader was already reaped. This is a scoped fix, not full native activation. diff --git a/openspec/changes/complete-plugin-hook-runtime/runtime-search.md b/openspec/changes/complete-plugin-hook-runtime/runtime-search.md index bf931e4e..f6638bcb 100644 --- a/openspec/changes/complete-plugin-hook-runtime/runtime-search.md +++ b/openspec/changes/complete-plugin-hook-runtime/runtime-search.md @@ -56,3 +56,13 @@ environment injection out of the tests, and verifies a restored suite after the specific failing receipt. No additional source finding remains within the lookup guard. The unresolved immutable identity, hard-link and writer-race obligations above are not claimed fixed by this increment, and no parent task is closed. + +## Hosted control correction at `19cf223c` + +The Linux x64/arm64 and Windows bootstrap jobs pass the new search suite and +the six bundled startup tests. The later Bun/Deno missing-filter control then +fails before executing a test: its mutation script still used the removed +`find_program` function as an end marker. This is a workflow refactor error, +not a passing denial control. It now selects the actual Bun/Deno argument-builder +functions; both exact mutation scripts are verified against temporary source +copies locally. Fresh hosted control and restored-source execution remain required. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index b992550e..52e90a54 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -23,6 +23,11 @@ now excludes project/package directories, relative entries and symlink targets back into either tree. It is not fixed reviewed runtime/image admission; the parent native enforcement and support gates remain open. +The [same-group host child cleanup](host-process-lifetime.md) routes crash and +graceful waits through the existing tree-aware Process owner. Live-child and +missing-fix regressions cover those paths; escaped descendants, startup/cancellation +settlement and complete native resource lifetime remain open. + ## 3. Host contribution lifecycle - [ ] 3.1 Validate initialize contributions and add plugin/digest/Location/generation ownership, collision handling, atomic replacement and late-result fences. From 228ac01e4298b77fc4d37406bf4bf1cf81349a7b Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 20:50:02 -0400 Subject: [PATCH 32/50] fix(plugins): retain host resources through cancelled cleanup --- .../plugin-bootstrap-environment.yml | 68 ++++-- crates/cyber-sandbox/src/proxy.rs | 53 ++++- crates/cyber-tools/src/plugin_host/launch.rs | 14 +- .../cyber-tools/src/plugin_host/lifetime.rs | 194 ++++++++++++++++ crates/cyber-tools/src/plugin_host/mod.rs | 34 +-- .../src/plugin_host/settlement_tests.rs | 208 +++++++++++++++++- .../host-process-lifetime.md | 6 + .../host-resource-ownership.md | 90 ++++++++ .../linux-ipc-candidate.md | 8 + .../specs/plugins-marketplace/spec.md | 5 + .../complete-plugin-hook-runtime/tasks.md | 5 + sdk/plugin/README.md | 3 +- 12 files changed, 624 insertions(+), 64 deletions(-) create mode 100644 crates/cyber-tools/src/plugin_host/lifetime.rs create mode 100644 openspec/changes/complete-plugin-hook-runtime/host-resource-ownership.md diff --git a/.github/workflows/plugin-bootstrap-environment.yml b/.github/workflows/plugin-bootstrap-environment.yml index 14c80683..441f17c3 100644 --- a/.github/workflows/plugin-bootstrap-environment.yml +++ b/.github/workflows/plugin-bootstrap-environment.yml @@ -104,38 +104,60 @@ jobs: if: runner.os != 'Windows' shell: bash run: cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests - - name: Verify leader-only wait controls fail child lifetime checks + - name: Verify host resource lifetime controls if: runner.os != 'Windows' shell: bash run: | set -euo pipefail - source=crates/cyber-tools/src/plugin_host/mod.rs - cp "$source" "$RUNNER_TEMP/plugin-settlement-positive.rs" - trap 'cp "$RUNNER_TEMP/plugin-settlement-positive.rs" "$source"' EXIT - python3 - "$source" <<'PY' + source=crates/cyber-tools/src/plugin_host/lifetime.rs + cp "$source" "$RUNNER_TEMP/plugin-lifetime-positive.rs" + trap 'cp "$RUNNER_TEMP/plugin-lifetime-positive.rs" "$source"' EXIT + for control in tree early-resources unknown-resources; do + cp "$RUNNER_TEMP/plugin-lifetime-positive.rs" "$source" + python3 - "$source" "$control" <<'PYCONTROL' from pathlib import Path import sys - path = Path(sys.argv[1]) - text = path.read_text() - before = 'started.process.wait_tree()' - assert text.count(before) == 4 - path.write_text(text.replace(before, 'started.process.wait()')) - PY - if cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests > "$RUNNER_TEMP/plugin-settlement-control.log" 2>&1; then - echo 'Leader-only waits unexpectedly passed descendant checks' >&2 - exit 1 - fi - python3 - "$RUNNER_TEMP/plugin-settlement-control.log" <<'PY' + path = Path(sys.argv[1]); text = path.read_text() + if sys.argv[2] == 'tree': + before, after = '.wait_tree()', '.wait()' + elif sys.argv[2] == 'early-resources': + before = 'if let Some(held) = self.held.take() {\n let _reply = schedule(held);' + after = 'if let Some(mut held) = self.held.take() {\n held.resources.take();\n let _reply = schedule(held);' + else: + before, after = 'std::mem::forget(resources);', 'drop(resources);' + assert text.count(before) == 1 + path.write_text(text.replace(before, after)) + PYCONTROL + query=same_group_child + if [[ "$control" == early-resources ]]; then query=plugin_host::settlement_tests::abandoned_initialization_keeps_scratch_until_cleanup_waits; fi + if [[ "$control" == unknown-resources ]]; then query=plugin_host::settlement_tests::unconfirmed_wait_retains_resources_without_signalling_cached_group; fi + if cargo test --locked -p cyber-tools --lib "$query" > "$RUNNER_TEMP/plugin-lifetime-$control-control.log" 2>&1; then + echo "Missing $control lifetime guard unexpectedly passed" >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/plugin-lifetime-$control-control.log" "$control" <<'PYCONTROL' from pathlib import Path import re, sys text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', Path(sys.argv[1]).read_text()) - for name in ['crashed_host_stops_its_same_group_child_and_releases_scratch', - 'acknowledged_shutdown_stops_a_same_group_child_after_leader_exit']: - assert name + ' ... FAILED' in text, text - assert text.count('plugin descendant retained stdout after host settlement') == 2, text - PY - cp "$RUNNER_TEMP/plugin-settlement-positive.rs" "$source" + cases = { + 'tree': (['crashed_host_stops_its_same_group_child_and_releases_scratch', + 'acknowledged_shutdown_stops_a_same_group_child_after_leader_exit'], + 'plugin descendant retained stdout after host settlement'), + 'early-resources': (['abandoned_initialization_keeps_scratch_until_cleanup_waits'], + 'scratch removed before cleanup waiter ran'), + 'unknown-resources': (['unconfirmed_wait_retains_resources_without_signalling_cached_group'], + 'unconfirmed wait deleted host scratch'), + } + names, reason = cases[sys.argv[2]] + for name in names: assert name + ' ... FAILED' in text, text + assert reason in text, text + PYCONTROL + done + cp "$RUNNER_TEMP/plugin-lifetime-positive.rs" "$source" cargo test --locked -p cyber-tools --lib plugin_host::settlement_tests + - name: Verify non-consuming proxy revocation + shell: bash + run: cargo test --locked -p cyber-sandbox --lib proxy::tests - name: Verify bundled runtime startup receipts shell: bash run: | @@ -265,6 +287,6 @@ jobs: ${{ runner.temp }}/runtime-deno-control.log ${{ runner.temp }}/workspace-boundary-control.log ${{ runner.temp }}/runtime-search-control.log - ${{ runner.temp }}/plugin-settlement-control.log + ${{ runner.temp }}/plugin-lifetime-*-control.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/src/proxy.rs b/crates/cyber-sandbox/src/proxy.rs index 06a5edad..957a96f5 100644 --- a/crates/cyber-sandbox/src/proxy.rs +++ b/crates/cyber-sandbox/src/proxy.rs @@ -87,12 +87,18 @@ impl Proxy { } } - /// Close every accepted client/upstream transport and acknowledge their disposal. - /// This settles local sockets; it cannot undo remote application effects. - pub async fn shutdown(mut self) -> std::io::Result<()> { + /// Request revocation while retaining the handle for disposal acknowledgement. + /// This is idempotent; it does not itself prove that sockets have closed. + pub fn request_shutdown(&mut self) { if let Some(stop) = self.stop.take() { let _ = stop.send(()); } + } + + /// Close every accepted client/upstream transport and acknowledge their disposal. + /// This settles local sockets; it cannot undo remote application effects. + pub async fn shutdown(mut self) -> std::io::Result<()> { + self.request_shutdown(); if let Some(task) = self.task.as_mut() { task.await.map_err(std::io::Error::other)?; } @@ -241,6 +247,47 @@ async fn reply( mod tests { use super::*; + #[tokio::test] + async fn shutdown_request_revokes_clients_while_owner_retains_the_proxy() { + let entered = Arc::new(tokio::sync::Notify::new()); + let signal = Arc::clone(&entered); + let decide: Decide = Arc::new(move |_| { + let signal = Arc::clone(&signal); + Box::pin(async move { + signal.notify_one(); + std::future::pending::().await + }) + }); + let mut proxy = Proxy::start(decide).await.unwrap(); + let Endpoint::Tcp(port) = proxy.endpoint else { + unreachable!() + }; + let mut client = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); + client + .write_all(b"CONNECT fixture.invalid:443 HTTP/1.1\r\n\r\n") + .await + .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(3), entered.notified()) + .await + .unwrap(); + proxy.request_shutdown(); + proxy.request_shutdown(); + let mut byte = [0]; + let closed = + tokio::time::timeout(std::time::Duration::from_secs(3), client.read(&mut byte)).await; + if closed.is_err() { + drop(proxy); + panic!("proxy revocation left client open"); + } + assert!( + matches!(closed, Ok(Ok(0))) + || matches!(closed, Ok(Err(ref error)) + if error.kind() == std::io::ErrorKind::ConnectionReset), + "{closed:?}" + ); + proxy.shutdown().await.unwrap(); + } + #[test] fn destinations_parse_connect_and_absolute_form() { assert_eq!( diff --git a/crates/cyber-tools/src/plugin_host/launch.rs b/crates/cyber-tools/src/plugin_host/launch.rs index 8eeaef6c..ee4b2dbe 100644 --- a/crates/cyber-tools/src/plugin_host/launch.rs +++ b/crates/cyber-tools/src/plugin_host/launch.rs @@ -39,13 +39,7 @@ pub(crate) struct PluginLaunch<'a> { pub capabilities: &'a Capabilities, } -/// A started host and the native resources it owns until it stops. -pub(crate) struct Started { - pub process: Process, - /// Removed when the host's resources are dropped. - pub _scratch: Scratch, - pub proxy: Option, -} +pub(crate) use super::lifetime::Started; impl LaunchContext<'_> { pub(crate) async fn start( @@ -91,11 +85,7 @@ impl LaunchContext<'_> { }) .await .map_err(|error| format!("plugin {} failed to start: {error}", plugin.name))?; - Ok(Started { - process, - _scratch: scratch, - proxy, - }) + Ok(Started::new(process, scratch, proxy, plugin.name)) } /// Narrow the resolved network to the capability; a proxy records refused hosts. diff --git a/crates/cyber-tools/src/plugin_host/lifetime.rs b/crates/cyber-tools/src/plugin_host/lifetime.rs new file mode 100644 index 00000000..ac2a8aa6 --- /dev/null +++ b/crates/cyber-tools/src/plugin_host/lifetime.rs @@ -0,0 +1,194 @@ +//! Retain a host's resources until its existing process backend confirms its wait. +//! This does not prove native containment of escaped descendants. +use std::io; +use std::process::ExitStatus; + +use cyber_sandbox::proxy::Proxy; +use tokio::sync::oneshot; + +use super::launch::Scratch; +use crate::tools::process::{Process, ReadStream, WriteStream}; + +pub(crate) struct Started { + held: Option, +} + +struct Resources { + _scratch: Scratch, + proxy: Option, +} + +struct Held { + name: String, + process: Option, + resources: Option, + wait_finished: bool, + wait_failed: bool, +} + +impl Started { + pub(super) fn new( + process: Process, + scratch: Scratch, + proxy: Option, + name: &str, + ) -> Self { + Self { + held: Some(Held { + name: name.into(), + process: Some(process), + resources: Some(Resources { + _scratch: scratch, + proxy, + }), + wait_finished: false, + wait_failed: false, + }), + } + } + + pub(super) fn stdin(&mut self) -> Option { + self.process().stdin() + } + pub(super) fn stdout(&mut self) -> Option { + self.process().stdout() + } + pub(super) fn stderr(&mut self) -> Option { + self.process().stderr() + } + + fn process(&mut self) -> &mut Process { + self.held + .as_mut() + .expect("owned host") + .process + .as_mut() + .expect("owned process") + } + + pub(super) async fn wait(&mut self) -> io::Result { + self.held.as_mut().expect("owned host").wait().await + } + + /// The cleanup task owns resources even if this caller's future is dropped. + pub(super) async fn finish(mut self) -> io::Result<()> { + let held = self.held.take().expect("owned host"); + let reply = + schedule(held).ok_or_else(|| io::Error::other("host cleanup has no runtime"))?; + reply + .await + .map_err(|_| io::Error::other("host cleanup did not finish"))? + } + + #[cfg(all(test, unix))] + pub(super) fn scratch(&self) -> &std::path::Path { + &self + .held + .as_ref() + .expect("owned host") + .resources + .as_ref() + .expect("owned resources") + ._scratch + .path + } +} + +impl Drop for Started { + fn drop(&mut self) { + if let Some(held) = self.held.take() { + let _reply = schedule(held); + } + } +} + +impl Held { + fn terminate_if_known(&mut self) { + if !self.wait_finished && !self.wait_failed { + self.process.as_mut().expect("owned process").terminate(); + } + } + + async fn wait(&mut self) -> io::Result { + if self.wait_failed { + return Err(io::Error::other("host process wait is unconfirmed")); + } + let result = self + .process + .as_mut() + .expect("owned process") + .wait_tree() + .await; + self.wait_finished |= result.is_ok(); + self.wait_failed |= result.is_err(); + result + } + + async fn finish(mut self) -> io::Result<()> { + if self.wait_failed { + return Err(io::Error::other("host process wait is unconfirmed")); + } + if !self.wait_finished { + self.wait().await?; + } + let proxy = self + .resources + .as_mut() + .and_then(|resources| resources.proxy.take()); + if let Some(proxy) = proxy { + proxy.shutdown().await?; + } + self.resources.take(); + Ok(()) + } +} + +fn schedule(mut held: Held) -> Option>> { + // Revoke execution promptly before the asynchronous waiter is polled. A failed + // wait cannot authorize signalling a numeric PID whose identity is uncertain. + held.terminate_if_known(); + if let Some(proxy) = held + .resources + .as_mut() + .and_then(|resources| resources.proxy.as_mut()) + { + proxy.request_shutdown(); + } + let Ok(runtime) = tokio::runtime::Handle::try_current() else { + return None; + }; + let (reply, received) = oneshot::channel(); + runtime.spawn(async move { + let result = held.finish().await; + let _ = reply.send(result); + }); + Some(received) +} + +impl Drop for Held { + fn drop(&mut self) { + if let Some(mut resources) = self.resources.take() { + // Runtime shutdown, a panic or an unconfirmed wait is not proof that + // it is safe to remove a namespace a writer might still use. + // Retain the namespace, not active network authority. Dropping a + // remaining proxy aborts its owned listener/connection task. + resources.proxy.take(); + std::mem::forget(resources); + if self.wait_failed + && let Some(process) = self.process.take() + { + // Process::drop would signal its cached group. After an unknown + // wait, neither signalling nor releasing that identity is proven. + std::mem::forget(process); + } + cyber_core::log::warn( + "plugins", + &format!( + "plugin {} resources retained after unconfirmed cleanup", + self.name + ), + serde_json::json!({"plugin":self.name,"settlement":"unconfirmed"}), + ); + } + } +} diff --git a/crates/cyber-tools/src/plugin_host/mod.rs b/crates/cyber-tools/src/plugin_host/mod.rs index bb24f0e6..2f72a91f 100644 --- a/crates/cyber-tools/src/plugin_host/mod.rs +++ b/crates/cyber-tools/src/plugin_host/mod.rs @@ -14,6 +14,7 @@ mod connection; mod environment; mod host; mod launch; +mod lifetime; pub(crate) mod network; mod restart; mod runtime_search; @@ -267,17 +268,20 @@ async fn supervise( entry.set(State::Ready(Arc::clone(&live))); let stopped = tokio::select! { _ = live.connection.closed.cancelled() => false, - _ = started.process.wait_tree() => false, + _ = started.wait() => false, _ = entry.stop.cancelled() => true, }; if stopped { shutdown(&live, started).await; return None; } - started.process.terminate(); - let _ = started.process.wait_tree().await; - if let Some(proxy) = started.proxy.take() { - let _ = proxy.shutdown().await; + live.connection.closed.cancel(); + if started.finish().await.is_err() { + entry.set(State::Failed(format!( + "plugin {} cleanup is unconfirmed; resources retained", + entry.name + ))); + return None; } let error = format!("plugin {} crashed", entry.name); Some((error, live.started.elapsed())) @@ -337,17 +341,9 @@ async fn shutdown(live: &Live, mut started: launch::Started) { Some(SHUTDOWN_GRACE), ) .await; - if tokio::time::timeout(SHUTDOWN_GRACE, started.process.wait_tree()) - .await - .is_err() - { - started.process.terminate(); - let _ = started.process.wait_tree().await; - } + let _ = tokio::time::timeout(SHUTDOWN_GRACE, started.wait()).await; live.connection.closed.cancel(); - if let Some(proxy) = started.proxy.take() { - let _ = proxy.shutdown().await; - } + let _ = started.finish().await; } async fn start( @@ -389,17 +385,20 @@ async fn start( .start(&request, &entry.denials) .await .map_err(StartError::Fatal)?; - let (Some(stdin), Some(stdout)) = (started.process.stdin(), started.process.stdout()) else { + let (Some(stdin), Some(stdout)) = (started.stdin(), started.stdout()) else { return Err(StartError::Fatal(format!( "plugin {}: host streams unavailable", plugin.name ))); }; - if let Some(stderr) = started.process.stderr() { + if let Some(stderr) = started.stderr() { tokio::spawn(log_stderr(plugin.name.clone(), stderr)); } let handler = handler(entry, &plugin, settings.secrets); let connection = Connection::start(stdin, stdout, handler); + // An aborted initialization must close its protocol scope too, even if an + // in-flight handler temporarily retains another Arc to the connection. + let initializing = connection.closed.clone().drop_guard(); let params = json!({ "protocolVersion": PROTOCOL_VERSION, "core": {"name": "cyber", "version": cyber_core::version::build_info().version}, @@ -436,6 +435,7 @@ async fn start( tools, started: Instant::now(), }); + initializing.disarm(); Ok((live, started)) } diff --git a/crates/cyber-tools/src/plugin_host/settlement_tests.rs b/crates/cyber-tools/src/plugin_host/settlement_tests.rs index 62a886d9..3d64a1a4 100644 --- a/crates/cyber-tools/src/plugin_host/settlement_tests.rs +++ b/crates/cyber-tools/src/plugin_host/settlement_tests.rs @@ -12,16 +12,21 @@ struct Fixture { live: Arc, host_input: ReadStream, host_output: WriteStream, + leader: i32, } impl Fixture { async fn new() -> Self { + Self::with_proxy(None).await + } + + async fn with_proxy(proxy: Option) -> Self { let root = tempfile::tempdir().unwrap(); let scratch = root.path().join("scratch"); std::fs::create_dir(&scratch).unwrap(); // The leader exits. Its same-group child holds stdout open and waits for // the owner's stdin EOF, providing deterministic cleanup even in controls. - let script = "import os\nif os.fork() == 0:\n os.write(1,b'child-ready\\n')\n os.read(0,1)\n os._exit(0)\nos._exit(0)\n"; + let script = "import os\nleader=os.getpid()\nif os.fork() == 0:\n os.write(1,('child-ready '+str(leader)+'\\n').encode())\n while os.read(0,1): os.write(1,b'child-alive\\n')\n os._exit(0)\nos._exit(0)\n"; let mut process = Process::spawn_with_stdin( "/usr/bin/python3", &["-c".into(), script.into()], @@ -44,7 +49,13 @@ impl Fixture { .await .unwrap() .unwrap(); - assert_eq!(ready, "child-ready\n"); + let leader: i32 = ready + .trim() + .strip_prefix("child-ready ") + .unwrap() + .parse() + .unwrap(); + assert!(leader > 0); let (core_out, host_in) = tokio::io::duplex(4096); let (host_out, core_in) = tokio::io::duplex(4096); let handler: Handler = Arc::new(|_, _| Box::pin(async { Ok(Value::Null) })); @@ -55,16 +66,18 @@ impl Fixture { }); Self { _root: root, - started: launch::Started { + started: launch::Started::new( process, - _scratch: launch::Scratch { path: scratch }, - proxy: None, - }, + launch::Scratch { path: scratch }, + proxy, + "settlement-fixture", + ), input, output, live, host_input: Box::new(host_in), host_output: Box::new(host_out), + leader, } } } @@ -106,7 +119,7 @@ async fn assert_child_closed(mut output: BufReader, input: WriteStre #[tokio::test] async fn crashed_host_stops_its_same_group_child_and_releases_scratch() { let f = Fixture::new().await; - let scratch = f.started._scratch.path.clone(); + let scratch = f.started.scratch().to_path_buf(); let e = entry(f._root.path().to_path_buf()); let outcome = tokio::time::timeout( Duration::from_secs(3), @@ -126,7 +139,7 @@ async fn crashed_host_stops_its_same_group_child_and_releases_scratch() { #[tokio::test] async fn acknowledged_shutdown_stops_a_same_group_child_after_leader_exit() { let f = Fixture::new().await; - let scratch = f.started._scratch.path.clone(); + let scratch = f.started.scratch().to_path_buf(); let peer = tokio::spawn(async move { let mut input = BufReader::new(f.host_input); let mut request = String::new(); @@ -147,3 +160,182 @@ async fn acknowledged_shutdown_stops_a_same_group_child_after_leader_exit() { assert_child_closed(f.output, f.input).await; assert!(!scratch.exists()); } + +async fn wait_removed(path: &std::path::Path) { + tokio::time::timeout(Duration::from_secs(3), async { + while path.exists() { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn abandoned_initialization_keeps_scratch_until_cleanup_waits() { + let f = Fixture::new().await; + let scratch = f.started.scratch().to_path_buf(); + drop(f.started); + // This is a current-thread runtime. No cleanup task can have been polled + // between dropping the owner and recording this observation. + let retained_before_wait = scratch.exists(); + assert_child_closed(f.output, f.input).await; + wait_removed(&scratch).await; + f.live.connection.closed.cancel(); + assert!( + retained_before_wait, + "scratch removed before cleanup waiter ran" + ); +} + +#[tokio::test] +async fn cancelled_finish_does_not_cancel_owned_cleanup() { + let f = Fixture::new().await; + let scratch = f.started.scratch().to_path_buf(); + let mut finish = Box::pin(f.started.finish()); + assert!(futures::poll!(&mut finish).is_pending()); + drop(finish); + assert_child_closed(f.output, f.input).await; + wait_removed(&scratch).await; + f.live.connection.closed.cancel(); +} + +#[allow(unsafe_code)] +fn reap_owned_leader(pid: i32) { + loop { + // SAFETY: this PID was captured by the fixed fixture before fork and + // names this test's direct, still-unreaped child. No signal is sent. + let result = unsafe { libc::waitpid(pid, std::ptr::null_mut(), 0) }; + if result == -1 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted + { + continue; + } + assert_eq!(result, pid); + break; + } +} + +async fn assert_alive_then_release(mut output: BufReader, mut input: WriteStream) { + let mut reply = String::new(); + let receipt = tokio::time::timeout(Duration::from_secs(3), async { + input.write_all(b"p").await?; + output.read_line(&mut reply).await + }) + .await; + drop(input); + let mut remaining = Vec::new(); + tokio::time::timeout(Duration::from_secs(3), output.read_to_end(&mut remaining)) + .await + .unwrap() + .unwrap(); + assert!( + matches!(receipt, Ok(Ok(_))) && reply == "child-alive\n", + "unconfirmed wait signalled the cached process group" + ); +} + +#[tokio::test] +async fn unconfirmed_wait_retains_resources_without_signalling_cached_group() { + let entered = Arc::new(tokio::sync::Notify::new()); + let signal = Arc::clone(&entered); + let decide: cyber_sandbox::proxy::Decide = Arc::new(move |_| { + let signal = Arc::clone(&signal); + Box::pin(async move { + signal.notify_one(); + std::future::pending::().await + }) + }); + let proxy = cyber_sandbox::proxy::Proxy::start(decide).await.unwrap(); + let cyber_sandbox::proxy::Endpoint::Tcp(port) = proxy.endpoint else { + unreachable!() + }; + let mut client = tokio::net::TcpStream::connect(("127.0.0.1", port)) + .await + .unwrap(); + client + .write_all(b"CONNECT fixture.invalid:443 HTTP/1.1\r\n\r\n") + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(3), entered.notified()) + .await + .unwrap(); + let mut f = Fixture::with_proxy(Some(proxy)).await; + let scratch = f.started.scratch().to_path_buf(); + reap_owned_leader(f.leader); + assert!(f.started.wait().await.is_err()); + let result = f.started.finish().await; + let retained = scratch.exists(); + let mut byte = [0]; + let closed = tokio::time::timeout(Duration::from_secs(3), client.read(&mut byte)).await; + assert_alive_then_release(f.output, f.input).await; + f.live.connection.closed.cancel(); + assert!(result.is_err()); + assert!(retained, "unconfirmed wait deleted host scratch"); + assert!( + matches!(closed, Ok(Ok(0))) + || matches!(closed, Ok(Err(ref error)) + if error.kind() == std::io::ErrorKind::ConnectionReset), + "unconfirmed host retained network authority: {closed:?}" + ); +} + +#[tokio::test] +async fn unconfirmed_supervision_stops_the_immediate_restart_loop() { + let f = Fixture::new().await; + let scratch = f.started.scratch().to_path_buf(); + let e = entry(f._root.path().to_path_buf()); + reap_owned_leader(f.leader); + let outcome = tokio::time::timeout( + Duration::from_secs(3), + supervise(&e, Arc::clone(&f.live), f.started), + ) + .await + .unwrap(); + let failed = matches!(&*e.state.lock().unwrap_or_else(PoisonError::into_inner), + State::Failed(reason) if reason.contains("cleanup is unconfirmed")); + let retained = scratch.exists(); + assert_alive_then_release(f.output, f.input).await; + assert!( + outcome.is_none(), + "unconfirmed supervision requested a crash restart" + ); + assert!( + failed && retained, + "unconfirmed supervision did not preserve failure and resources" + ); + assert!(f.live.connection.closed.is_cancelled()); +} + +#[tokio::test] +async fn aborting_cleanup_runtime_retains_unconfirmed_resources() { + let f = Fixture::new().await; + let scratch = f.started.scratch().to_path_buf(); + std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async { + drop(f.started); + }); + // Its queued cleanup was never polled. Dropping this runtime aborts it. + drop(runtime); + }) + .join() + .unwrap(); + let retained = scratch.exists(); + assert_child_closed(f.output, f.input).await; + f.live.connection.closed.cancel(); + assert!(retained, "aborted cleanup deleted unconfirmed host scratch"); +} + +#[tokio::test] +async fn dropping_owner_without_a_runtime_retains_unconfirmed_resources() { + let f = Fixture::new().await; + let scratch = f.started.scratch().to_path_buf(); + std::thread::spawn(move || drop(f.started)).join().unwrap(); + let retained = scratch.exists(); + assert_child_closed(f.output, f.input).await; + f.live.connection.closed.cancel(); + assert!(retained, "missing cleanup runtime deleted host scratch"); +} diff --git a/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md b/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md index b7f936f0..3f3c134c 100644 --- a/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md +++ b/openspec/changes/complete-plugin-hook-runtime/host-process-lifetime.md @@ -27,6 +27,12 @@ generation-bound resource retention still require their own implementation and proof. Sending a termination signal is not an assertion that every possible descendant is reaped. No parent native admission or lifetime task is closed here. +The [started-host ownership follow-up](host-resource-ownership.md) now retains +resources across startup returns and cancellation and tracks unconfirmed backend +waits. It centralizes the tree-aware wait; the historical four-wait mutation below +is replaced by a mutation of that shared wait plus two resource-retention controls. +Escaped descendants and full generation/admission obligations remain open. + ## Verification On local macOS arm64 the two live-child regressions pass. Both fail their exact diff --git a/openspec/changes/complete-plugin-hook-runtime/host-resource-ownership.md b/openspec/changes/complete-plugin-hook-runtime/host-resource-ownership.md new file mode 100644 index 00000000..0acba299 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/host-resource-ownership.md @@ -0,0 +1,90 @@ +# Cancellation-safe ownership of started hosts + +The real production launch now returns an opaque `Started` owner. It holds the +Process, scratch and optional proxy together before initialization receives any +stdio endpoints. Callers cannot independently reap the raw Process and forget +the resource-retention state. Its tracked wait uses the existing backend's +tree-aware wait and records success or an unconfirmed error. + +Dropping the owner on an early return or cancelled initialization requests known +process-group termination and proxy shutdown immediately, then transfers ownership +to a cleanup task. `finish()` awaits that same owned cleanup pattern; dropping its +caller future does not cancel the cleanup task. Scratch is released only after +the backend wait succeeds and proxy disposal is acknowledged. The proxy's new +idempotent `request_shutdown()` separates prompt revocation from later disposal +acknowledgement; ordinary `shutdown()` retains its previous behavior. +Initialization also holds a protocol cancellation guard until successful handoff; +abandoning it cancels the connection even if an in-flight handler holds an Arc. +An unconfirmed supervised cleanup records a failed host and stops the immediate +crash-restart loop rather than reporting a confirmed crash. + +If a wait returns an error, the owner cannot safely signal a cached numeric group. +It preserves the Process identity and scratch rather than running Process Drop's +terminator or deleting the namespace. Runtime loss, task abortion and unwinding +also retain unconfirmed scratch. A remaining proxy is aborted rather than retained +as active network authority. The plugin-named diagnostic reports unconfirmed +cleanup without exposing resource paths. Retention is intentionally conservative; +it does not certify that the host stopped or that remote effects were undone. + +The process backend's containment assumptions still apply. Escaped process groups, +native namespace/platform admission, protected staging ancestors, generation-bound +launch/revocation, quarantine of unconfirmed generations and retry fencing remain +open. In particular this owner alone does not prevent a later host incarnation +from being launched by the registry. Those obligations must be resolved before +native activation or closing the parent lifecycle requirements. The App continues +to use its legacy sandbox. + +## Regressions and evidence scope + +Eight real Unix process tests exercise production supervision and this owner: + +- Crash and acknowledged shutdown kill the same-group child and release scratch. +- Abandoned initialization retains scratch until its queued waiter actually runs. +- Cancelling a `finish` future still lets owned cleanup complete. +- An actual externally reaped fixture leader makes the backend wait unconfirmed. + Scratch remains; the owner does not signal the cached group. A live-child token + roundtrip proves the child still runs, then the fixture releases it deliberately. + A real accepted proxy client closes independently despite that unconfirmed wait. +- Dropping a separate cleanup runtime before it polls its task retains scratch. +- Dropping the owner outside a runtime retains scratch instead of pretending that + a reap was observed. +- An actual unconfirmed supervisor wait records failed cleanup, cancels the + connection and returns the directive that stops the immediate restart loop. + +The fixed fixture provides its own direct-child PID before fork. Its test-only +waitpid reaps only that child and sends no signal. All controls release stdin +and drain the owned child's stdout before asserting, avoiding daemon leaks or +killing a reused PID. A separate sandbox test requests proxy revocation while +retaining its owner handle and observes accepted-client disposal before joining. + +Missing-tree-wait, premature-resource-drop and missing-unconfirmed-retention +mutations must fail their exact named native assertions, followed by restored +tests. The existing bootstrap workflow keeps these on Linux/macOS and runs the +proxy test on every configured platform. Windows Job Object behavior and all +hosted platform results still require CI evidence; local Unix positives cannot +certify them. + +## Local verification and review + +On macOS arm64 all eight lifetime tests and both sandbox proxy tests pass. Each +of the three missing-guard mutations fails its named native assertion; the exact +positive source is restored and the broader plugin-host suite passes 33 tests. +The two configured Bun/Deno receipts remain ignored locally because neither +runtime is installed. The hosted bootstrap workflow installs and executes them. +Sandbox/Tools library-and-test Clippy passes with Rust warnings denied; two +deprecated Seatbelt SDK warnings are observed without assigning their origin. +Formatting, workflow YAML/13 shell blocks and ten embedded Python blocks, exact +application of all three hosted mutation scripts, diff checks, 107 strict +OpenSpec items and spec lint (zero errors, 25 warnings) pass. Fresh native platform +execution is required for this ownership revision. + +The scoped code/security review covers all changed production paths, tests, +workflow and SDK/spec documentation. It identified that retaining the entire +resource bundle could retain active proxy authority; the final implementation +requests shutdown synchronously and aborts any remaining proxy before retaining +scratch. The real accepted-client/unknown-wait regression verifies this distinction. +Process identity is not signalled after an unconfirmed wait. Cleanup state remains +owned across caller cancellation, and runtime abortion leaks unconfirmed scratch +rather than deleting it. The test-only external reap is limited to the fixture's +own direct child. Registry quarantine, retry fencing and native containment are +explicit remaining integration requirements, not acceptance supplied by this review. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md index 0c165dd3..389f9994 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-ipc-candidate.md @@ -104,3 +104,11 @@ job-reported candidate helper hashes are and `909a09be86ab2173c489e75a0a1b3bd4b75a0902ea511d4598518490535f3766` (arm64). Binaries were not retained for independent rehashing. The support/design gate and the explicit bind/listen limitation remain open despite these passes. + +The compatible-mechanism investigation remains necessary: a descriptor-pinned +syscall broker is a possible next experiment for preserving private IP bind/listen +and internal pairs. It must first prove native descriptor-duplication and thread +identity support on both Linux hosts. It must operate on a pinned descriptor and +validated copied arguments rather than continue a guest-pointer syscall after a +racy inspection. No such broker is implemented or accepted here; its lifecycle, +thread/fork coverage, policy intersection and support gate remain required. diff --git a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md index f97853b7..7dc61c5e 100644 --- a/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md +++ b/openspec/changes/complete-plugin-hook-runtime/specs/plugins-marketplace/spec.md @@ -27,6 +27,11 @@ - **THEN** host runtime lookup ignores that entry and selects a suitable outside executable or refuses with a plugin-named diagnostic - **AND** this search guard does not substitute for immutable reviewed runtime and dependency admission +#### Scenario: Initialization cleanup retains unconfirmed resources +- **WHEN** initialization returns early or its future is cancelled after host process creation +- **THEN** a separate owner continues process cleanup and proxy revocation and retains scratch until backend wait and proxy disposal are acknowledged +- **AND** runtime loss or an unconfirmed wait retains resources and reports uncertainty rather than treating a termination request as proof of disposal + ### Requirement: Runtime contribution generation ownership (P1) Runtime hooks, providers and catalog transforms SHALL be validated and owned by plugin identity, digest, Location and host generation. Revocation SHALL remove only that generation before later dispatch, and stale results SHALL NOT restore it. Runtime hooks SHALL follow ordinary plugin hook scope and managed-only policy. Provider streams SHALL use normal usage/cancellation paths and SHALL NOT supply trusted reserved bounds by declaration alone. Catalog transforms SHALL be bounded and ordered, preserve policy/configuration ceilings and leave the previous valid view intact on failure. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 52e90a54..2c5c4282 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -28,6 +28,11 @@ graceful waits through the existing tree-aware Process owner. Live-child and missing-fix regressions cover those paths; escaped descendants, startup/cancellation settlement and complete native resource lifetime remain open. +The [cancellation-safe started-host owner](host-resource-ownership.md) now retains +scratch through asynchronous cleanup and unconfirmed waits while revoking its +proxy. Native containment, staging ancestry, quarantine and generation/retry +fencing still gate complete host lifetime acceptance. + ## 3. Host contribution lifecycle - [ ] 3.1 Validate initialize contributions and add plugin/digest/Location/generation ownership, collision handling, atomic replacement and late-result fences. diff --git a/sdk/plugin/README.md b/sdk/plugin/README.md index f6632965..a0565104 100644 --- a/sdk/plugin/README.md +++ b/sdk/plugin/README.md @@ -63,7 +63,8 @@ Use `initialize` configuration and the authorized `secrets/get` API for plugin settings and credentials. Host processes receive an explicit bootstrap environment; ordinary-tool environment overlays and inherited runtime/preload variables are not passed through. Home, temporary and runtime cache paths point -inside the host's private scratch, which is removed when the host stops. The +inside the host's private scratch. Scratch cleanup waits for process and proxy +disposal; unconfirmed cleanup retains it and reports a plugin-named diagnostic. The owner supplies any permitted proxy endpoint. This does not complete native filesystem/subprocess capability enforcement or runtime admission. From 7936cb6da961cdb753e482c7c905acb034916ad7 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 22:09:42 -0400 Subject: [PATCH 33/50] test(plugins): probe thread-specific socket descriptor pinning --- .github/workflows/plugin-linux-ipc.yml | 16 ++ .../linux-pidfd-proof.md | 63 ++++++ .../complete-plugin-hook-runtime/tasks.md | 5 + .../plugin-native-entry/linux_pidfd_probe.c | 214 ++++++++++++++++++ scripts/probes/plugin-native-entry/pidfd.mts | 80 +++++++ 5 files changed, 378 insertions(+) create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md create mode 100644 scripts/probes/plugin-native-entry/linux_pidfd_probe.c create mode 100644 scripts/probes/plugin-native-entry/pidfd.mts diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index cc0d622d..540b443d 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -10,6 +10,8 @@ on: - scripts/probes/plugin-native-entry/build_socket_candidate.py - scripts/probes/plugin-native-entry/linux_socket_landlock9.h - scripts/probes/plugin-native-entry/linux_unix9_policy.h + - scripts/probes/plugin-native-entry/linux_pidfd_probe.c + - scripts/probes/plugin-native-entry/pidfd.mts permissions: contents: read concurrency: @@ -50,6 +52,20 @@ jobs: sudo apt-get update sudo apt-get install -y bubblewrap build-essential pkg-config cmake strace sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + - name: Observe descriptor-pinning prerequisites + shell: bash + run: | + set -euo pipefail + cc -Wall -Wextra -Werror -pthread scripts/probes/plugin-native-entry/linux_pidfd_probe.c -o "$RUNNER_TEMP/pidfd-probe" + node --experimental-strip-types scripts/probes/plugin-native-entry/pidfd.mts --probe "$RUNNER_TEMP/pidfd-probe" --report "$RUNNER_TEMP/linux-pidfd.json" + - name: Retain descriptor prerequisite observations + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-pidfd-${{ matrix.os }} + path: ${{ runner.temp }}/linux-pidfd.json + if-no-files-found: warn + retention-days: 14 - name: Build production helper and diagnostic id: build shell: bash diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md new file mode 100644 index 00000000..f49eb03d --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md @@ -0,0 +1,63 @@ +# Descriptor-pinned broker prerequisites (experiment only) + +The ABI-3 global bind/listen restriction and currently unavailable ABI-9 alternative +remain unselected. This experiment checks whether an unprivileged native parent +can safely pin the exact requesting thread's socket and operate on a copied +address. It is not the broker itself, a capability policy or production admission. + +The controlled child creates a worker with an independently unshared descriptor +table. The same descriptor number denotes a UNIX socket in its thread-group +leader and an INET socket in the worker. A TGID-only duplicate must expose the +wrong-family counterexample; `PIDFD_THREAD` plus `pidfd_getfd` must select the +worker's actual socket. The parent reads only the fixed fixture sockaddr from +that child, then has the child overwrite the original address before binding +and listening through the pinned descriptor and copied address. The child must +observe the resulting bound port. + +Next the worker replaces its descriptor number with a UNIX socket. A fresh +lookup must see UNIX while the retained descriptor remains INET and completes +a real TCP token exchange. After the worker exits, its pidfd must fail rather +than select the still-live leader's table. Finally the controlled child becomes +nondumpable and both descriptor duplication and memory copying must refuse with +EPERM; no privilege fallback is attempted. + +The driver runs this in the ordinary host and in private user/PID/network/IPC/UTS +namespaces with capabilities dropped. Only fixture-owned sockets and memory are +used. Pipe frames are bounded; each transfer has a deadline. Failure paths kill +and reap the exact owned child before reporting, and parent death closes the +command pipe so the child cannot remain a daemon. Reports bind checkout/run, +source manifest/hash and probe binary hash to observations. + +Explicit kernel/permission refusals are recorded as prerequisites unavailable, +not passing confinement or runtime acceptance. Resource exhaustion and failed +assertions remain failures. Native Linux x64/arm64 execution is required; macOS +can validate only the TypeScript driver and workflow. `PIDFD_THREAD` is a kernel +feature dependency, not an approved new production support floor. + +The kernel [pidfd UAPI](https://github.com/torvalds/linux/blob/master/include/uapi/linux/pidfd.h) +defines the thread flag. The [pidfd_open manual](https://man7.org/linux/man-pages/man2/pidfd_open.2.html) +documents its Linux 6.9 introduction, and the +[pidfd_getfd manual](https://man7.org/linux/man-pages/man2/pidfd_getfd.2.html) +documents duplication of an open file description and ptrace permission checks. +The actual hosted result must establish availability rather than infer it from +a kernel version string. + +Even a positive prerequisite result leaves seccomp notification ownership, +copied-address validation, cancellation, descendants/threads, namespace and +network/proxy/secret ceilings, runtime compatibility and explicit support/design +review open. No production source or support decision changes in this experiment. + +Local verification: TypeScript syntax and the driver's positive/unavailable plus +four negative classification controls pass. Workflow YAML and nine shell blocks, +diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) +pass. Linux C compilation and kernel execution cannot run on this macOS host and +are not claimed verified; both native architecture jobs are required. + +The scoped source/security review checks fixed-size pipe frames, thread-table +ownership, snapshot/pinned-object ordering, explicit unavailability classification, +child reaping on every report path and cleanup on parent death. It tightened the +reap result check, distinguishes short memory copies from permission refusals, +and makes guest-address mutation volatile so the counterexample is not removed +by optimization. The probe reads only its own fixture child's memory. It does not +change the one-shot exec supervisor, authorize later exec or inspect real plugins. +Native results remain authoritative for whether the proposed prerequisites work. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 2c5c4282..53172d38 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -33,6 +33,11 @@ scratch through asynchronous cleanup and unconfirmed waits while revoking its proxy. Native containment, staging ancestry, quarantine and generation/retry fencing still gate complete host lifetime acceptance. +The next compatible Linux IPC investigation starts with [descriptor-pinning +prerequisites](linux-pidfd-proof.md), including independent thread file tables, +descriptor-number replacement, copied arguments and actual private TCP exchange. +This experiment changes no production policy or accepted support floor. + ## 3. Host contribution lifecycle - [ ] 3.1 Validate initialize contributions and add plugin/digest/Location/generation ownership, collision handling, atomic replacement and late-result fences. diff --git a/scripts/probes/plugin-native-entry/linux_pidfd_probe.c b/scripts/probes/plugin-native-entry/linux_pidfd_probe.c new file mode 100644 index 00000000..35f6277b --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_pidfd_probe.c @@ -0,0 +1,214 @@ +/* Fixture-only broker prerequisites. No production helper or ambient service. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* PIDFD_THREAD is O_EXCL in the kernel UAPI, including older build headers. */ +#ifndef PIDFD_THREAD +#define PIDFD_THREAD O_EXCL +#endif + +struct notice { + int process, thread, fd, phase, domain, error; + uintptr_t address; + unsigned int length, port; +}; +_Static_assert(sizeof(struct notice) <= 64, "bounded fixture frame"); +struct context { int command, report, fd; }; + +static int transfer(int fd, void *bytes, size_t size, int writing) { + size_t offset = 0; + while (offset < size) { + struct pollfd wait = { .fd = fd, .events = writing ? POLLOUT : POLLIN }; + int ready = poll(&wait, 1, 3000); + if (ready < 0 && errno == EINTR) continue; + if (ready <= 0) { errno = ready == 0 ? ETIMEDOUT : errno; return -1; } + ssize_t count = writing ? write(fd, (char *)bytes + offset, size - offset) + : read(fd, (char *)bytes + offset, size - offset); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) { if (count == 0) errno = EPIPE; return -1; } + offset += (size_t)count; + } + return 0; +} +static int domain(int fd) { + int result = -1; + socklen_t size = sizeof(result); + return getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &result, &size) == 0 ? result : -1; +} +static void report(struct context *c, int phase, volatile struct sockaddr_in *address, int error) { + struct sockaddr_in bound = {0}; + socklen_t length = sizeof(bound); + unsigned int port = getsockname(c->fd, (struct sockaddr *)&bound, &length) == 0 + && bound.sin_family == AF_INET ? ntohs(bound.sin_port) : 0; + struct notice value = { .process = getpid(), .thread = (int)syscall(SYS_gettid), + .fd = c->fd, .phase = phase, .domain = domain(c->fd), .error = error, + .address = (uintptr_t)address, .length = sizeof(*address), .port = port }; + if (transfer(c->report, &value, sizeof(value), 1) != 0) _exit(3); +} +static void expect(struct context *c, char expected) { + char actual; + if (transfer(c->command, &actual, 1, 0) != 0 || actual != expected) _exit(3); +} +static void *worker(void *argument) { + struct context *c = argument; + volatile struct sockaddr_in address = { .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + if (unshare(CLONE_FILES) != 0) { report(c, -1, &address, errno); return NULL; } + int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0 || dup2(fd, c->fd) != c->fd) _exit(3); + if (fd != c->fd) close(fd); + report(c, 0, &address, 0); + expect(c, 'M'); + volatile unsigned char *bytes = (volatile unsigned char *)&address; + for (size_t i = 0; i < sizeof(address); i++) bytes[i] = 0xa5; + report(c, 1, &address, 0); + expect(c, 'B'); + report(c, 2, &address, 0); + expect(c, 'C'); + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0 || + dup2(pair[0], c->fd) != c->fd) _exit(3); + if (pair[0] != c->fd) close(pair[0]); + close(pair[1]); + report(c, 3, &address, 0); + expect(c, 'X'); + return NULL; +} +static void child(struct context c) { + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) _exit(3); + c.fd = pair[0]; close(pair[1]); + pthread_t thread; + if (pthread_create(&thread, NULL, worker, &c) != 0 || pthread_join(thread, NULL) != 0) _exit(3); + struct sockaddr_in cell = {0}; + report(&c, 4, &cell, 0); + expect(&c, 'D'); + if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) != 0) _exit(3); + report(&c, 5, &cell, 0); + expect(&c, 'Q'); + _exit(0); +} +static ssize_t copy_memory(pid_t pid, uintptr_t address, void *out, size_t size) { + struct iovec local = { .iov_base = out, .iov_len = size }; + struct iovec remote = { .iov_base = (void *)address, .iov_len = size }; + ssize_t result = process_vm_readv(pid, &local, 1, &remote, 1, 0); + if (result >= 0 && result != (ssize_t)size) errno = EIO; + return result; +} +static int roundtrip(int listener, const struct sockaddr_in *address) { + int client = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0), peer = -1, ok = 0; + if (client < 0 || connect(client, (const struct sockaddr *)address, sizeof(*address)) != 0) goto done; + peer = accept4(listener, NULL, NULL, SOCK_CLOEXEC); + char token[12]; + ok = peer >= 0 && send(client, "fixture-only", 12, MSG_NOSIGNAL) == 12 && + recv(peer, token, sizeof(token), MSG_WAITALL) == 12 && memcmp(token, "fixture-only", 12) == 0; +done: + if (peer >= 0) close(peer); + if (client >= 0) close(client); + return ok; +} + +int main(void) { + int commands[2], reports[2]; + if (pipe2(commands, O_CLOEXEC) != 0 || pipe2(reports, O_CLOEXEC) != 0) return 2; + signal(SIGPIPE, SIG_IGN); + pid_t pid = fork(); + if (pid < 0) return 2; + if (pid == 0) { + close(commands[1]); close(reports[0]); + child((struct context){ .command = commands[0], .report = reports[1] }); + } + close(commands[0]); close(reports[1]); + const char *stage = "initial_notice", *outcome = "failed"; + int error = 0, process_handle = -1, thread_handle = -1, wrong = -1, pinned = -1, replacement = -1; + int leader_domain = -1, thread_domain = -1, replacement_domain = -1, pinned_domain = -1; + int copied_changed = 0, guest_bound = 0, tcp = 0, thread_gone = 0, private_fd = 0, private_memory = 0; + struct notice n; + struct sockaddr_in snapshot, changed, bound; + socklen_t length = sizeof(bound); +#define CHECK(test, label) do { stage = label; if (!(test)) { error = errno; goto done; } } while (0) +#define CAP(test, label) do { stage = label; if (!(test)) { error = errno; \ + if (error == EPERM || error == ENOSYS || error == EINVAL) { outcome = "unavailable"; } \ + goto done; } } while (0) +#define COMMAND(value) do { char byte = value; CHECK(transfer(commands[1], &byte, 1, 1) == 0, "command"); } while (0) +#define NOTICE(phase_value) do { CHECK(transfer(reports[0], &n, sizeof(n), 0) == 0, "notice"); \ + CHECK(n.phase == phase_value, "phase"); } while (0) + CHECK(transfer(reports[0], &n, sizeof(n), 0) == 0, "initial_notice"); + if (n.phase == -1) { + stage = "unshare_files"; error = n.error; + if (error == EPERM || error == ENOSYS || error == EINVAL) outcome = "unavailable"; + goto done; + } + CHECK(n.phase == 0 && n.process == pid && n.thread != pid && n.length == sizeof(snapshot), "initial_identity"); + CAP((process_handle = (int)syscall(SYS_pidfd_open, pid, 0)) >= 0, "pidfd_process"); + CAP((thread_handle = (int)syscall(SYS_pidfd_open, n.thread, PIDFD_THREAD)) >= 0, "pidfd_thread"); + CAP((wrong = (int)syscall(SYS_pidfd_getfd, process_handle, n.fd, 0)) >= 0, "duplicate_process"); + CAP((pinned = (int)syscall(SYS_pidfd_getfd, thread_handle, n.fd, 0)) >= 0, "duplicate_thread"); + leader_domain = domain(wrong); thread_domain = domain(pinned); + CHECK(leader_domain == AF_UNIX && thread_domain == AF_INET, "thread_file_table"); + CAP(copy_memory(n.thread, n.address, &snapshot, sizeof(snapshot)) == (ssize_t)sizeof(snapshot), "copy_guest_memory"); + CHECK(snapshot.sin_family == AF_INET && snapshot.sin_addr.s_addr == htonl(INADDR_LOOPBACK) && snapshot.sin_port == 0, "copied_address"); + COMMAND('M'); NOTICE(1); + CHECK(copy_memory(n.thread, n.address, &changed, sizeof(changed)) == (ssize_t)sizeof(changed), "copy_changed_memory"); + copied_changed = memcmp(&snapshot, &changed, sizeof(snapshot)) != 0; + CHECK(copied_changed, "guest_changed_address"); + CHECK(bind(pinned, (struct sockaddr *)&snapshot, sizeof(snapshot)) == 0 && listen(pinned, 1) == 0, "pinned_bind_listen"); + CHECK(getsockname(pinned, (struct sockaddr *)&bound, &length) == 0, "bound_address"); + COMMAND('B'); NOTICE(2); + guest_bound = n.port == ntohs(bound.sin_port) && n.port != 0; + CHECK(guest_bound, "guest_observed_bind"); + COMMAND('C'); NOTICE(3); + CAP((replacement = (int)syscall(SYS_pidfd_getfd, thread_handle, n.fd, 0)) >= 0, "duplicate_replacement"); + replacement_domain = domain(replacement); pinned_domain = domain(pinned); + CHECK(replacement_domain == AF_UNIX && pinned_domain == AF_INET, "descriptor_replacement"); + tcp = roundtrip(pinned, &bound); + CHECK(tcp, "pinned_tcp_roundtrip"); + COMMAND('X'); NOTICE(4); + int dead = (int)syscall(SYS_pidfd_getfd, thread_handle, n.fd, 0); + thread_gone = dead < 0 ? errno : 0; if (dead >= 0) close(dead); + CHECK(thread_gone == ESRCH, "thread_handle_after_exit"); + COMMAND('D'); NOTICE(5); + int denied = (int)syscall(SYS_pidfd_getfd, process_handle, n.fd, 0); + private_fd = denied < 0 ? errno : 0; if (denied >= 0) close(denied); + private_memory = copy_memory(pid, n.address, &changed, sizeof(changed)) < 0 ? errno : 0; + CHECK(private_fd == EPERM && private_memory == EPERM, "nondumpable_denial"); + COMMAND('Q'); + outcome = "passed"; stage = "complete"; +done: + close(commands[1]); close(reports[0]); + if (strcmp(outcome, "passed") != 0) kill(pid, SIGKILL); + int status = 0; + pid_t waited; + do { waited = waitpid(pid, &status, 0); } while (waited < 0 && errno == EINTR); + if (waited != pid) { outcome = "failed"; stage = "child_reap"; error = errno; } + else if (strcmp(outcome, "passed") == 0 && (!WIFEXITED(status) || WEXITSTATUS(status) != 0)) { + outcome = "failed"; stage = "child_exit"; + } + for (size_t i = 0; i < 5; i++) { + int fd = (int[]){process_handle, thread_handle, wrong, pinned, replacement}[i]; + if (fd >= 0) close(fd); + } + printf("{\"outcome\":\"%s\",\"stage\":\"%s\",\"errno\":%d," + "\"leader_domain\":%d,\"thread_domain\":%d,\"replacement_domain\":%d,\"pinned_domain\":%d," + "\"copied_address_changed\":%s,\"guest_observed_bind\":%s,\"pinned_tcp_roundtrip\":%s," + "\"thread_after_exit_errno\":%d,\"nondumpable_fd_errno\":%d,\"nondumpable_memory_errno\":%d}\n", + outcome, stage, error, leader_domain, thread_domain, replacement_domain, pinned_domain, + copied_changed ? "true" : "false", guest_bound ? "true" : "false", tcp ? "true" : "false", + thread_gone, private_fd, private_memory); + return strcmp(outcome, "failed") == 0 ? 1 : 0; +} diff --git a/scripts/probes/plugin-native-entry/pidfd.mts b/scripts/probes/plugin-native-entry/pidfd.mts new file mode 100644 index 00000000..c6016ab7 --- /dev/null +++ b/scripts/probes/plugin-native-entry/pidfd.mts @@ -0,0 +1,80 @@ +/** Descriptor-pinning prerequisites only; no production confinement acceptance. */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { release } from "node:os"; +import { parseArgs } from "node:util"; + +type Observation = { + outcome: "passed" | "unavailable" | "failed"; stage: string; errno: number; + leader_domain: number; thread_domain: number; replacement_domain: number; pinned_domain: number; + copied_address_changed: boolean; guest_observed_bind: boolean; pinned_tcp_roundtrip: boolean; + thread_after_exit_errno: number; nondumpable_fd_errno: number; nondumpable_memory_errno: number; +}; +function verify(row: Observation): void { + assert(["passed", "unavailable"].includes(row.outcome), `native prerequisite failed: ${JSON.stringify(row)}`); + if (row.outcome === "unavailable") { + assert(["unshare_files", "pidfd_process", "pidfd_thread", "duplicate_process", "duplicate_thread", + "copy_guest_memory", "duplicate_replacement"].includes(row.stage)); + assert([1, 22, 38].includes(row.errno), "unavailability must report a specific permission/kernel refusal"); + return; + } + assert.equal(row.stage, "complete"); assert.equal(row.errno, 0); + assert.equal(row.leader_domain, 1, "TGID lookup must expose the wrong UNIX socket counterexample"); + assert.equal(row.thread_domain, 2, "thread pidfd must select its independent INET table"); + assert.equal(row.replacement_domain, 1); assert.equal(row.pinned_domain, 2); + for (const key of ["copied_address_changed", "guest_observed_bind", "pinned_tcp_roundtrip"] as const) { + assert.equal(row[key], true, key); + } + assert.equal(row.thread_after_exit_errno, 3, "thread handle must fail after that thread exits"); + assert.equal(row.nondumpable_fd_errno, 1); assert.equal(row.nondumpable_memory_errno, 1); +} +function selfTest(): void { + const good: Observation = { outcome: "passed", stage: "complete", errno: 0, + leader_domain: 1, thread_domain: 2, replacement_domain: 1, pinned_domain: 2, + copied_address_changed: true, guest_observed_bind: true, pinned_tcp_roundtrip: true, + thread_after_exit_errno: 3, nondumpable_fd_errno: 1, nondumpable_memory_errno: 1 }; + verify(good); + verify({ ...good, outcome: "unavailable", stage: "duplicate_thread", errno: 1 }); + assert.throws(() => verify({ ...good, pinned_domain: 1 })); + assert.throws(() => verify({ ...good, thread_after_exit_errno: 0 })); + assert.throws(() => verify({ ...good, outcome: "unavailable", stage: "pinned_tcp_roundtrip", errno: 1 })); + assert.throws(() => verify({ ...good, outcome: "unavailable", stage: "duplicate_thread", errno: 12 })); +} +const { values } = parseArgs({ options: { probe: { type: "string" }, report: { type: "string" }, + "self-test": { type: "boolean" } } }); +selfTest(); +if (!values["self-test"]) { + assert.equal(process.platform, "linux"); assert(values.probe && values.report); + const rows: { mode: string; status: number | null; observed: Observation }[] = []; + let outcome = "failed"; + try { + for (const mode of ["host", "private-namespaces"]) { + const program = mode === "host" ? values.probe : "/usr/bin/bwrap"; + const args = mode === "host" ? [] : ["--die-with-parent", "--unshare-user", "--unshare-pid", + "--unshare-net", "--unshare-ipc", "--unshare-uts", "--disable-userns", "--cap-drop", "ALL", + "--ro-bind", "/", "/", "--proc", "/proc", "--dev", "/dev", "--", values.probe]; + const result = spawnSync(program, args, { encoding: "utf8", timeout: 20_000, + maxBuffer: 64 * 1024, env: { PATH: "/usr/bin:/bin" } }); + assert(!result.error, `native process observation failed: ${result.error}`); + const observed: Observation = JSON.parse(result.stdout); + rows.push({ mode, status: result.status, observed }); + assert.equal(result.status, 0, result.stderr); + verify(observed); + } + outcome = rows.every(row => row.observed.outcome === "passed") ? "prerequisites-passed" : "prerequisites-unavailable"; + } finally { + const sources = ["scripts/probes/plugin-native-entry/linux_pidfd_probe.c", + "scripts/probes/plugin-native-entry/pidfd.mts", ".github/workflows/plugin-linux-ipc.yml"]; + const hash = createHash("sha256"); + for (const path of sources) hash.update(path + "\0").update(readFileSync(path)); + writeFileSync(values.report, JSON.stringify({ platform: process.platform, architecture: process.arch, + kernel_release: release(), checkout: process.env.GITHUB_SHA ?? null, run_id: process.env.GITHUB_RUN_ID ?? null, + outcome, sources, source_sha256: hash.digest("hex"), + probe_sha256: createHash("sha256").update(readFileSync(values.probe)).digest("hex"), rows, + scope: "Owned-fixture syscall prerequisites only. No syscall broker, sandbox policy, runtime support revision or production acceptance." }, null, 2) + "\n"); + console.log(JSON.stringify({ outcome, native_policy_acceptance: false, + observations: rows.map(row => ({ mode: row.mode, outcome: row.observed.outcome, stage: row.observed.stage, errno: row.observed.errno })) })); + } +} From c0feb881a2ac34499ab91b2191f0a89736aa245e Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Thu, 29 Oct 2026 22:27:12 -0400 Subject: [PATCH 34/50] docs: prioritize active goal PR readiness --- docs/goal-delivery.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/goal-delivery.md b/docs/goal-delivery.md index cd20b660..b41fed1c 100644 --- a/docs/goal-delivery.md +++ b/docs/goal-delivery.md @@ -31,6 +31,12 @@ separate review. This changes delivery granularity, not feature scope or accepta its delivery increment is incomplete. - Do not open the next implementation PR until the current increment is merged, superseded or deliberately split for independent review within the cap. +- Prioritize fixing failing checks and making the active increment ready for + review before starting another delivery increment. Report pending and failing + checks explicitly; do not describe the active PR as ready while they remain. +- Count goal PRs separately from unrelated repository PRs. Inspect ownership and + scope before consolidating or closing other contributors' work; the delivery + cap does not authorize closing unrelated PRs. - Preserve native proof and explicit support/design revision gates. Passing probes are not production feature acceptance. Never weaken the agreed contract merely to finish a PR, and never merge automatically. From 91568c780d7f481ca8bfe2af3fb6a74bceb4f35c Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 00:55:24 -0400 Subject: [PATCH 35/50] test: bound feedback chain waits by progress --- .../tests/support/native_exec_linux.rs | 15 +++++------ .../tests/file_observation_dispatch.rs | 27 ++++++++++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/crates/cyber-sandbox/tests/support/native_exec_linux.rs b/crates/cyber-sandbox/tests/support/native_exec_linux.rs index e3975fa6..ed6dda81 100644 --- a/crates/cyber-sandbox/tests/support/native_exec_linux.rs +++ b/crates/cyber-sandbox/tests/support/native_exec_linux.rs @@ -14,14 +14,13 @@ fn output(command: &mut Command, input: Option<&[u8]>) -> Output { .stderr(Stdio::piped()) .spawn() .unwrap(); - if let Some(input) = input { - 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}"); - } + if let Some(input) = input + && 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); diff --git a/crates/cyber-tools/tests/file_observation_dispatch.rs b/crates/cyber-tools/tests/file_observation_dispatch.rs index 4a09020f..3d6d463e 100644 --- a/crates/cyber-tools/tests/file_observation_dispatch.rs +++ b/crates/cyber-tools/tests/file_observation_dispatch.rs @@ -1103,10 +1103,29 @@ async fn hook_writes_stop_at_the_durable_feedback_limit_after_quiet_time() { "SELECT COUNT(*) FROM event WHERE type=?1 AND json_extract(data,'$.dispatch')=0 AND json_array_length(json_extract(data,'$.updates'))>0", [records::CHANGED], |row| row.get::<_, i64>(0))?)).unwrap() }; - await_condition("self-write loop reaches its durable pause", || { - suppressed() > 0 - }) - .await; + // This is 64 sequential native observations, each followed by an HTTP hook + // and a durable commit. A missed native hint may wait for the owner's + // three-second reconciliation. Bound the whole chain and each stalled step + // separately instead of applying the single-observation 45-second budget. + let deadline = tokio::time::Instant::now() + + Duration::from_secs(5 * u64::from(records::FEEDBACK_LIMIT + 1)); + let mut progress_deadline = tokio::time::Instant::now() + Duration::from_secs(45); + let mut observed = 0; + while suppressed() == 0 { + let current = effects.load(Ordering::SeqCst); + assert!(current <= records::FEEDBACK_LIMIT as usize); + if current > observed { + observed = current; + progress_deadline = tokio::time::Instant::now() + Duration::from_secs(45); + } + let now = tokio::time::Instant::now(); + assert!( + now < deadline && now < progress_deadline, + "self-write loop did not reach its durable pause: {current}/{} callbacks", + records::FEEDBACK_LIMIT + ); + tokio::time::sleep(Duration::from_millis(30)).await; + } assert_eq!( effects.load(Ordering::SeqCst), records::FEEDBACK_LIMIT as usize From 7fbf8924ad2229b7c4ba949c674c89fe4b97f55c Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 00:55:24 -0400 Subject: [PATCH 36/50] docs: retain native descriptor pinning receipts --- .../linux-pidfd-proof.md | 20 +++++++ .../evidence/linux-pidfd-7936cb6d/arm64.json | 56 +++++++++++++++++++ .../evidence/linux-pidfd-7936cb6d/x64.json | 56 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/arm64.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/x64.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md index f49eb03d..0421ed10 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-pidfd-proof.md @@ -61,3 +61,23 @@ and makes guest-address mutation volatile so the counterexample is not removed by optimization. The probe reads only its own fixture child's memory. It does not change the one-shot exec supervisor, authorize later exec or inspect real plugins. Native results remain authoritative for whether the proposed prerequisites work. + +## Native prerequisite receipts + +[Run 40990090248](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/40990090248) +completed successfully on Linux x64 and arm64. The retained +[x64 report](../../../scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/x64.json) +and [arm64 report](../../../scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/arm64.json) +each contain passing host and private-namespace observations on kernel +`7.0.0-1017-azure`. Both establish thread-table selection, socket pinning across +descriptor replacement, copied-address mutation followed by a TCP exchange, +ESRCH after thread exit and EPERM for nondumpable-child access. + +The reports bind merge checkout `c42831be5ee522833b802960c9e568db8433a522` +to source SHA-256 `9ef84af7a5ee191fbaa25127844674c7758e00dc31330616d4905f5488462a9f`. +That manifest was independently recomputed from the committed checkout when +retaining these receipts. Probe binary hashes are reported by the native jobs +inside each receipt; local macOS verification does not independently execute +those Linux binaries. These four positive cases support implementing an isolated +broker candidate. They do not prove confinement, compatibility on other kernels, +production admission or an approved support-floor change. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/arm64.json b/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/arm64.json new file mode 100644 index 00000000..782d1af7 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/arm64.json @@ -0,0 +1,56 @@ +{ + "platform": "linux", + "architecture": "arm64", + "kernel_release": "7.0.0-1017-azure", + "checkout": "c42831be5ee522833b802960c9e568db8433a522", + "run_id": "40990090248", + "outcome": "prerequisites-passed", + "sources": [ + "scripts/probes/plugin-native-entry/linux_pidfd_probe.c", + "scripts/probes/plugin-native-entry/pidfd.mts", + ".github/workflows/plugin-linux-ipc.yml" + ], + "source_sha256": "9ef84af7a5ee191fbaa25127844674c7758e00dc31330616d4905f5488462a9f", + "probe_sha256": "798fea438ad18f137655d48d43947825b571292d7e5a4e76d2167823deb16533", + "rows": [ + { + "mode": "host", + "status": 0, + "observed": { + "outcome": "passed", + "stage": "complete", + "errno": 0, + "leader_domain": 1, + "thread_domain": 2, + "replacement_domain": 1, + "pinned_domain": 2, + "copied_address_changed": true, + "guest_observed_bind": true, + "pinned_tcp_roundtrip": true, + "thread_after_exit_errno": 3, + "nondumpable_fd_errno": 1, + "nondumpable_memory_errno": 1 + } + }, + { + "mode": "private-namespaces", + "status": 0, + "observed": { + "outcome": "passed", + "stage": "complete", + "errno": 0, + "leader_domain": 1, + "thread_domain": 2, + "replacement_domain": 1, + "pinned_domain": 2, + "copied_address_changed": true, + "guest_observed_bind": true, + "pinned_tcp_roundtrip": true, + "thread_after_exit_errno": 3, + "nondumpable_fd_errno": 1, + "nondumpable_memory_errno": 1 + } + } + ], + "scope": "Owned-fixture syscall prerequisites only. No syscall broker, sandbox policy, runtime support revision or production acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/x64.json b/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/x64.json new file mode 100644 index 00000000..0eae1382 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-pidfd-7936cb6d/x64.json @@ -0,0 +1,56 @@ +{ + "platform": "linux", + "architecture": "x64", + "kernel_release": "7.0.0-1017-azure", + "checkout": "c42831be5ee522833b802960c9e568db8433a522", + "run_id": "40990090248", + "outcome": "prerequisites-passed", + "sources": [ + "scripts/probes/plugin-native-entry/linux_pidfd_probe.c", + "scripts/probes/plugin-native-entry/pidfd.mts", + ".github/workflows/plugin-linux-ipc.yml" + ], + "source_sha256": "9ef84af7a5ee191fbaa25127844674c7758e00dc31330616d4905f5488462a9f", + "probe_sha256": "0b13a037dbadf03abe6dc9a3e613c6e614c19b3dea0ac318034479ccbee13290", + "rows": [ + { + "mode": "host", + "status": 0, + "observed": { + "outcome": "passed", + "stage": "complete", + "errno": 0, + "leader_domain": 1, + "thread_domain": 2, + "replacement_domain": 1, + "pinned_domain": 2, + "copied_address_changed": true, + "guest_observed_bind": true, + "pinned_tcp_roundtrip": true, + "thread_after_exit_errno": 3, + "nondumpable_fd_errno": 1, + "nondumpable_memory_errno": 1 + } + }, + { + "mode": "private-namespaces", + "status": 0, + "observed": { + "outcome": "passed", + "stage": "complete", + "errno": 0, + "leader_domain": 1, + "thread_domain": 2, + "replacement_domain": 1, + "pinned_domain": 2, + "copied_address_changed": true, + "guest_observed_bind": true, + "pinned_tcp_roundtrip": true, + "thread_after_exit_errno": 3, + "nondumpable_fd_errno": 1, + "nondumpable_memory_errno": 1 + } + } + ], + "scope": "Owned-fixture syscall prerequisites only. No syscall broker, sandbox policy, runtime support revision or production acceptance." +} From 321e607230c0c9c10f0f51276c418f5033531325 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 00:58:09 -0400 Subject: [PATCH 37/50] test(plugins): separate fixture startup from cleanup deadlines --- crates/cyber-tools/src/plugin_host/settlement_tests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/cyber-tools/src/plugin_host/settlement_tests.rs b/crates/cyber-tools/src/plugin_host/settlement_tests.rs index 3d64a1a4..1db040a3 100644 --- a/crates/cyber-tools/src/plugin_host/settlement_tests.rs +++ b/crates/cyber-tools/src/plugin_host/settlement_tests.rs @@ -45,10 +45,13 @@ impl Fixture { let input = process.stdin().unwrap(); let mut output = BufReader::new(process.stdout().unwrap()); let mut ready = String::new(); - tokio::time::timeout(Duration::from_secs(3), output.read_line(&mut ready)) + // Cold system-Python startup and fork can exceed three seconds on a + // busy hosted runner. This handshake precedes every measured cleanup; + // it must not consume or relax the descendant-settlement deadlines. + tokio::time::timeout(Duration::from_secs(30), output.read_line(&mut ready)) .await - .unwrap() - .unwrap(); + .expect("system Python fixture did not report child-ready within 30 seconds") + .expect("system Python fixture readiness pipe failed"); let leader: i32 = ready .trim() .strip_prefix("child-ready ") From f84c3942852b81239e63e95d0e7a1a7ea37e443c Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:08:52 -0400 Subject: [PATCH 38/50] test(plugins): prototype descriptor-pinned socket mediation --- .github/workflows/plugin-linux-ipc.yml | 31 +- .../examples/plugin_unix_socket_probe.rs | 23 +- .../linux-socket-broker-candidate.md | 88 ++++++ .../complete-plugin-hook-runtime/tasks.md | 2 +- .../build_socket_candidate.py | 43 ++- .../plugin-native-entry/linux_socket_broker.h | 281 ++++++++++++++++++ 6 files changed, 453 insertions(+), 15 deletions(-) create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md create mode 100644 scripts/probes/plugin-native-entry/linux_socket_broker.h diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 540b443d..76acdac9 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -5,8 +5,13 @@ on: paths: - crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c + - crates/cyber-sandbox/native/** + - crates/cyber-sandbox/src/plugin_policy.rs + - crates/cyber-sandbox/src/plugin_policy/** + - scripts/probes/plugin-confinement/** - .github/workflows/plugin-linux-ipc.yml - scripts/probes/plugin-native-entry/linux_socket_candidate.h + - scripts/probes/plugin-native-entry/linux_socket_broker.h - scripts/probes/plugin-native-entry/build_socket_candidate.py - scripts/probes/plugin-native-entry/linux_socket_landlock9.h - scripts/probes/plugin-native-entry/linux_unix9_policy.h @@ -21,7 +26,7 @@ jobs: ipc: name: UNIX socket diagnostic (${{ matrix.os }}) runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -71,16 +76,34 @@ jobs: shell: bash run: | set -euo pipefail - cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-candidate" + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker" broker + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-bind" broker-no-bind python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-candidate" landlock9 python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-missing-resolve" landlock9-no-resolve + cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe - name: Observe fixture-only host socket reachability shell: bash run: | set -euo pipefail target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker.json" --broker + - name: Reject blanket bind denial as broker acceptance + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-no-bind" "$RUNNER_TEMP/linux-ipc-broker-control.json" --broker > "$RUNNER_TEMP/broker-no-bind.log" 2>&1; then + echo 'Missing IP server capability unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/broker-no-bind.log" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'IP server/client token exchange' in text, text + assert '"tcp_roundtrip":false' in text, text + PY - name: Record pathname policy kernel availability id: abi shell: bash @@ -153,13 +176,14 @@ jobs: set -euo pipefail mkdir -p "$RUNNER_TEMP/socket-runtime" status=0 - for policy in seccomp unix9; do + for policy in seccomp broker unix9; do if [[ "$policy" == unix9 && '${{ steps.abi.outputs.supported }}' != true ]]; then echo 'UNVERIFIED: skipping ABI-9 runtime matrix on this unsupported kernel.' continue fi mkdir -p "$RUNNER_TEMP/socket-runtime/$policy" helper="$RUNNER_TEMP/socket-candidate" + if [[ "$policy" == broker ]]; then helper="$RUNNER_TEMP/socket-broker"; fi if [[ "$policy" == unix9 ]]; then helper="$RUNNER_TEMP/unix9-candidate"; fi for runtime in node python bun deno native; do args=(--runtime "$runtime" --initial-exec --packaged-helper "$helper" --report "$RUNNER_TEMP/socket-runtime/$policy/$runtime.json" --trace "$RUNNER_TEMP/socket-runtime/$policy/trace-$runtime") @@ -186,5 +210,6 @@ jobs: ${{ runner.temp }}/socket-runtime/ ${{ runner.temp }}/unix9-missing-resolve.log ${{ runner.temp }}/unix9-unavailable.log + ${{ runner.temp }}/broker-no-bind.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index 4169aba6..ec50eab2 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -63,6 +63,7 @@ mod probe { enum Mode { Baseline, Seccomp, + Broker, Unix9, } @@ -93,7 +94,7 @@ mod probe { policy.target_args[1].clone(), "pair".into(), ]; - if mode == Mode::Unix9 { + if matches!(mode, Mode::Unix9 | Mode::Broker) { pair_args.push(policy.scratch.join("local.sock").to_str().unwrap().into()); } let pair_policy = LinuxPolicy { @@ -115,19 +116,19 @@ mod probe { "private pair escape: {key}: {observed}" ); } - for key in [ - "datagram_pair", - "seqpacket_pair", - "tcp_roundtrip", - "scratch_unix_roundtrip", - ] { + for key in ["datagram_pair", "seqpacket_pair", "scratch_unix_roundtrip"] { assert_eq!( observed[key], mode == Mode::Unix9, "pair/local support: {key}: {observed}" ); } - if mode == Mode::Seccomp { + assert_eq!( + observed["tcp_roundtrip"], + matches!(mode, Mode::Unix9 | Mode::Broker), + "IP server/client token exchange: {observed}" + ); + if matches!(mode, Mode::Seccomp | Mode::Broker) { assert_eq!(observed["pair_listen"], false, "pair listener: {observed}"); } let delivered = receipt(listener, &observed); @@ -150,6 +151,7 @@ mod probe { let mode = match args.get(2).and_then(|arg| arg.to_str()) { None => Mode::Baseline, Some("--candidate") => Mode::Seccomp, + Some("--broker") => Mode::Broker, Some("--landlock9") => Mode::Unix9, _ => panic!("unknown candidate mode"), }; @@ -271,6 +273,7 @@ mod probe { "Cargo.lock", ".github/workflows/plugin-linux-ipc.yml", "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", "scripts/probes/plugin-native-entry/linux_unix9_policy.h", "scripts/probes/plugin-native-entry/build_socket_candidate.py", @@ -283,9 +286,9 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "landlock9": mode == Mode::Unix9, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode == Mode::Broker, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), - "rows": rows, "scope": "Diagnostic host UNIX-socket reachability only. Fixture messages only; no real broker, filesystem/shell escalation or capability acceptance."}); + "rows": rows, "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance."}); std::fs::write( report_path, format!("{}\n", serde_json::to_string_pretty(&report).unwrap()), diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md new file mode 100644 index 00000000..e4af898e --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -0,0 +1,88 @@ +# Descriptor-pinned socket broker candidate + +This is an isolated early-proof experiment under the existing confinement gate. +It does not activate native hosting or select a production support revision. +The [prerequisite receipts](linux-pidfd-proof.md) support trying exact-thread +descriptor duplication and owned sockaddr copies on the tested Linux hosts. + +The copied C launcher becomes a trusted single-threaded parent plus a guest. +The guest enters the existing filesystem policy, installs a long-lived +notification filter and transfers its listener before the trusted initial exec. +An initial proposal to keep two separate listeners was rejected during source +review: the [seccomp API](https://man7.org/linux/man-pages/man2/seccomp.2.html) +can reject a second NEW_LISTENER with EBUSY. The candidate therefore uses one +listener, with a separately bounded one-use startup decision in its parent. +Only the exact guest PID's first execve may continue, while no untrusted code or +threads have run. That budget is consumed before replying; later execve requests +receive ENOSYS when shell is false. Existing spawn/execveat denials remain in the +copied filter. With shell true, the existing inherited exec/spawn policy remains. +This is a candidate-specific change to the initial-exec mechanism, requiring real +runtime/constructor and adversarial tests plus explicit review. The production +one-shot supervisor is unchanged. The guest sees sanitized standard descriptors. +The parent is nondumpable and closes stdin/stdout. Admission still requires the +existing private mount/PID/network/IPC namespaces and dropped capabilities. + +The socket filter intercepts bind, listen and connect. The parent validates the +notification, opens a PIDFD_THREAD handle for its exact requesting TID, duplicates +the socket, and copies the bounded sockaddr into parent-owned storage. It checks +the socket domain/type and copied address, then validates the notification again. +Only INET/INET6 stream/datagram objects are admitted; listen requires a stream. +An AF_UNSPEC connect may disconnect an IP object. UNIX stream pairs remain usable +but cannot bind, listen or reconnect. Other UNIX pair types remain unavailable in +this candidate. These restrictions and the PIDFD_THREAD dependency require review; +they are not silently substituted for the approved production contract. + +The broker performs the operation on the pinned object and copied arguments. It +never continues a socket syscall after inspecting mutable pointers or descriptor +numbers. Dedicated forked workers keep blocking connect calls out of the event +loop. A fixed slot limit bounds workers, pipes and retained descriptors. Workers +retain only their pinned socket and result pipe, close inherited listeners and +standard descriptors, and set parent-death termination. Notification cancellation +kills and reaps the corresponding owned worker; it cannot undo an operation that +already happened. Cancellation therefore cannot establish absence of an effect. +Each worker identity remains owned until reaped, including after its reply. + +The parent polls worker results, validates notification identities before replies, +and checks guest termination. A timer interrupts a receive whose pending request +disappears after poll, so cancellation cannot strand the event loop there. Guest +exit tears down all outstanding owned workers. Parent-death termination plus the +mandatory private PID namespace remain essential; escaped descendant settlement +and namespace teardown need their own native proof before production admission. + +Required evidence starts with actual private TCP server/client token exchange, +UNIX stream-pair communication, denied ambient fixture sockets and forbidden pair +operations, across read/write combinations on x64 and arm64. The existing runtime +matrix must also execute this helper. Follow-up adversarial cases must cover +thread-local tables, argument/FD mutation during real notifications, cancellation, +worker saturation, signals, descendants and listener lifetime. The prerequisite +fixture alone cannot establish those broker properties. Network/proxy/secret +ceilings, immutable runtime admission and the full native support/design gate +remain open even if these initial candidate cases pass. + +The initial workflow also builds a precise control that retains blanket bind +denial in the otherwise identical candidate. It must fail the actual TCP token +exchange assertion. A missing server capability therefore cannot count as this +candidate's successful confinement/compatibility result. + +The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) +requires copied tracee arguments to avoid policy decisions on mutable memory. +This experiment additionally pins the actual socket object and emulates the +operation instead of using CONTINUE. Native checks must establish its behavior; +source review and successful compilation are insufficient acceptance evidence. + +Local verification on macOS: all five builder modes produce their expected +temporary source transformations without changing production native source +hashes; this check intercepts the compiler and does not compile Linux C. The +Rust diagnostic example passes Clippy with Rust warnings denied. Workflow YAML, +ten shell blocks and embedded Python parse; formatting/diff checks, all 107 +strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C +SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, +kernel behavior, the positive TCP cases, missing-bind control and runtime matrix +remain unverified until the corresponding hosted x64/arm64 jobs complete. + +Scoped source/security review rejected the nested-listener design, corrected a +potential second signal after guest reaping, drains worker replies before +reaping their identities, unblocks the receive-watchdog signal, and ensures the +child closes bootstrap/listener descriptors before executing target code. +These source corrections are not native evidence. In particular the unified +startup decision and adversarial lifetime cases remain explicit review gates. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 53172d38..e87dfa6c 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -15,7 +15,7 @@ The full goal scope, order and acceptance gates remain unchanged. See - [ ] 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 [Rust Linux composition](linux-policy-composition.md), which supplies bounded arguments/profile and actual-helper regressions passing on Linux x64/arm64 at `546641f9`; 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. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) passes its fixture matrix but restricts bind/listen, while the [path-scoped ABI-9 alternative](linux-unix9-candidate.md) tests preserving private IP/UNIX functionality without production activation or an approved support-floor change. None closes this task. +- [ ] 2.3 Enforce `fs.read`, `fs.write`, `shell`, network and secret intersections; refuse unsupported confinement with actionable plugin diagnostics. [Linux IPC diagnostic](linux-ipc-proof.md) proves fixture-owned host socket reachability through the composed production boundary; the [isolated socket-family candidate](linux-ipc-candidate.md) passes its fixture matrix but restricts bind/listen, while the [path-scoped ABI-9 alternative](linux-unix9-candidate.md) tests preserving private IP/UNIX functionality without production activation or an approved support-floor change. The [descriptor-pinned broker experiment](linux-socket-broker-candidate.md) now attempts private IP bind/listen/connect with bounded workers and a one-use startup-exec decision. Native and runtime results plus the support/design gate remain required. None closes this task. - [ ] 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. Runtime-selection follow-up for task 2.2: the [production PATH guard](runtime-search.md) diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py index e25bc2fb..39c2c4b3 100644 --- a/scripts/probes/plugin-native-entry/build_socket_candidate.py +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -8,7 +8,7 @@ root = Path(__file__).resolve().parents[3] output = Path(sys.argv[1]).resolve() mode = sys.argv[2] if len(sys.argv) == 3 else "seccomp" -assert mode in {"seccomp", "landlock9", "landlock9-no-resolve"} +assert mode in {"seccomp", "broker", "broker-no-bind", "landlock9", "landlock9-no-resolve"} unix9 = mode.startswith("landlock9") with tempfile.TemporaryDirectory(prefix="cyber-socket-candidate-") as temporary: target = Path(temporary) @@ -16,6 +16,47 @@ shutil.copyfile(root / "crates/cyber-sandbox/native" / name, target / name) family = "linux_socket_landlock9.h" if unix9 else "linux_socket_candidate.h" shutil.copyfile(Path(__file__).with_name(family), target / "socket_candidate.h") + if mode.startswith("broker"): + shutil.copyfile(Path(__file__).with_name("linux_socket_broker.h"), target / "socket_broker.h") + # Preserve family/pair restrictions, replacing the blanket IP server + # denial with the candidate's pinned-object syscall mediation. + socket_header = target / "socket_candidate.h" + text = socket_header.read_text() + for name in ["bind", "listen"]: + if mode == "broker-no-bind" and name == "bind": + continue + line = f" CYBER_DENY(SYS_{name}), \\\n" + assert text.count(line) == 1 + text = text.replace(line, "") + socket_header.write_text(text) + initial_header = target / "linux_initial_exec.h" + text = initial_header.read_text() + # Linux permits only one listener in this inherited filter tree. The + # candidate's parent owns the one-use startup budget and socket service. + before = "static int continue_initial_exec(int listener, pid_t expected) {" + assert text.count(before) == 1 + text = text.split(before)[0] + "#endif\n" + before = "shell ? 0 : SECCOMP_FILTER_FLAG_NEW_LISTENER, &program" + assert text.count(before) == 1 + text = text.replace(before, "SECCOMP_FILTER_FLAG_NEW_LISTENER, &program") + before = " CYBER_ESCAPE_DENIALS,\n" + assert text.count(before) == 1 + notify = "".join( + f" BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_{name}, 0, 1),\n" + " BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),\n" + for name in ["bind", "listen", "connect"]) + initial_header.write_text(text.replace(before, before + notify)) + source = target / "linux_plugin_exec.c" + text = source.read_text() + marker = "int cyber_plugin_exec(int argc, const char *const argv[]) {" + assert text.count(marker) == 1 + prefix, entry = text.split(marker) + boundary = " if (!single_threaded() || confine(argv[1]) != 0) {" + assert entry.count(boundary) == 1 and entry.endswith("}\n") + arguments = entry.split(boundary)[0] + source.write_text(prefix + '#include "socket_broker.h"\n\n' + marker + arguments + + ' if (!single_threaded() || syscall(SYS_close_range, 3U, UINT_MAX, 0) != 0) return 2;\n' + ' return broker_entry(argv, shell);\n}\n') if unix9: shutil.copyfile(Path(__file__).with_name("linux_unix9_policy.h"), target / "unix9_policy.h") if mode == "landlock9-no-resolve": diff --git a/scripts/probes/plugin-native-entry/linux_socket_broker.h b/scripts/probes/plugin-native-entry/linux_socket_broker.h new file mode 100644 index 00000000..f35401f7 --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_socket_broker.h @@ -0,0 +1,281 @@ +/* Isolated experiment only. Included after confine() in a copied C launcher. */ +#ifndef CYBER_SOCKET_BROKER_H +#define CYBER_SOCKET_BROKER_H +#include +#include +#include +#include +#include + +#ifndef PIDFD_THREAD +#define PIDFD_THREAD O_EXCL +#endif +#define BROKER_SLOTS 16 + +struct broker_result { int value; int error; }; +struct broker_job { pid_t pid; int pipe; uint64_t id; }; +struct broker_call { + int socket; + int operation; + int backlog; + socklen_t length; + struct sockaddr_storage address; +}; + +static void broker_tick(int signal_number) { (void)signal_number; } + +static void broker_reply(int listener, uint64_t id, int value, int error) { + struct seccomp_notif_resp response = { .id = id, .val = value, .error = -error }; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == 0) { + /* No CONTINUE: guest pointer/descriptor values are never re-resolved. */ + (void)ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &response); + } +} + +/* Returns a pinned socket, or an errno. No worker reads guest memory. */ +static int broker_copy(int listener, const struct seccomp_notif *request, + struct broker_call *call) { + call->socket = -1; + call->operation = request->data.nr; + if (request->flags != 0 || request->pid == 0 || request->data.arch != CYBER_ARCH || + (call->operation != SYS_bind && call->operation != SYS_listen && + call->operation != SYS_connect)) return EPERM; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &request->id) != 0) return ENOENT; + int task = syscall(SYS_pidfd_open, request->pid, PIDFD_THREAD); + if (task < 0) return errno; + int socket = syscall(SYS_pidfd_getfd, task, (int)request->data.args[0], 0); + int error = errno; + close(task); + if (socket < 0) return error; + int domain = 0, type = 0; + socklen_t size = sizeof(int); + error = EPERM; + if (getsockopt(socket, SOL_SOCKET, SO_DOMAIN, &domain, &size) != 0 || + size != sizeof(int)) goto failed; + size = sizeof(int); + if (getsockopt(socket, SOL_SOCKET, SO_TYPE, &type, &size) != 0 || + size != sizeof(int) || (domain != AF_INET && domain != AF_INET6) || + (type != SOCK_STREAM && type != SOCK_DGRAM)) goto failed; + if (call->operation == SYS_listen) { + if (type != SOCK_STREAM) goto failed; + call->backlog = (int)request->data.args[1]; + } else { + uint64_t length = request->data.args[2]; + error = EINVAL; + if (length < sizeof(sa_family_t) || length > sizeof(call->address)) goto failed; + call->length = (socklen_t)length; + struct iovec local = { .iov_base = &call->address, .iov_len = call->length }; + struct iovec remote = { + .iov_base = (void *)(uintptr_t)request->data.args[1], .iov_len = call->length, + }; + ssize_t copied = process_vm_readv(request->pid, &local, 1, &remote, 1, 0); + if (copied != (ssize_t)call->length) { + error = copied < 0 ? errno : EFAULT; + goto failed; + } + int family = call->address.ss_family; + error = EPERM; + if (family == AF_UNSPEC && call->operation == SYS_connect) { + /* Only IP objects reach here; UNIX pairs cannot disconnect/rebind. */ + } else if (family != domain || + (domain == AF_INET && length < sizeof(struct sockaddr_in)) || + (domain == AF_INET6 && length < sizeof(struct sockaddr_in6))) goto failed; + } + error = ENOENT; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &request->id) != 0) goto failed; + call->socket = socket; + return 0; +failed: + close(socket); + return error; +} + +static void broker_worker(struct broker_call call, int result_pipe, pid_t parent) { + if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) != 0 || getppid() != parent) _exit(125); + /* Duplicate above both fixed slots first, avoiding dup2 alias clobbering. */ + int socket = fcntl(call.socket, F_DUPFD_CLOEXEC, 5); + int output = fcntl(result_pipe, F_DUPFD_CLOEXEC, 5); + if (socket < 0 || output < 0 || dup2(socket, 3) != 3 || dup2(output, 4) != 4 || + syscall(SYS_close_range, 5U, UINT_MAX, 0) != 0) _exit(125); + close(0); close(1); close(2); + int value; + if (call.operation == SYS_bind) value = bind(3, (struct sockaddr *)&call.address, call.length); + else if (call.operation == SYS_listen) value = listen(3, call.backlog); + else value = connect(3, (struct sockaddr *)&call.address, call.length); + struct broker_result result = { .value = value, .error = value < 0 ? errno : 0 }; + ssize_t written; + do { written = write(4, &result, sizeof(result)); } while (written < 0 && errno == EINTR); + _exit(written == (ssize_t)sizeof(result) ? 0 : 125); +} + +static int broker_start(int listener, const struct seccomp_notif *request, + struct broker_job jobs[BROKER_SLOTS]) { + int slot = 0; + while (slot < BROKER_SLOTS && jobs[slot].pid != 0) slot++; + if (slot == BROKER_SLOTS) return EAGAIN; + struct broker_call call = {0}; + int error = broker_copy(listener, request, &call); + if (error != 0) return error; + int channel[2]; + if (pipe2(channel, O_CLOEXEC | O_NONBLOCK) != 0) { + error = errno; + close(call.socket); + return error; + } + pid_t parent = getpid(); + pid_t worker = fork(); + error = errno; + if (worker == 0) broker_worker(call, channel[1], parent); + close(call.socket); + close(channel[1]); + if (worker < 0) { close(channel[0]); return error; } + jobs[slot] = (struct broker_job){ .pid = worker, .pipe = channel[0], .id = request->id }; + return 0; +} + +static void broker_settle_job(int listener, struct broker_job *job, short events) { + if (job->pid == 0) return; + if (job->pipe >= 0) { + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &job->id) != 0) { + /* Identity is an owned, unreaped child, never a cached guest PID. */ + (void)kill(job->pid, SIGKILL); + close(job->pipe); + job->pipe = -1; + } else if (events & (POLLIN | POLLHUP | POLLERR)) { + struct broker_result result; + ssize_t count = read(job->pipe, &result, sizeof(result)); + if (count == (ssize_t)sizeof(result) && + ((result.value == 0 && result.error == 0) || + (result.value == -1 && result.error > 0 && result.error < 4096))) { + broker_reply(listener, job->id, result.value, result.error); + } else broker_reply(listener, job->id, -1, EIO); + close(job->pipe); + job->pipe = -1; + } + } + int status; + /* Drain the result before reaping, including a reply written just after + * poll. An exited worker's pipe will become readable/HUP on the next tick. */ + if (job->pipe < 0 && waitpid(job->pid, &status, WNOHANG) == job->pid) { + *job = (struct broker_job){0}; + } +} + +static void broker_reap(pid_t child) { + int status; + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} +} + +static int broker_loop(int listener, pid_t guest, int shell) { + struct seccomp_notif_sizes sizes; + if (syscall(SYS_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, &sizes) != 0 || + sizes.seccomp_notif < sizeof(struct seccomp_notif)) { + (void)kill(guest, SIGKILL); broker_reap(guest); return 2; + } + struct seccomp_notif *request = calloc(1, sizes.seccomp_notif); + if (request == NULL) { (void)kill(guest, SIGKILL); broker_reap(guest); return 2; } + struct broker_job jobs[BROKER_SLOTS] = {0}; + struct sigaction action = { .sa_handler = broker_tick }; + sigemptyset(&action.sa_mask); + int guest_reaped = 0, status = 0, result = 2; + int initial_exec = !shell; + if (sigaction(SIGALRM, &action, NULL) != 0) goto done; + for (;;) { + pid_t observed = waitpid(guest, &status, WNOHANG); + if (observed == guest) { + guest_reaped = 1; + result = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); + break; + } + if (observed < 0) { + if (errno == EINTR) continue; + if (errno == ECHILD) guest_reaped = 1; + goto done; + } + struct pollfd pending[BROKER_SLOTS + 1] = {{ .fd = listener, .events = POLLIN }}; + for (int i = 0; i < BROKER_SLOTS; i++) pending[i + 1] = + (struct pollfd){ .fd = jobs[i].pid == 0 ? -1 : jobs[i].pipe, .events = POLLIN }; + int ready = poll(pending, BROKER_SLOTS + 1, 100); + if (ready < 0 && errno != EINTR) goto done; + for (int i = 0; i < BROKER_SLOTS; i++) broker_settle_job(listener, &jobs[i], pending[i + 1].revents); + if (pending[0].revents & (POLLERR | POLLNVAL)) goto done; + if (pending[0].revents & POLLIN) { + memset(request, 0, sizes.seccomp_notif); + /* A signal may cancel a request between poll and RECV. Bound that + * blocking receive without assuming O_NONBLOCK ioctl semantics. */ + alarm(1); + int received = ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, request); + int error = errno; + alarm(0); + if (received != 0) { + if (error == EINTR || error == ENOENT) continue; + goto done; + } + if (request->data.nr == SYS_execve) { + if (initial_exec && request->pid == (unsigned int)guest && + request->flags == 0 && request->data.arch == CYBER_ARCH && + ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, &request->id) == 0) { + /* The dedicated C guest has not executed any untrusted + * instruction or created a thread. Burn this budget before + * replying; no later request can acquire startup authority. */ + initial_exec = 0; + struct seccomp_notif_resp response = { + .id = request->id, .flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE, + }; + if (ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &response) != 0) goto done; + } else broker_reply(listener, request->id, -1, ENOSYS); + continue; + } + error = broker_start(listener, request, jobs); + if (error != 0) broker_reply(listener, request->id, -1, error); + } + } +done: + alarm(0); + for (int i = 0; i < BROKER_SLOTS; i++) { + if (jobs[i].pid == 0) continue; + (void)kill(jobs[i].pid, SIGKILL); + broker_reap(jobs[i].pid); + if (jobs[i].pipe >= 0) close(jobs[i].pipe); + } + if (!guest_reaped) { (void)kill(guest, SIGKILL); broker_reap(guest); } + free(request); + return result; +} + +static int broker_entry(const char *const argv[], int shell) { + int channel[2]; + struct sigaction child_action = { .sa_handler = SIG_DFL }; + sigemptyset(&child_action.sa_mask); + sigset_t required_signals; + sigemptyset(&required_signals); + sigaddset(&required_signals, SIGALRM); + sigaddset(&required_signals, SIGCHLD); + if (sigprocmask(SIG_UNBLOCK, &required_signals, NULL) != 0 || + sigaction(SIGCHLD, &child_action, NULL) != 0 || + prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) != 0 || + socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, channel) != 0) return 2; + pid_t parent = getpid(); + pid_t guest = fork(); + if (guest < 0) { close(channel[0]); close(channel[1]); return 2; } + if (guest == 0) { + close(channel[0]); + if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) != 0 || getppid() != parent || + prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) != 0 || confine(argv[1]) != 0) _exit(2); + int listener = initial_filter(shell); + int sent = listener < 0 ? -1 : transfer_listener(channel[1], listener); + if (listener >= 0) close(listener); + close(channel[1]); + if (sent != 0) _exit(2); + execv(argv[3], (char *const *)(argv + 3)); + _exit(2); + } + close(channel[1]); close(0); close(1); + int listener = receive_listener(channel[0]); + close(channel[0]); + if (listener < 0) { (void)kill(guest, SIGKILL); broker_reap(guest); return 2; } + int result = broker_loop(listener, guest, shell); + close(listener); + return result; +} +#endif From fb5a5a13118013f8db7be0b850bc126add20d763 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:18:09 -0400 Subject: [PATCH 39/50] docs: retain source-bound socket broker results --- .../linux-socket-broker-candidate.md | 54 +- .../complete-plugin-hook-runtime/tasks.md | 7 +- .../arm64/broker-no-bind.log | 6 + .../arm64/bun.json | 485 ++++++++++++ .../arm64/deno.json | 436 +++++++++++ .../arm64/linux-ipc-broker.json | 690 ++++++++++++++++++ .../arm64/native.json | 472 ++++++++++++ .../arm64/node.json | 436 +++++++++++ .../arm64/python.json | 427 +++++++++++ .../x64/broker-no-bind.log | 6 + .../linux-socket-broker-f84c3942/x64/bun.json | 485 ++++++++++++ .../x64/deno.json | 436 +++++++++++ .../x64/linux-ipc-broker.json | 690 ++++++++++++++++++ .../x64/native.json | 472 ++++++++++++ .../x64/node.json | 436 +++++++++++ .../x64/python.json | 427 +++++++++++ 16 files changed, 5959 insertions(+), 6 deletions(-) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/broker-no-bind.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/linux-ipc-broker.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/python.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/broker-no-bind.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/linux-ipc-broker.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/python.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md index e4af898e..56162887 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -64,21 +64,32 @@ denial in the otherwise identical candidate. It must fail the actual TCP token exchange assertion. A missing server capability therefore cannot count as this candidate's successful confinement/compatibility result. +The next real-notification case creates a pthread that detaches its file table +using close_range with CLOSE_RANGE_UNSHARE. Its leader retains a UNIX socket at +the same descriptor number where that worker installs an IP server. The worker +must perform a full TCP token exchange through the broker; after joining, the +leader must still see its original UNIX object. This covers the broker's real +thread selection, beyond the earlier standalone prerequisite fixture. A copied +control removes PIDFD_THREAD and must fail this assertion while the ordinary +single-thread TCP exchange still succeeds. Descriptor/address mutation during +an already admitted operation and cancellation/saturation cases remain open. + The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) requires copied tracee arguments to avoid policy decisions on mutable memory. This experiment additionally pins the actual socket object and emulates the operation instead of using CONTINUE. Native checks must establish its behavior; source review and successful compilation are insufficient acceptance evidence. -Local verification on macOS: all five builder modes produce their expected +Local verification on macOS: builder modes produce their expected temporary source transformations without changing production native source hashes; this check intercepts the compiler and does not compile Linux C. The Rust diagnostic example passes Clippy with Rust warnings denied. Workflow YAML, -ten shell blocks and embedded Python parse; formatting/diff checks, all 107 +shell blocks and embedded Python parse; formatting/diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, -kernel behavior, the positive TCP cases, missing-bind control and runtime matrix -remain unverified until the corresponding hosted x64/arm64 jobs complete. +kernel behavior for each revision requires corresponding hosted x64/arm64 jobs. +The new thread-specific case/control and corrected runtime report label still +require fresh hosted results; earlier receipts below cannot prove those changes. Scoped source/security review rejected the nested-listener design, corrected a potential second signal after guest reaping, drains worker replies before @@ -86,3 +97,38 @@ reaping their identities, unblocks the receive-watchdog signal, and ensures the child closes bootstrap/listener descriptors before executing target code. These source corrections are not native evidence. In particular the unified startup decision and adversarial lifetime cases remain explicit review gates. + +## First native candidate receipts + +[Run 41006793275](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41006793275) +completed successfully on x64 (job 123903409605) and arm64 (job 123903409514). +The retained [x64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/linux-ipc-broker.json) +and [arm64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/linux-ipc-broker.json) +reports each contain 32 socket cases: 16 direct ambient-endpoint denials and 16 +pair cases requiring private stream-pair communication and an actual IP server/ +client exchange across all four read/write combinations. Forbidden UNIX pair +bind/listen/reconnect and other pair types remain denied in this candidate. +Both missing-bind controls fail the named TCP assertion. These are fixture +messages only, with no ambient service or credential access. + +Merge checkout `23eb5e490ffbc9733bac39d187e9d10c680ca05e` binds source SHA-256 +`fdbae19e0c20cc4b0035f7f2399284260eb896f3be2da6b219f5174d74720f22`. +It was independently recomputed from that checkout before retaining the reports. +The recorded helper hashes are x64 +`4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179` +and arm64 `c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d`. +Each architecture's five adjacent runtime JSON reports contain nine cases per +runtime (including its unconfined control), with no unsupported cases. Their +input/executed-helper hashes match that architecture's socket report. The runtime +source manifest hash `07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a` +was also independently recomputed. All report kernel `7.0.0-1017-azure`. + +The retained runtime reports are unmodified originals. Their shared driver's +static mechanism label incorrectly says the listener is closed; that statement +describes the exec-only launcher and is not evidence about this broker. Here the +listener stays live for socket operations and startup authority is a consumed +parent budget. The next workflow passes an explicit socket-broker reporting flag +to label that mechanism accurately. Source/binary bindings and observed cases +support only the specific tested candidate, not that inaccurate label or the +unperformed adversarial cases. Production admission and support/design review +remain open. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index e87dfa6c..958ea593 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -33,10 +33,13 @@ scratch through asynchronous cleanup and unconfirmed waits while revoking its proxy. Native containment, staging ancestry, quarantine and generation/retry fencing still gate complete host lifetime acceptance. -The next compatible Linux IPC investigation starts with [descriptor-pinning +The compatible Linux IPC investigation includes [descriptor-pinning prerequisites](linux-pidfd-proof.md), including independent thread file tables, descriptor-number replacement, copied arguments and actual private TCP exchange. -This experiment changes no production policy or accepted support floor. +The [broker candidate](linux-socket-broker-candidate.md) has source-bound initial +socket/runtime receipts on x64 and arm64; its real thread-local descriptor case +and precise missing-thread control still require fresh native execution. These +experiments change no production policy or accepted support floor. ## 3. Host contribution lifecycle diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/broker-no-bind.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/broker-no-bind.log new file mode 100644 index 00000000..a9bfcf62 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/broker-no-bind.log @@ -0,0 +1,6 @@ + +thread 'main' (3832) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:126:9: +assertion `left == right` failed: IP server/client token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/bun.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/bun.json new file mode 100644 index 00000000..9390635c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/bun.json @@ -0,0 +1,485 @@ +{ + "recorded_at": "2026-10-30T05:10:19.472Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "arm64", + "runtime": "bun", + "version": "1.4.4", + "startup_control": { + "preload": true, + "dotenv": true + }, + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false, + "proc_root_reads": [ + true, + true + ], + "proc_init_list": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/deno.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/deno.json new file mode 100644 index 00000000..ebc5391b --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/deno.json @@ -0,0 +1,436 @@ +{ + "recorded_at": "2026-10-30T05:10:21.712Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "arm64", + "runtime": "deno", + "version": "2.9.8", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/linux-ipc-broker.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/linux-ipc-broker.json new file mode 100644 index 00000000..170a206c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/linux-ipc-broker.json @@ -0,0 +1,690 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "23eb5e490ffbc9733bac39d187e9d10c680ca05e", + "run_id": "41006793275", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "fdbae19e0c20cc4b0035f7f2399284260eb896f3be2da6b219f5174d74720f22", + "helper_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/native.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/native.json new file mode 100644 index 00000000..11db508e --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/native.json @@ -0,0 +1,472 @@ +{ + "recorded_at": "2026-10-30T05:10:22.088Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "arm64", + "runtime": "native", + "version": "fixture-v2", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "private_read": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "symlink_private_read": true, + "hardlink_private": true, + "project_rename": true, + "inherited_descriptor": true, + "thread": true, + "package_hardlink": true, + "package_alias_write": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "cannot_relax": null, + "constructor_private_read": true, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "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": 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 + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "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 + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "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": 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": 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 + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "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 + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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 + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "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": 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 + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/node.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/node.json new file mode 100644 index 00000000..9b7fee15 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/node.json @@ -0,0 +1,436 @@ +{ + "recorded_at": "2026-10-30T05:10:17.612Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "arm64", + "runtime": "node", + "version": "22.23.4", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/python.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/python.json new file mode 100644 index 00000000..9c346507 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/arm64/python.json @@ -0,0 +1,427 @@ +{ + "recorded_at": "2026-10-30T05:10:18.574Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "arm64", + "runtime": "python", + "version": "3.13.17", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "cannot_relax": null, + "child_reads": [ + true, + true + ], + "filter_replacement_denied": false, + "hardlink_private": true, + "inherited_descriptor": true, + "native_child_reads": [ + true, + true + ], + "native_fork": true, + "package_read": true, + "private_list": true, + "private_read": true, + "private_write": true, + "project_list": true, + "project_read": true, + "project_rename": true, + "project_write": true, + "protected_read": true, + "protected_write": true, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/broker-no-bind.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/broker-no-bind.log new file mode 100644 index 00000000..839f51c1 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/broker-no-bind.log @@ -0,0 +1,6 @@ + +thread 'main' (3457) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:126:9: +assertion `left == right` failed: IP server/client token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/bun.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/bun.json new file mode 100644 index 00000000..954e4905 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/bun.json @@ -0,0 +1,485 @@ +{ + "recorded_at": "2026-10-30T05:10:12.558Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "x64", + "runtime": "bun", + "version": "1.4.4", + "startup_control": { + "preload": true, + "dotenv": true + }, + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false, + "proc_root_reads": [ + true, + true + ], + "proc_init_list": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/deno.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/deno.json new file mode 100644 index 00000000..5fbccf51 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/deno.json @@ -0,0 +1,436 @@ +{ + "recorded_at": "2026-10-30T05:10:15.302Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "x64", + "runtime": "deno", + "version": "2.9.8", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/linux-ipc-broker.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/linux-ipc-broker.json new file mode 100644 index 00000000..7dcd1266 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/linux-ipc-broker.json @@ -0,0 +1,690 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "23eb5e490ffbc9733bac39d187e9d10c680ca05e", + "run_id": "41006793275", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "fdbae19e0c20cc4b0035f7f2399284260eb896f3be2da6b219f5174d74720f22", + "helper_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/native.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/native.json new file mode 100644 index 00000000..1b0464c8 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/native.json @@ -0,0 +1,472 @@ +{ + "recorded_at": "2026-10-30T05:10:15.656Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "x64", + "runtime": "native", + "version": "fixture-v2", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "private_read": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "symlink_private_read": true, + "hardlink_private": true, + "project_rename": true, + "inherited_descriptor": true, + "thread": true, + "package_hardlink": true, + "package_alias_write": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "cannot_relax": null, + "constructor_private_read": true, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "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": 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 + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "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 + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "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": 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": 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 + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "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 + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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 + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "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": 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 + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/node.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/node.json new file mode 100644 index 00000000..12886f19 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/node.json @@ -0,0 +1,436 @@ +{ + "recorded_at": "2026-10-30T05:10:10.744Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "x64", + "runtime": "node", + "version": "22.23.4", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/python.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/python.json new file mode 100644 index 00000000..e59d095f --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-f84c3942/x64/python.json @@ -0,0 +1,427 @@ +{ + "recorded_at": "2026-10-30T05:10:11.717Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "07a4cdbfba834bb2c367745240a1cfd924a279954692ba796c48b59b629b011a", + "platform": "linux", + "architecture": "x64", + "runtime": "python", + "version": "3.13.17", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "mechanism": "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "cannot_relax": null, + "child_reads": [ + true, + true + ], + "filter_replacement_denied": false, + "hardlink_private": true, + "inherited_descriptor": true, + "native_child_reads": [ + true, + true + ], + "native_fork": true, + "package_read": true, + "private_list": true, + "private_read": true, + "private_write": true, + "project_list": true, + "project_read": true, + "project_rename": true, + "project_write": true, + "protected_read": true, + "protected_write": true, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + } + ] +} From 9de3bf7759896a6249d1184cce41f58929567127 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:18:09 -0400 Subject: [PATCH 40/50] test(plugins): exercise broker thread-local descriptor tables --- .github/workflows/plugin-linux-ipc.yml | 18 ++++++ .../examples/plugin_unix_socket_probe.rs | 19 +++++-- .../tests/fixtures/plugin_unix_socket.c | 55 ++++++++++++++++--- scripts/probes/plugin-confinement/run.mts | 6 +- .../build_socket_candidate.py | 8 ++- 5 files changed, 91 insertions(+), 15 deletions(-) diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 76acdac9..e950b690 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -79,6 +79,7 @@ jobs: python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-candidate" python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker" broker python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-bind" broker-no-bind + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-thread" broker-no-thread python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-candidate" landlock9 python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-missing-resolve" landlock9-no-resolve cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe @@ -104,6 +105,21 @@ jobs: assert 'IP server/client token exchange' in text, text assert '"tcp_roundtrip":false' in text, text PY + - name: Reject missing thread-specific descriptor ownership + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-no-thread" "$RUNNER_TEMP/linux-ipc-broker-thread-control.json" --broker > "$RUNNER_TEMP/broker-no-thread.log" 2>&1; then + echo 'Missing thread-specific pidfd unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/broker-no-thread.log" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'thread-local IP descriptor token exchange' in text, text + assert '"tcp_roundtrip":true' in text and '"thread_tcp_roundtrip":false' in text, text + PY - name: Record pathname policy kernel availability id: abi shell: bash @@ -187,6 +203,7 @@ jobs: if [[ "$policy" == unix9 ]]; then helper="$RUNNER_TEMP/unix9-candidate"; fi for runtime in node python bun deno native; do args=(--runtime "$runtime" --initial-exec --packaged-helper "$helper" --report "$RUNNER_TEMP/socket-runtime/$policy/$runtime.json" --trace "$RUNNER_TEMP/socket-runtime/$policy/trace-$runtime") + if [[ "$policy" == broker ]]; then args+=(--socket-broker); fi if [[ "$policy" == unix9 ]]; then args+=(--unix-scratch-policy); fi if [[ "$runtime" == python ]]; then args+=(--python "$(command -v python3)") @@ -211,5 +228,6 @@ jobs: ${{ runner.temp }}/unix9-missing-resolve.log ${{ runner.temp }}/unix9-unavailable.log ${{ runner.temp }}/broker-no-bind.log + ${{ runner.temp }}/broker-no-thread.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index ec50eab2..1d0866f0 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -116,6 +116,14 @@ mod probe { "private pair escape: {key}: {observed}" ); } + check_pair_support(&observed, mode); + let delivered = receipt(listener, &observed); + assert!(!delivered, "pair reached host: {observed}"); + json!({"socket": label, "fs_read": pair_policy.fs_read, "fs_write": pair_policy.fs_write, + "shell": false, "mode": "pair", "host_received_fixture": delivered, "observed": observed}) + } + + fn check_pair_support(observed: &Value, mode: Mode) { for key in ["datagram_pair", "seqpacket_pair", "scratch_unix_roundtrip"] { assert_eq!( observed[key], @@ -128,13 +136,14 @@ mod probe { matches!(mode, Mode::Unix9 | Mode::Broker), "IP server/client token exchange: {observed}" ); + assert_eq!( + observed["thread_tcp_roundtrip"], + matches!(mode, Mode::Unix9 | Mode::Broker), + "thread-local IP descriptor token exchange: {observed}" + ); if matches!(mode, Mode::Seccomp | Mode::Broker) { assert_eq!(observed["pair_listen"], false, "pair listener: {observed}"); } - let delivered = receipt(listener, &observed); - assert!(!delivered, "pair reached host: {observed}"); - json!({"socket": label, "fs_read": pair_policy.fs_read, "fs_write": pair_policy.fs_write, - "shell": false, "mode": "pair", "host_received_fixture": delivered, "observed": observed}) } fn options() -> (PathBuf, PathBuf, Mode) { @@ -178,7 +187,7 @@ mod probe { run(Command::new("cc") .env_clear() .env("PATH", "/usr/bin:/bin") - .args(["-Wall", "-Wextra", "-Werror"]) + .args(["-Wall", "-Wextra", "-Werror", "-pthread"]) .arg(workspace.join("crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c")) .arg("-o") .arg(&target)); diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 075e44dd..4fba0e2e 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -1,4 +1,5 @@ /* Fixture-only connection probe. It never contacts a non-fixture service. */ +#define _GNU_SOURCE #include #include #include @@ -9,7 +10,9 @@ #include #include #include +#include #ifdef __linux__ +#include #include #include #endif @@ -22,8 +25,8 @@ static int abi(void) { #endif } -static int local_exchange(int family, const char *path) { - int server = -1, client = -1, peer = -1, ok = 0; +static int local_exchange(int family, const char *path, int server) { + int client = -1, peer = -1, ok = 0; struct sockaddr_storage storage = {0}; socklen_t length; if (family == AF_UNIX) { @@ -38,7 +41,7 @@ static int local_exchange(int family, const char *path) { address->sin_addr.s_addr = htonl(INADDR_LOOPBACK); length = sizeof(*address); } - server = socket(family, SOCK_STREAM, 0); + if (server < 0) server = socket(family, SOCK_STREAM, 0); if (server < 0 || bind(server, (struct sockaddr *)&storage, length) != 0 || listen(server, 1) != 0 || getsockname(server, (struct sockaddr *)&storage, &length) != 0) goto done; client = socket(family, SOCK_STREAM, 0); @@ -55,6 +58,43 @@ static int local_exchange(int family, const char *path) { return ok; } +#ifdef __linux__ +struct thread_exchange { int descriptor; int roundtrip; }; +static void *thread_exchange(void *argument) { + struct thread_exchange *test = argument; + /* A real runtime may detach its FD table without unshare(2). Leave the + * leader's UNIX socket at this number while this thread installs an IP + * socket there. close_range UNSHARE closes only this thread's unused stdin. */ + if (syscall(SYS_close_range, 0U, 0U, CLOSE_RANGE_UNSHARE) != 0) return NULL; + int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) return NULL; + if (dup2(fd, test->descriptor) != test->descriptor) { close(fd); return NULL; } + if (fd != test->descriptor) close(fd); + test->roundtrip = local_exchange(AF_INET, NULL, test->descriptor); + return NULL; +} + +static int thread_local_exchange(void) { + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) return 0; + int descriptor = fcntl(pair[0], F_DUPFD_CLOEXEC, 128); + close(pair[0]); + if (descriptor < 0) { close(pair[1]); return 0; } + struct thread_exchange test = { .descriptor = descriptor }; + pthread_t thread; + int ok = pthread_create(&thread, NULL, thread_exchange, &test) == 0; + if (ok && pthread_join(thread, NULL) != 0) _exit(3); + int domain = 0; + socklen_t length = sizeof(domain); + ok = ok && test.roundtrip && getsockopt(descriptor, SOL_SOCKET, SO_DOMAIN, &domain, &length) == 0 && + length == sizeof(domain) && domain == AF_UNIX; + close(descriptor); close(pair[1]); + return ok; +} +#else +static int thread_local_exchange(void) { return 0; } +#endif + static int pair_probe(const struct sockaddr_un *address, socklen_t length, const char *data_path, const char *scratch_socket) { int pair[2]; @@ -91,20 +131,21 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, char byte; int readable = data >= 0 && read(data, &byte, 1) == 1; if (data >= 0) close(data); - int tcp = scratch_socket != NULL && local_exchange(AF_INET, NULL); - int scratch_ipc = scratch_socket != NULL && local_exchange(AF_UNIX, scratch_socket); + int tcp = scratch_socket != NULL && local_exchange(AF_INET, NULL, -1); + int thread_tcp = scratch_socket != NULL && thread_local_exchange(); + int scratch_ipc = scratch_socket != NULL && local_exchange(AF_UNIX, scratch_socket, -1); printf("{\"started\":true,\"connected\":%s,\"sent\":%s,\"project_read\":%s," "\"pair_roundtrip\":%s,\"pair_disconnect\":%s,\"pair_reconnect\":%s," "\"pair_bind\":%s,\"pair_listen\":%s,\"pair_sendto\":%s,\"pair_sendmsg\":%s," "\"datagram_pair\":%s,\"seqpacket_pair\":%s,\"landlock_abi\":%d," - "\"tcp_roundtrip\":%s,\"scratch_unix_roundtrip\":%s}\n", + "\"tcp_roundtrip\":%s,\"thread_tcp_roundtrip\":%s,\"scratch_unix_roundtrip\":%s}\n", (connected || reconnected) ? "true" : "false", sent ? "true" : "false", readable ? "true" : "false", roundtrip ? "true" : "false", disconnected ? "true" : "false", reconnected ? "true" : "false", bound ? "true" : "false", listening ? "true" : "false", sent_to ? "true" : "false", sent_message ? "true" : "false", datagram ? "true" : "false", sequence ? "true" : "false", abi(), - tcp ? "true" : "false", scratch_ipc ? "true" : "false"); + tcp ? "true" : "false", thread_tcp ? "true" : "false", scratch_ipc ? "true" : "false"); return 0; } diff --git a/scripts/probes/plugin-confinement/run.mts b/scripts/probes/plugin-confinement/run.mts index ae0ce9a8..40ddc980 100644 --- a/scripts/probes/plugin-confinement/run.mts +++ b/scripts/probes/plugin-confinement/run.mts @@ -364,7 +364,7 @@ function check(root: string, artifacts: Artifacts, runtime: Runtime, name: strin function main() { assert(["darwin", "linux"].includes(process.platform), "native proof requires macOS or Linux; other platforms are unproved"); - const { values } = parseArgs({ options: { runtime: { type: "string", default: "python" }, "packaged-helper": { type: "string" }, python: { type: "string" }, executable: { type: "string" }, report: { type: "string" }, trace: { type: "string" }, "initial-exec": { type: "boolean", default: false }, "unix-scratch-policy": { type: "boolean", default: false }, "macos-staged-native": { type: "boolean", default: false }, "macos-signing-native": { type: "boolean", default: false }, "macos-signing-runtime": { type: "boolean", default: false } } }); + const { values } = parseArgs({ options: { runtime: { type: "string", default: "python" }, "packaged-helper": { type: "string" }, python: { type: "string" }, executable: { type: "string" }, report: { type: "string" }, trace: { type: "string" }, "initial-exec": { type: "boolean", default: false }, "unix-scratch-policy": { type: "boolean", default: false }, "socket-broker": { type: "boolean", default: false }, "macos-staged-native": { type: "boolean", default: false }, "macos-signing-native": { type: "boolean", default: false }, "macos-signing-runtime": { type: "boolean", default: false } } }); assert(["python", "node", "bun", "deno", "native"].includes(values.runtime), "runtime must be python, node, bun, deno or native"); if (values.runtime === "bun" || values.runtime === "deno") assert(values.executable, "supply an explicit trusted Bun/Deno executable"); const stagedPlatform = values["macos-staged-native"]; @@ -382,6 +382,7 @@ function main() { assert(!initialExec || isLinux, "initial-exec proof currently requires Linux"); if (values.runtime === "python") assert(values.python, "supply an explicit trusted Python; embedding additionally requires development headers and a dynamic library"); assert(!values["unix-scratch-policy"] || (isLinux && initialExec && packagedHelper), "unix scratch experiment requires a Linux packaged initial-exec helper"); + assert(!values["socket-broker"] || (isLinux && initialExec && packagedHelper && !values["unix-scratch-policy"]), "socket broker experiment requires a Linux packaged initial-exec helper without the UNIX-9 alternative"); const root = realpathSync(mkdtempSync(join(tmpdir(), "cyber-plugin-native-proof-"))); try { const runtime: Runtime = values.runtime === "node" ? nodeInfo() : values.runtime === "bun" || values.runtime === "deno" @@ -433,7 +434,8 @@ function main() { executed_sha256: createHash("sha256").update(readFileSync(artifacts.executable)).digest("hex"), private_copy: true, direct_runtime: true, } : undefined, - mechanism: signingRuntime ? "fresh signing identifier on trusted launcher + exact-target Seatbelt exec rule before any runtime instruction; target signature unchanged; experimental subject-identity candidate" : stagedPlatform ? "ad-hoc-staged native image + system platform launcher Seatbelt policy before first target instruction; platform-subject exact-path exec admission" : initialExec ? "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter" : isLinux ? "bubblewrap private mounts + Landlock before runtime startup + synchronized seccomp before fixture execution" : "trusted native/runtime bootstrap followed by native sandbox_init before fixture execution", + socket_broker_candidate: values["socket-broker"], + mechanism: values["socket-broker"] ? "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter" : signingRuntime ? "fresh signing identifier on trusted launcher + exact-target Seatbelt exec rule before any runtime instruction; target signature unchanged; experimental subject-identity candidate" : stagedPlatform ? "ad-hoc-staged native image + system platform launcher Seatbelt policy before first target instruction; platform-subject exact-path exec admission" : initialExec ? "bubblewrap private mounts + Landlock/seccomp before first target instruction + one-shot exec admission; closed listener and immutable filter" : isLinux ? "bubblewrap private mounts + Landlock before runtime startup + synchronized seccomp before fixture execution" : "trusted native/runtime bootstrap followed by native sandbox_init before fixture execution", limitation: packagedHelper ? "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required" : "experimental native mechanisms; macOS uses deprecated Seatbelt API; not production packaging or Windows acceptance", unsupported_cases, rows }; if (values.report) writeFileSync(values.report, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 }); console.log(JSON.stringify(report, null, 2)); diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py index 39c2c4b3..34df365b 100644 --- a/scripts/probes/plugin-native-entry/build_socket_candidate.py +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -8,7 +8,7 @@ root = Path(__file__).resolve().parents[3] output = Path(sys.argv[1]).resolve() mode = sys.argv[2] if len(sys.argv) == 3 else "seccomp" -assert mode in {"seccomp", "broker", "broker-no-bind", "landlock9", "landlock9-no-resolve"} +assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "landlock9", "landlock9-no-resolve"} unix9 = mode.startswith("landlock9") with tempfile.TemporaryDirectory(prefix="cyber-socket-candidate-") as temporary: target = Path(temporary) @@ -18,6 +18,12 @@ shutil.copyfile(Path(__file__).with_name(family), target / "socket_candidate.h") if mode.startswith("broker"): shutil.copyfile(Path(__file__).with_name("linux_socket_broker.h"), target / "socket_broker.h") + if mode == "broker-no-thread": + broker = target / "socket_broker.h" + text = broker.read_text() + before = "syscall(SYS_pidfd_open, request->pid, PIDFD_THREAD)" + assert text.count(before) == 1 + broker.write_text(text.replace(before, "syscall(SYS_pidfd_open, request->pid, 0)")) # Preserve family/pair restrictions, replacing the blanket IP server # denial with the candidate's pinned-object syscall mediation. socket_header = target / "socket_candidate.h" From 9d0e205c1a4bfbd704e195160b2c79d67c7deda0 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:40:02 -0400 Subject: [PATCH 41/50] test(hooks): hold only the callback exercised by shutdown --- crates/cyber-tools/tests/file_observation_dispatch.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/cyber-tools/tests/file_observation_dispatch.rs b/crates/cyber-tools/tests/file_observation_dispatch.rs index 3d6d463e..d370b25f 100644 --- a/crates/cyber-tools/tests/file_observation_dispatch.rs +++ b/crates/cyber-tools/tests/file_observation_dispatch.rs @@ -1002,7 +1002,10 @@ async fn known_private_subtree_retires_old_public_descendants_without_callbacks( async fn shutdown_settles_a_held_observer_callback_and_restart_does_not_replay_it() { let mut flow = support::flow::Flow::new(vec![support::flow::text("Session reopened")], false); let gate = Arc::new(tokio::sync::Notify::new()); - let recorder = Recorder::with_gate(Some(Arc::clone(&gate))).await; + // Hold only the callback whose cancellation is under test. A new observer + // may reconcile other paths before fresh-after-restart; those callbacks + // must not consume another gate wait and stall serial observation delivery. + let recorder = Recorder::with_first_gate(Arc::clone(&gate)).await; automatic_hooks(&flow, &recorder); let session = flow.session("default").await; await_condition("held-callback baseline", || { From 51bf55529050aeb189bc731a635ef107c2bc21b4 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:40:02 -0400 Subject: [PATCH 42/50] docs: retain broker thread-selection evidence --- .../arm64/broker-no-bind.log | 6 + .../arm64/broker-no-thread.log | 6 + .../arm64/bun.json | 486 ++++++++++++ .../arm64/deno.json | 437 +++++++++++ .../arm64/linux-ipc-broker.json | 706 ++++++++++++++++++ .../arm64/native.json | 473 ++++++++++++ .../arm64/node.json | 437 +++++++++++ .../arm64/python.json | 428 +++++++++++ .../x64/broker-no-bind.log | 6 + .../x64/broker-no-thread.log | 6 + .../linux-socket-broker-9de3bf77/x64/bun.json | 486 ++++++++++++ .../x64/deno.json | 437 +++++++++++ .../x64/linux-ipc-broker.json | 706 ++++++++++++++++++ .../x64/native.json | 473 ++++++++++++ .../x64/node.json | 437 +++++++++++ .../x64/python.json | 428 +++++++++++ 16 files changed, 5958 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-bind.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-thread.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/linux-ipc-broker.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/python.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-bind.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-thread.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/bun.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/deno.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/linux-ipc-broker.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/native.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/node.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/python.json diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-bind.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-bind.log new file mode 100644 index 00000000..f17e23e3 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-bind.log @@ -0,0 +1,6 @@ + +thread 'main' (3727) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:134:9: +assertion `left == right` failed: IP server/client token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":false,"thread_tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-thread.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-thread.log new file mode 100644 index 00000000..aac30017 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/broker-no-thread.log @@ -0,0 +1,6 @@ + +thread 'main' (3745) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:139:9: +assertion `left == right` failed: thread-local IP descriptor token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":true,"thread_tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/bun.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/bun.json new file mode 100644 index 00000000..265db6f9 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/bun.json @@ -0,0 +1,486 @@ +{ + "recorded_at": "2026-10-30T05:19:36.414Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "arm64", + "runtime": "bun", + "version": "1.4.4", + "startup_control": { + "preload": true, + "dotenv": true + }, + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false, + "proc_root_reads": [ + true, + true + ], + "proc_init_list": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/deno.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/deno.json new file mode 100644 index 00000000..6aa3bdc5 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/deno.json @@ -0,0 +1,437 @@ +{ + "recorded_at": "2026-10-30T05:19:38.791Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "arm64", + "runtime": "deno", + "version": "2.9.8", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/linux-ipc-broker.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/linux-ipc-broker.json new file mode 100644 index 00000000..1b7c0f0a --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/linux-ipc-broker.json @@ -0,0 +1,706 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "b1a06c56cff5caab2fd2635d7b43a717a9edef2f", + "run_id": "41007688907", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "6b5a2468c8f0e007d5399f5c7878bf98af37c6a4ab4a509d17c74139784f38a7", + "helper_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/native.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/native.json new file mode 100644 index 00000000..58683b2c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/native.json @@ -0,0 +1,473 @@ +{ + "recorded_at": "2026-10-30T05:19:39.180Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "arm64", + "runtime": "native", + "version": "fixture-v2", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "private_read": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "symlink_private_read": true, + "hardlink_private": true, + "project_rename": true, + "inherited_descriptor": true, + "thread": true, + "package_hardlink": true, + "package_alias_write": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "cannot_relax": null, + "constructor_private_read": true, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "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": 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 + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "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 + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "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": 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": 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 + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "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 + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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 + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "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": 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 + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/node.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/node.json new file mode 100644 index 00000000..12ef2e1b --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/node.json @@ -0,0 +1,437 @@ +{ + "recorded_at": "2026-10-30T05:19:34.466Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "arm64", + "runtime": "node", + "version": "22.23.4", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/python.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/python.json new file mode 100644 index 00000000..2eb0bb4b --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/python.json @@ -0,0 +1,428 @@ +{ + "recorded_at": "2026-10-30T05:19:35.464Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "arm64", + "runtime": "python", + "version": "3.13.17", + "packaged_helper": { + "input_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "executed_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "cannot_relax": null, + "child_reads": [ + true, + true + ], + "filter_replacement_denied": false, + "hardlink_private": true, + "inherited_descriptor": true, + "native_child_reads": [ + true, + true + ], + "native_fork": true, + "package_read": true, + "private_list": true, + "private_read": true, + "private_write": true, + "project_list": true, + "project_read": true, + "project_rename": true, + "project_write": true, + "protected_read": true, + "protected_write": true, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-bind.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-bind.log new file mode 100644 index 00000000..57b6ce46 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-bind.log @@ -0,0 +1,6 @@ + +thread 'main' (3461) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:134:9: +assertion `left == right` failed: IP server/client token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":false,"thread_tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-thread.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-thread.log new file mode 100644 index 00000000..c2805ba5 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/broker-no-thread.log @@ -0,0 +1,6 @@ + +thread 'main' (3479) panicked at crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs:139:9: +assertion `left == right` failed: thread-local IP descriptor token exchange: {"started":true,"connected":false,"sent":false,"project_read":false,"pair_roundtrip":true,"pair_disconnect":false,"pair_reconnect":false,"pair_bind":false,"pair_listen":false,"pair_sendto":false,"pair_sendmsg":false,"datagram_pair":false,"seqpacket_pair":false,"landlock_abi":8,"tcp_roundtrip":true,"thread_tcp_roundtrip":false,"scratch_unix_roundtrip":false} + left: Bool(false) + right: true +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/bun.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/bun.json new file mode 100644 index 00000000..df4170d6 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/bun.json @@ -0,0 +1,486 @@ +{ + "recorded_at": "2026-10-30T05:19:31.203Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "x64", + "runtime": "bun", + "version": "1.4.4", + "startup_control": { + "preload": true, + "dotenv": true + }, + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false, + "proc_root_reads": [ + true, + true + ], + "proc_init_list": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + false, + false + ], + "proc_init_list": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true, + "proc_root_reads": [ + true, + false + ], + "proc_init_list": false + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/deno.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/deno.json new file mode 100644 index 00000000..a5eb3801 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/deno.json @@ -0,0 +1,437 @@ +{ + "recorded_at": "2026-10-30T05:19:33.468Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "x64", + "runtime": "deno", + "version": "2.9.8", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/linux-ipc-broker.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/linux-ipc-broker.json new file mode 100644 index 00000000..b5b25482 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/linux-ipc-broker.json @@ -0,0 +1,706 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "b1a06c56cff5caab2fd2635d7b43a717a9edef2f", + "run_id": "41007688907", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "6b5a2468c8f0e007d5399f5c7878bf98af37c6a4ab4a509d17c74139784f38a7", + "helper_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/native.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/native.json new file mode 100644 index 00000000..d0e8ed94 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/native.json @@ -0,0 +1,473 @@ +{ + "recorded_at": "2026-10-30T05:19:33.934Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "x64", + "runtime": "native", + "version": "fixture-v2", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "private_read": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "symlink_private_read": true, + "hardlink_private": true, + "project_rename": true, + "inherited_descriptor": true, + "thread": true, + "package_hardlink": true, + "package_alias_write": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "cannot_relax": null, + "constructor_private_read": true, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "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": 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 + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "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 + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "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": 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": 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 + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "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 + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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 + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "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": 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 + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "project_write": true, + "written_project_read": true, + "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": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "cannot_relax": true, + "constructor_private_read": false, + "constructor_fork": true, + "native_spawn": true, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/node.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/node.json new file mode 100644 index 00000000..0f609a14 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/node.json @@ -0,0 +1,437 @@ +{ + "recorded_at": "2026-10-30T05:19:29.435Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "x64", + "runtime": "node", + "version": "22.23.4", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": true, + "private_write": true, + "protected_read": true, + "protected_write": true, + "hardlink_private": true, + "cannot_relax": null, + "private_read": true, + "symlink_private_read": true, + "project_write": true, + "inherited_descriptor": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + true + ], + "native_child_reads": [ + true, + true + ], + "written_project_read": true, + "project_rename": true, + "filter_replacement_denied": false + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "shell": false, + "native_fork": false, + "child_reads": null, + "native_child_reads": null, + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": false, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": false, + "project_list": false, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + false, + false + ], + "native_child_reads": [ + false, + false + ], + "written_project_read": false, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "stdio_tool": true, + "package_read": true, + "startup_artifacts": false, + "scratch_write": true, + "scratch_read": true, + "project_read": true, + "project_list": true, + "private_list": false, + "private_write": false, + "protected_read": false, + "protected_write": false, + "hardlink_private": false, + "cannot_relax": true, + "private_read": false, + "symlink_private_read": false, + "project_write": true, + "inherited_descriptor": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "shell": true, + "native_fork": true, + "child_reads": [ + true, + false + ], + "native_child_reads": [ + true, + false + ], + "written_project_read": true, + "project_rename": false, + "filter_replacement_denied": true + }, + "self_exec": true + } + ] +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/python.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/python.json new file mode 100644 index 00000000..74814a38 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/python.json @@ -0,0 +1,428 @@ +{ + "recorded_at": "2026-10-30T05:19:30.382Z", + "kernel_release": "7.0.0-1017-azure", + "sources": [ + "run.mts", + "macos_python.c", + "macos_node.c", + "native_exec.c", + "node_bootstrap.cjs", + "fixture.py", + "fixture.cjs", + "process_policy.h", + "linux_filter.h", + "linux_exec.c", + "linux_initial_exec.h", + "fixture_native.c", + "macos_exec.c", + "macos_stage.mts" + ], + "source_sha256": "d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286", + "platform": "linux", + "architecture": "x64", + "runtime": "python", + "version": "3.13.17", + "packaged_helper": { + "input_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "executed_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "private_copy": true, + "direct_runtime": true + }, + "socket_broker_candidate": true, + "mechanism": "experimental descriptor-pinned socket broker with one-use parent startup-exec budget; notification listener remains live for socket operations; immutable inherited filter", + "limitation": "packaged entry with experimental policy composition; production admission, protected ownership, network/secret integration and Windows acceptance remain required", + "unsupported_cases": [], + "rows": [ + { + "name": "unconfined-control", + "flags": { + "read": true, + "write": true, + "shell": true, + "unrestricted": true + }, + "observations": { + "cannot_relax": null, + "child_reads": [ + true, + true + ], + "filter_replacement_denied": false, + "hardlink_private": true, + "inherited_descriptor": true, + "native_child_reads": [ + true, + true + ], + "native_fork": true, + "package_read": true, + "private_list": true, + "private_read": true, + "private_write": true, + "project_list": true, + "project_read": true, + "project_rename": true, + "project_write": true, + "protected_read": true, + "protected_write": true, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": true, + "thread": true, + "thread_reads": [ + true, + true + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + }, + { + "name": "no-capabilities", + "flags": { + "read": false, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "read-only", + "flags": { + "read": true, + "write": false, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "write-without-read", + "flags": { + "read": false, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": false, + "written_project_read": false + }, + "self_exec": false + }, + { + "name": "shell-without-read", + "flags": { + "read": false, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "read-write-without-shell", + "flags": { + "read": true, + "write": true, + "shell": false + }, + "observations": { + "cannot_relax": true, + "child_reads": null, + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": null, + "native_fork": false, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": false, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": false, + "written_project_read": true + }, + "self_exec": false + }, + { + "name": "read-shell-without-write", + "flags": { + "read": true, + "write": false, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": false, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "write-shell-without-read", + "flags": { + "read": false, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + false, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + false, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": false, + "project_read": false, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + false, + false + ], + "thread_shell": true, + "written_project_read": false + }, + "self_exec": true + }, + { + "name": "declared-read-write-shell", + "flags": { + "read": true, + "write": true, + "shell": true + }, + "observations": { + "cannot_relax": true, + "child_reads": [ + true, + false + ], + "filter_replacement_denied": true, + "hardlink_private": false, + "inherited_descriptor": false, + "native_child_reads": [ + true, + false + ], + "native_fork": true, + "package_read": true, + "private_list": false, + "private_read": false, + "private_write": false, + "project_list": true, + "project_read": true, + "project_rename": false, + "project_write": true, + "protected_read": false, + "protected_write": false, + "scratch_read": true, + "scratch_write": true, + "shell": true, + "stdio_tool": true, + "symlink_private_read": false, + "thread": true, + "thread_reads": [ + true, + false + ], + "thread_shell": true, + "written_project_read": true + }, + "self_exec": true + } + ] +} From 7a8ed4e68aaa6904a4de7cb61ef66b20174eeff2 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 01:40:02 -0400 Subject: [PATCH 43/50] test(plugins): prove cancellation settles owned socket workers --- .github/workflows/plugin-linux-ipc.yml | 22 ++ .../examples/plugin_unix_socket_probe.rs | 45 +++- .../examples/support/plugin_socket_cancel.rs | 225 ++++++++++++++++++ .../tests/fixtures/plugin_socket_cancel.h | 66 +++++ .../tests/fixtures/plugin_unix_socket.c | 3 + .../linux-socket-broker-cancellation.md | 54 +++++ .../linux-socket-broker-candidate.md | 30 ++- .../complete-plugin-hook-runtime/tasks.md | 5 +- .../build_socket_candidate.py | 18 +- 9 files changed, 457 insertions(+), 11 deletions(-) create mode 100644 crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index e950b690..d811cf46 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -5,6 +5,8 @@ on: paths: - crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c + - crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h + - crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs - crates/cyber-sandbox/native/** - crates/cyber-sandbox/src/plugin_policy.rs - crates/cyber-sandbox/src/plugin_policy/** @@ -80,6 +82,8 @@ jobs: python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker" broker python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-bind" broker-no-bind python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-thread" broker-no-thread + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-audit" broker-audit + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-cancel" broker-audit-no-cancel python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-candidate" landlock9 python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-missing-resolve" landlock9-no-resolve cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe @@ -90,6 +94,7 @@ jobs: target/debug/examples/plugin_unix_socket_probe "$PWD/target/debug/cyber-sandbox-exec" "$RUNNER_TEMP/linux-ipc.json" target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker.json" --broker + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-audit" "$RUNNER_TEMP/linux-ipc-broker-cancel.json" --broker-cancel - name: Reject blanket bind denial as broker acceptance shell: bash run: | @@ -120,6 +125,22 @@ jobs: assert 'thread-local IP descriptor token exchange' in text, text assert '"tcp_roundtrip":true' in text and '"thread_tcp_roundtrip":false' in text, text PY + - name: Reject missing cancelled-worker termination + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-no-cancel" "$RUNNER_TEMP/linux-ipc-broker-cancel-control.json" --broker-cancel > "$RUNNER_TEMP/broker-no-cancel.log" 2>&1; then + echo 'Missing cancelled-worker termination unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/broker-no-cancel.log" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'cancelled socket worker was not reaped' in text, text + assert '"client_syn_sent":true' in text and '"cancelled":true' in text, text + assert 'BCANCEL ' in text, text + PY - name: Record pathname policy kernel availability id: abi shell: bash @@ -229,5 +250,6 @@ jobs: ${{ runner.temp }}/unix9-unavailable.log ${{ runner.temp }}/broker-no-bind.log ${{ runner.temp }}/broker-no-thread.log + ${{ runner.temp }}/broker-no-cancel.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index 1d0866f0..ba2a038d 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -1,5 +1,8 @@ //! Diagnostic evidence, not capability acceptance. Uses only owned test sockets. #[cfg(unix)] +#[path = "support/plugin_socket_cancel.rs"] +mod cancellation; +#[cfg(unix)] mod probe { use cyber_sandbox::plugin_policy::{LinuxPolicy, MaskPath}; use serde_json::{Value, json}; @@ -64,8 +67,17 @@ mod probe { Baseline, Seccomp, Broker, + BrokerCancel, Unix9, } + impl Mode { + fn ip_servers(self) -> bool { + matches!(self, Self::Unix9 | Self::Broker | Self::BrokerCancel) + } + fn broker(self) -> bool { + matches!(self, Self::Broker | Self::BrokerCancel) + } + } fn observe(policy: &LinuxPolicy<'_>, mode: Mode) -> Value { let mut plan = policy.compose().unwrap(); @@ -94,7 +106,7 @@ mod probe { policy.target_args[1].clone(), "pair".into(), ]; - if matches!(mode, Mode::Unix9 | Mode::Broker) { + if mode.ip_servers() { pair_args.push(policy.scratch.join("local.sock").to_str().unwrap().into()); } let pair_policy = LinuxPolicy { @@ -133,19 +145,37 @@ mod probe { } assert_eq!( observed["tcp_roundtrip"], - matches!(mode, Mode::Unix9 | Mode::Broker), + mode.ip_servers(), "IP server/client token exchange: {observed}" ); assert_eq!( observed["thread_tcp_roundtrip"], - matches!(mode, Mode::Unix9 | Mode::Broker), + mode.ip_servers(), "thread-local IP descriptor token exchange: {observed}" ); - if matches!(mode, Mode::Seccomp | Mode::Broker) { + if mode == Mode::Seccomp || mode.broker() { assert_eq!(observed["pair_listen"], false, "pair listener: {observed}"); } } + fn candidate_rows( + policy: LinuxPolicy<'_>, + listener: &UnixListener, + label: &str, + mode: Mode, + ) -> Vec { + let mut rows = Vec::new(); + if mode == Mode::BrokerCancel && label == "outside" { + let observed = super::cancellation::observe(&policy); + rows.push( + json!({"mode":"cancel","socket":label,"fs_read":policy.fs_read, + "fs_write":policy.fs_write,"shell":false,"observed":observed}), + ); + } + rows.push(pair_row(policy, listener, label, mode)); + rows + } + fn options() -> (PathBuf, PathBuf, Mode) { assert_eq!( std::env::consts::OS, @@ -161,6 +191,7 @@ mod probe { None => Mode::Baseline, Some("--candidate") => Mode::Seccomp, Some("--broker") => Mode::Broker, + Some("--broker-cancel") => Mode::BrokerCancel, Some("--landlock9") => Mode::Unix9, _ => panic!("unknown candidate mode"), }; @@ -263,13 +294,15 @@ mod probe { rows.push(json!({"socket": label, "fs_read": read, "fs_write": write, "shell": false, "host_received_fixture": delivered, "observed": observed})); if candidate { - rows.push(pair_row(policy, &listeners[index], label, mode)); + rows.extend(candidate_rows(policy, &listeners[index], label, mode)); } } } let sources = [ "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", "crates/cyber-sandbox/src/plugin_policy.rs", "crates/cyber-sandbox/src/plugin_policy/linux.rs", "crates/cyber-sandbox/native/linux_plugin_exec.c", @@ -295,7 +328,7 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode == Mode::Broker, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), "rows": rows, "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance."}); std::fs::write( diff --git a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs new file mode 100644 index 00000000..bfea4330 --- /dev/null +++ b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs @@ -0,0 +1,225 @@ +//! Fixed-fixture audit handshake; never used by a production plugin host. +use cyber_sandbox::plugin_policy::LinuxPolicy; +use serde_json::{Value, json}; +use std::collections::BTreeSet; +use std::io::{BufRead, BufReader, Read, Write}; +use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::time::{Duration, Instant}; + +enum Message { + Stdout(String), + Stderr(String), + Fault(String), +} + +fn reader( + input: impl Read + Send + 'static, + tx: Sender, + stdout: bool, +) -> std::thread::JoinHandle<()> { + std::thread::spawn(move || { + let mut input = BufReader::new(input); + for _ in 0..128 { + let mut bytes = Vec::new(); + match (&mut input).take(2049).read_until(b'\n', &mut bytes) { + Ok(0) => return, + Ok(_) if bytes.len() <= 2048 && bytes.ends_with(b"\n") => {} + result => { + let _ = tx.send(Message::Fault(format!( + "bounded fixture stream: {result:?}" + ))); + return; + } + } + let Ok(line) = String::from_utf8(bytes) else { + let _ = tx.send(Message::Fault("non-UTF8 fixture stream".into())); + return; + }; + let message = if stdout { + Message::Stdout(line) + } else { + Message::Stderr(line) + }; + if tx.send(message).is_err() { + return; + } + } + let _ = tx.send(Message::Fault("fixture stream exceeded 128 lines".into())); + }) +} + +#[derive(Default)] +struct Trace { + ready: Option, + finished: Option, + cancelled: Option, + started: BTreeSet, + reaped: BTreeSet, + audit: Vec, +} +impl Trace { + fn record(&mut self, message: Message) -> Result<(), String> { + match message { + Message::Stdout(line) => self.output(&line), + Message::Stderr(line) => self.event(&line), + Message::Fault(error) => Err(error), + } + } + fn output(&mut self, line: &str) -> Result<(), String> { + let value: Value = serde_json::from_str(line).map_err(|e| e.to_string())?; + let slot = match value["phase"].as_str() { + Some("cancel-ready") => &mut self.ready, + Some("cancel-finished") => &mut self.finished, + _ => return Err(format!("unexpected fixture phase: {value}")), + }; + if slot.is_some() { + return Err("duplicate fixture phase".into()); + } + *slot = Some(value); + Ok(()) + } + fn event(&mut self, line: &str) -> Result<(), String> { + self.audit.push(line.trim_end().into()); + let words: Vec<_> = line.split_ascii_whitespace().collect(); + if words.len() != 2 { + return Err(format!("invalid audit event: {line}")); + } + let pid: u32 = words[1].parse().map_err(|_| "invalid audit PID")?; + if pid == 0 { + return Err("zero audit PID".into()); + } + match words[0] { + "BSTART" => { + self.reaped.remove(&pid); + self.started.insert(pid); + } + "BCANCEL" => { + self.cancelled = Some(pid); + } + "BREAP" => { + self.reaped.insert(pid); + } + _ => return Err(format!("unknown audit event: {line}")), + } + Ok(()) + } + fn can_release(&self) -> bool { + let Some(pid) = self.cancelled else { + return false; + }; + self.ready + .as_ref() + .is_some_and(|v| v["client_syn_sent"] == true && v["cancelled"] == true) + && self.started.contains(&pid) + && self.reaped.contains(&pid) + } + fn report(&self) -> Value { + json!({"ready":self.ready,"finished":self.finished,"cancelled_pid":self.cancelled, + "started":self.started,"reaped":self.reaped,"audit":self.audit}) + } +} + +fn drive( + child: &mut Child, + input: &mut Option, + rx: &Receiver, + trace: &mut Trace, + released: &mut bool, +) -> Result, String> { + let deadline = Instant::now() + Duration::from_secs(30); + while Instant::now() < deadline { + if let Ok(message) = rx.recv_timeout(Duration::from_millis(30)) { + trace.record(message)?; + } + if !*released && trace.can_release() { + input + .take() + .ok_or("fixture input already closed")? + .write_all(b"R") + .map_err(|e| e.to_string())?; + *released = true; + } + if let Some(status) = child.try_wait().map_err(|e| e.to_string())? { + return Ok(Some(status)); + } + std::thread::sleep(Duration::from_millis(1)); + } + Ok(None) +} + +pub fn observe(policy: &LinuxPolicy<'_>) -> Value { + let args = [ + policy.target_args[0].clone(), + policy.target_args[1].clone(), + "cancel".into(), + ]; + let policy = LinuxPolicy { + target_args: &args, + ..*policy + }; + let plan = policy.compose().unwrap(); + std::fs::write(policy.profile, plan.landlock).unwrap(); + let mut child = Command::new("/usr/bin/bwrap") + .args(plan.arguments) + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let mut input = child.stdin.take(); + let (tx, rx) = mpsc::channel(); + let stdout = reader(child.stdout.take().unwrap(), tx.clone(), true); + let stderr = reader(child.stderr.take().unwrap(), tx.clone(), false); + drop(tx); + let mut trace = Trace::default(); + let mut released = false; + let outcome = drive(&mut child, &mut input, &rx, &mut trace, &mut released); + // This single owner is the only waiter. An unreaped bwrap child retains its + // identity; terminating its private PID namespace settles the owned fixture + // before any assertion, including the intentionally missing-cancel control. + let status = child.try_wait().unwrap().unwrap_or_else(|| { + let _ = child.kill(); + child.wait().unwrap() + }); + drop(input); + stdout.join().unwrap(); + stderr.join().unwrap(); + let mut errors = Vec::new(); + for message in rx { + if let Err(error) = trace.record(message) { + errors.push(error); + } + } + assert!( + outcome.is_ok() && errors.is_empty(), + "fixture protocol failed: {outcome:?} {errors:?}: {}", + trace.report() + ); + let ready = trace + .ready + .as_ref() + .expect("missing cancellation readiness"); + assert!( + ready["client_syn_sent"] == true && ready["cancelled"] == true, + "missing actual blocked-connect interruption: {}", + trace.report() + ); + assert!( + released, + "cancelled socket worker was not reaped: {}", + trace.report() + ); + assert!( + status.success(), + "cancel fixture failed: {status}: {}", + trace.report() + ); + assert_eq!( + trace.finished.as_ref().unwrap()["tcp_after_cancel"], + true, + "broker did not serve after cancellation" + ); + trace.report() +} diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h b/crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h new file mode 100644 index 00000000..bc4ecedd --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h @@ -0,0 +1,66 @@ +/* Controlled cancellation fixture; contacts only its private loopback server. */ +#ifdef __linux__ +#include +#include +#include + +struct cancel_call { int fd; struct sockaddr_in address; int result; int error; }; +static void cancel_signal(int number) { (void)number; } +static void *cancel_connect(void *argument) { + struct cancel_call *call = argument; + call->result = connect(call->fd, (struct sockaddr *)&call->address, sizeof(call->address)); + call->error = call->result < 0 ? errno : 0; + return NULL; +} +static int syn_pending(int fd) { + struct timespec start, now; + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) return 0; + do { + struct tcp_info info = {0}; + socklen_t size = sizeof(info); + if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &size) != 0) return 0; + if (info.tcpi_state == TCP_SYN_SENT) return 1; + struct timespec pause = { .tv_nsec = 1000000 }; + nanosleep(&pause, NULL); + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0; + } while (now.tv_sec - start.tv_sec < 10); + return 0; +} +static int cancel_probe(void) { + struct sigaction action = { .sa_handler = cancel_signal }; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR1, &action, NULL) != 0) return 3; + int server = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + int filler = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + int pending = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (server < 0 || filler < 0 || pending < 0) return 3; + int retries = 10; + if (setsockopt(pending, IPPROTO_TCP, TCP_SYNCNT, &retries, sizeof(retries)) != 0) return 3; + struct sockaddr_in address = { .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + socklen_t length = sizeof(address); + /* backlog zero admits one completed connection on Linux. Keep it queued + * without accept, making the next blocking connect enter SYN_SENT. */ + if (bind(server, (struct sockaddr *)&address, length) != 0 || listen(server, 0) != 0 || + getsockname(server, (struct sockaddr *)&address, &length) != 0 || + connect(filler, (struct sockaddr *)&address, length) != 0) return 3; + struct cancel_call call = { .fd = pending, .address = address }; + pthread_t thread; + if (pthread_create(&thread, NULL, cancel_connect, &call) != 0) return 3; + int started = syn_pending(pending); + if (pthread_kill(thread, SIGUSR1) != 0 || pthread_join(thread, NULL) != 0) _exit(3); + int cancelled = call.result == -1 && call.error == EINTR; + printf("{\"phase\":\"cancel-ready\",\"client_syn_sent\":%s,\"cancelled\":%s}\n", + started ? "true" : "false", cancelled ? "true" : "false"); + fflush(stdout); + /* The trusted test driver releases this only after observing the exact + * cancelled worker's normal waitpid receipt. No fixed cleanup sleep. */ + if (getchar() != 'R') return 3; + close(pending); close(filler); close(server); + int alive = local_exchange(AF_INET, NULL, -1); + printf("{\"phase\":\"cancel-finished\",\"tcp_after_cancel\":%s}\n", alive ? "true" : "false"); + return started && cancelled && alive ? 0 : 3; +} +#else +static int cancel_probe(void) { return 3; } +#endif diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 4fba0e2e..4d6c1052 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -149,12 +149,15 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, return 0; } +#include "plugin_socket_cancel.h" + int main(int argc, char **argv) { if (argc != 3 && argc != 4 && argc != 5) return 2; struct sockaddr_un address = { .sun_family = AF_UNIX }; size_t length = strlen(argv[1]); if (length == 0 || length >= sizeof(address.sun_path)) return 2; memcpy(address.sun_path, argv[1], length + 1); + if (argc == 4 && strcmp(argv[3], "cancel") == 0) return cancel_probe(); if (argc >= 4) { if (strcmp(argv[3], "pair") != 0) return 2; return pair_probe(&address, diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md new file mode 100644 index 00000000..42b50443 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md @@ -0,0 +1,54 @@ +# Socket-worker cancellation proof (pending native execution) + +This extends the isolated [socket broker candidate](linux-socket-broker-candidate.md), +not production hosting or an approved support floor. The current candidate's +real thread-local table case has separate native receipts; it does not prove +cancellation settlement. + +The controlled C target fills the accept queue of its own private loopback +listener. A second thread calls blocking connect. The fixture waits until +TCP_INFO reports SYN_SENT on that socket, establishing a real in-kernel attempt, +then sends SIGUSR1 specifically to that requesting thread with no restart flag. +It must observe EINTR and report both facts before waiting on its owned stdin. +The pending socket selects ten SYN retries rather than depending on a machine's +possibly short default. The listener, queued connection and pending socket stay +open during that wait. +No ambient network endpoint or credential is contacted. + +An audit-only copied helper prints bounded worker-start, cancellation-request and +normal-waitpid receipts. The Rust driver releases the target only after seeing +the exact cancelled worker's successful normal reaping, matched to its start. +The C target then requires a fresh private TCP token exchange before exiting. +This establishes settlement while the target is still alive, not just teardown +when the namespace exits. The driver uses bounded line/count readers and a +30-second deadline. It terminates/waits the owned bwrap process and drains its +readers before asserting, including on a deliberately failing control. Only its +single owner waits that unreaped child; reader threads do no process signalling. + +The audit markers are test instrumentation in a temporary copied header. The +base candidate and production helper sources are unchanged. The controlled target +does not forge markers on the shared stderr channel; these markers are not an +authenticated protocol for arbitrary plugins and must never grant authority. +Kernel notification identities and owned worker identities remain the actual +broker authority. The receipts make their exercised path observable to this +fixed source-bound fixture only. + +The missing-cancel control removes only the owned-worker SIGKILL on notification +invalidation, retaining the audit markers and cleanup at guest exit. It must fail +the named reaping assertion after a genuine SYN_SENT/EINTR readiness receipt. +It cannot pass by delaying cleanup until target exit: the target is waiting for +the driver's acknowledgement. Neither a fixture startup failure nor a missing +notification may count as the expected missing-cancel counterexample. + +Cancellation does not roll back socket effects. The SYN has already reached the +fixture network and a guest-held socket can retain network state after a worker +dies. This test establishes worker-process settlement and continued broker +service, not absence of prior effects, retry safety or network-policy acceptance. +Descriptor/address mutation, worker saturation, signal races beyond this case, +descendant/namespace lifecycle, runtime admission and the full support/design +review remain open. + +Local macOS checks cover Rust example Clippy, formatting/diff, portable C syntax, +eight temporary builder transformations with compiler interception, workflow +syntax and OpenSpec validation. The Linux-specific C branch and actual signal/ +seccomp/TCP behavior require the hosted x64/arm64 jobs; they are not verified here. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md index 56162887..276bb638 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -73,6 +73,9 @@ thread selection, beyond the earlier standalone prerequisite fixture. A copied control removes PIDFD_THREAD and must fail this assertion while the ordinary single-thread TCP exchange still succeeds. Descriptor/address mutation during an already admitted operation and cancellation/saturation cases remain open. +The [cancellation proof fixture](linux-socket-broker-cancellation.md) now tests +a real blocked connect, targeted interruption and a normal-reap acknowledgement +before target exit. Native execution of that new fixture is still required. The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) requires copied tracee arguments to avoid policy decisions on mutable memory. @@ -88,8 +91,9 @@ shell blocks and embedded Python parse; formatting/diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, kernel behavior for each revision requires corresponding hosted x64/arm64 jobs. -The new thread-specific case/control and corrected runtime report label still -require fresh hosted results; earlier receipts below cannot prove those changes. +The new cancellation case/control still requires hosted results. The separate +thread-specific receipts below establish that earlier case and corrected report +label; the first candidate receipts cannot prove those later changes. Scoped source/security review rejected the nested-listener design, corrected a potential second signal after guest reaping, drains worker replies before @@ -132,3 +136,25 @@ to label that mechanism accurately. Source/binary bindings and observed cases support only the specific tested candidate, not that inaccurate label or the unperformed adversarial cases. Production admission and support/design review remain open. + +## Thread-specific native receipts + +[Run 41007688907](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41007688907) +passed on x64 (job 123906188321) and arm64 (job 123906188488). Retained +[x64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/x64/linux-ipc-broker.json) +and [arm64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-9de3bf77/arm64/linux-ipc-broker.json) +reports each contain 32 socket cases, including 16 successful thread-local TCP +exchanges after CLOSE_RANGE_UNSHARE while the leader retains its UNIX object. +The adjacent missing-thread controls fail that exact assertion with ordinary +TCP still successful, and missing-bind controls still fail their TCP assertion. + +Merge checkout `b1a06c56cff5caab2fd2635d7b43a717a9edef2f` binds socket source hash +`6b5a2468c8f0e007d5399f5c7878bf98af37c6a4ab4a509d17c74139784f38a7`. +The five runtime reports per architecture each pass nine cases with no unsupported +cases, correctly label the live listener and have source hash +`d95a07ad97ad17f76cb3386545624a9cc67b1868f99c3f1a0c7b1c01a7565286`. +Both manifests were independently recomputed from that checkout. Their reported +input/executed-helper hashes match the socket reports, unchanged from the first +candidate's architecture hashes above. Kernel remains `7.0.0-1017-azure`. +These observations prove the real broker's tested thread-table selection, not +cancellation, saturation, descriptor/address mutation or production admission. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 958ea593..ef298471 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -37,8 +37,9 @@ The compatible Linux IPC investigation includes [descriptor-pinning prerequisites](linux-pidfd-proof.md), including independent thread file tables, descriptor-number replacement, copied arguments and actual private TCP exchange. The [broker candidate](linux-socket-broker-candidate.md) has source-bound initial -socket/runtime receipts on x64 and arm64; its real thread-local descriptor case -and precise missing-thread control still require fresh native execution. These +socket/runtime and real thread-local descriptor receipts on x64 and arm64, with +the precise missing-thread control. Its [cancellation case and control](linux-socket-broker-cancellation.md) +still require fresh native execution. These experiments change no production policy or accepted support floor. ## 3. Host contribution lifecycle diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py index 34df365b..05cb283d 100644 --- a/scripts/probes/plugin-native-entry/build_socket_candidate.py +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -8,7 +8,7 @@ root = Path(__file__).resolve().parents[3] output = Path(sys.argv[1]).resolve() mode = sys.argv[2] if len(sys.argv) == 3 else "seccomp" -assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "landlock9", "landlock9-no-resolve"} +assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "broker-audit", "broker-audit-no-cancel", "landlock9", "landlock9-no-resolve"} unix9 = mode.startswith("landlock9") with tempfile.TemporaryDirectory(prefix="cyber-socket-candidate-") as temporary: target = Path(temporary) @@ -24,6 +24,22 @@ before = "syscall(SYS_pidfd_open, request->pid, PIDFD_THREAD)" assert text.count(before) == 1 broker.write_text(text.replace(before, "syscall(SYS_pidfd_open, request->pid, 0)")) + if mode.startswith("broker-audit"): + # Fixture-only receipts from the actual worker owner. Logging is + # injected solely into this temporary copy, never the base helper. + broker = target / "socket_broker.h" + text = broker.read_text() + before = " jobs[slot] = (struct broker_job){ .pid = worker, .pipe = channel[0], .id = request->id };\n" + assert text.count(before) == 1 + text = text.replace(before, before + ' fprintf(stderr, "BSTART %d\\n", (int)worker);\n') + before = " (void)kill(job->pid, SIGKILL);\n" + assert text.count(before) == 1 + cancel = '' if mode == "broker-audit-no-cancel" else before + text = text.replace(before, ' fprintf(stderr, "BCANCEL %d\\n", (int)job->pid);\n' + cancel) + before = " *job = (struct broker_job){0};\n" + assert text.count(before) == 1 + text = text.replace(before, ' fprintf(stderr, "BREAP %d\\n", (int)job->pid);\n' + before) + broker.write_text(text) # Preserve family/pair restrictions, replacing the blanket IP server # denial with the candidate's pinned-object syscall mediation. socket_header = target / "socket_candidate.h" From 204866ec64adcc9537b3d8babdd561d5b624ac8f Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 02:03:50 -0400 Subject: [PATCH 44/50] docs: retain socket-worker cancellation receipts --- .../linux-socket-broker-cancellation.md | 23 +- .../arm64/broker-no-cancel.log | 4 + .../arm64/linux-ipc-broker-cancel.json | 925 ++++++++++++++++++ .../x64/broker-no-cancel.log | 4 + .../x64/linux-ipc-broker-cancel.json | 925 ++++++++++++++++++ 5 files changed, 1880 insertions(+), 1 deletion(-) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/broker-no-cancel.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/linux-ipc-broker-cancel.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/broker-no-cancel.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/linux-ipc-broker-cancel.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md index 42b50443..7e103ab4 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-cancellation.md @@ -1,4 +1,4 @@ -# Socket-worker cancellation proof (pending native execution) +# Socket-worker cancellation proof This extends the isolated [socket broker candidate](linux-socket-broker-candidate.md), not production hosting or an approved support floor. The current candidate's @@ -52,3 +52,24 @@ Local macOS checks cover Rust example Clippy, formatting/diff, portable C syntax eight temporary builder transformations with compiler interception, workflow syntax and OpenSpec validation. The Linux-specific C branch and actual signal/ seccomp/TCP behavior require the hosted x64/arm64 jobs; they are not verified here. + +## Native receipts + +[Run 41009708899](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41009708899) +passed on x64 (job 123912578832) and arm64 (job 123912578660). Retained +[x64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/linux-ipc-broker-cancel.json) +and [arm64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/linux-ipc-broker-cancel.json) +reports each contain four actual SYN_SENT/EINTR, exact-worker normal-reap and +subsequent TCP exchange cases across all read/write combinations, plus the 32 +ordinary socket cases. Adjacent missing-cancel logs fail the named reaping +assertion after genuine interruption while the target remains waiting. + +Merge checkout `6a9ac84504defd5ebe7de06e45b80da6b51ed7e6` binds source SHA-256 +`5c2327b86c3ca1edad606213b88d977b734a5f5da2ce7c531a5454f985c83357`, +independently recomputed from that checkout. Audit helper hashes are x64 +`ca1c090ee7a3eb939f9591bfa247bc89e9cc4f09efa1a07f93d703f5818fe5c1` +and arm64 `9a87557fa36eab0e716e5a2a67a2ae51979f97aff66cc437528dd03fdf56d139`. +The native runtime reports identify kernel `7.0.0-1017-azure`. +These receipts establish the exercised cancellation settlement, not rollback, +arbitrary-plugin marker authenticity or unperformed mutation/saturation cases. +Subsequent shared-driver changes require their own fresh hosted execution. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/broker-no-cancel.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/broker-no-cancel.log new file mode 100644 index 00000000..4b61fe9a --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/broker-no-cancel.log @@ -0,0 +1,4 @@ + +thread 'main' (6975) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:209:5: +cancelled socket worker was not reaped: {"ready":{"phase":"cancel-ready","client_syn_sent":true,"cancelled":true},"finished":null,"cancelled_pid":8,"started":[4,5,6,8],"reaped":[4,5,6],"audit":["BSTART 4","BSTART 5","BREAP 4","BSTART 6","BREAP 5","BREAP 6","BSTART 8","BCANCEL 8"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/linux-ipc-broker-cancel.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/linux-ipc-broker-cancel.json new file mode 100644 index 00000000..a90a7ad4 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/arm64/linux-ipc-broker-cancel.json @@ -0,0 +1,925 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "6a9ac84504defd5ebe7de06e45b80da6b51ed7e6", + "run_id": "41009708899", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "5c2327b86c3ca1edad606213b88d977b734a5f5da2ce7c531a5454f985c83357", + "helper_sha256": "9a87557fa36eab0e716e5a2a67a2ae51979f97aff66cc437528dd03fdf56d139", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BREAP 4", + "BSTART 5", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BSTART 11", + "BREAP 10", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BSTART 11", + "BREAP 10", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BSTART 11", + "BREAP 10", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BREAP 5", + "BSTART 6", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BSTART 11", + "BREAP 10", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/broker-no-cancel.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/broker-no-cancel.log new file mode 100644 index 00000000..9f8dfc97 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/broker-no-cancel.log @@ -0,0 +1,4 @@ + +thread 'main' (7164) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:209:5: +cancelled socket worker was not reaped: {"ready":{"phase":"cancel-ready","client_syn_sent":true,"cancelled":true},"finished":null,"cancelled_pid":8,"started":[4,5,6,8],"reaped":[4,5,6],"audit":["BSTART 4","BSTART 5","BREAP 4","BSTART 6","BREAP 5","BREAP 6","BSTART 8","BCANCEL 8"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/linux-ipc-broker-cancel.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/linux-ipc-broker-cancel.json new file mode 100644 index 00000000..5d99e544 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-cancel-7a8ed4e6/x64/linux-ipc-broker-cancel.json @@ -0,0 +1,925 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "6a9ac84504defd5ebe7de06e45b80da6b51ed7e6", + "run_id": "41009708899", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "5c2327b86c3ca1edad606213b88d977b734a5f5da2ce7c531a5454f985c83357", + "helper_sha256": "ca1c090ee7a3eb939f9591bfa247bc89e9cc4f09efa1a07f93d703f5818fe5c1", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BREAP 10", + "BSTART 11", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BREAP 10", + "BSTART 11", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BREAP 10", + "BSTART 11", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "cancel", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "cancel-ready", + "client_syn_sent": true, + "cancelled": true + }, + "finished": { + "phase": "cancel-finished", + "tcp_after_cancel": true + }, + "cancelled_pid": 8, + "started": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "reaped": [ + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 8", + "BCANCEL 8", + "BREAP 8", + "BSTART 9", + "BSTART 10", + "BREAP 9", + "BSTART 11", + "BREAP 10", + "BREAP 11" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} From d47cfe27786bc65e244ea477408f7d908c7b04af Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 02:03:50 -0400 Subject: [PATCH 45/50] test(plugins): exercise socket and address mutation --- .github/workflows/plugin-linux-ipc.yml | 30 +++++ .../examples/plugin_unix_socket_probe.rs | 23 +++- .../examples/support/plugin_socket_cancel.rs | 122 +++++++++++++----- .../tests/fixtures/plugin_socket_mutation.h | 61 +++++++++ .../tests/fixtures/plugin_unix_socket.c | 2 + .../linux-socket-broker-candidate.md | 6 +- .../linux-socket-broker-mutation.md | 51 ++++++++ .../complete-plugin-hook-runtime/tasks.md | 1 + .../build_socket_candidate.py | 43 +++++- .../linux_socket_broker_mutation.h | 32 +++++ 10 files changed, 333 insertions(+), 38 deletions(-) create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md create mode 100644 scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index d811cf46..4d6211b3 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -6,6 +6,7 @@ on: - crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c - crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h + - crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h - crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs - crates/cyber-sandbox/native/** - crates/cyber-sandbox/src/plugin_policy.rs @@ -14,6 +15,7 @@ on: - .github/workflows/plugin-linux-ipc.yml - scripts/probes/plugin-native-entry/linux_socket_candidate.h - scripts/probes/plugin-native-entry/linux_socket_broker.h + - scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h - scripts/probes/plugin-native-entry/build_socket_candidate.py - scripts/probes/plugin-native-entry/linux_socket_landlock9.h - scripts/probes/plugin-native-entry/linux_unix9_policy.h @@ -84,6 +86,9 @@ jobs: python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-thread" broker-no-thread python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-audit" broker-audit python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-cancel" broker-audit-no-cancel + for mode in mutation mutation-no-pin mutation-no-copy mutation-continue; do + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-$mode" "broker-$mode" + done python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-candidate" landlock9 python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/unix9-missing-resolve" landlock9-no-resolve cargo build --locked -p cyber-sandbox --bin cyber-sandbox-exec --example plugin_unix_socket_probe @@ -95,6 +100,7 @@ jobs: target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-candidate" "$RUNNER_TEMP/linux-ipc-candidate.json" --candidate target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker.json" --broker target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-audit" "$RUNNER_TEMP/linux-ipc-broker-cancel.json" --broker-cancel + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-mutation" "$RUNNER_TEMP/linux-ipc-broker-mutation.json" --broker-mutation - name: Reject blanket bind denial as broker acceptance shell: bash run: | @@ -141,6 +147,29 @@ jobs: assert '"client_syn_sent":true' in text and '"cancelled":true' in text, text assert 'BCANCEL ' in text, text PY + - name: Reject descriptor and address re-resolution controls + shell: bash + run: | + set -euo pipefail + for control in no-pin no-copy continue; do + log="$RUNNER_TEMP/broker-mutation-$control.log" + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-mutation-$control" "$RUNNER_TEMP/linux-ipc-broker-mutation-$control.json" --broker-mutation > "$log" 2>&1; then + echo "Missing mutation safeguard unexpectedly passed: $control" >&2 + exit 1 + fi + python3 - "$log" "$control" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'socket/address mutation changed the admitted operation' in text, text + for token in ['"fd_replaced":true', '"address_changed":true', '"result_ok":false', 'BPIN ']: + assert token in text, text + if sys.argv[2] == 'continue': + assert '"bound_original":true' in text, text + else: + assert '"bound_original":false' in text, text + PY + done - name: Record pathname policy kernel availability id: abi shell: bash @@ -251,5 +280,6 @@ jobs: ${{ runner.temp }}/broker-no-bind.log ${{ runner.temp }}/broker-no-thread.log ${{ runner.temp }}/broker-no-cancel.log + ${{ runner.temp }}/broker-mutation-*.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index ba2a038d..01741e73 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -68,14 +68,21 @@ mod probe { Seccomp, Broker, BrokerCancel, + BrokerMutation, Unix9, } impl Mode { fn ip_servers(self) -> bool { - matches!(self, Self::Unix9 | Self::Broker | Self::BrokerCancel) + matches!( + self, + Self::Unix9 | Self::Broker | Self::BrokerCancel | Self::BrokerMutation + ) } fn broker(self) -> bool { - matches!(self, Self::Broker | Self::BrokerCancel) + matches!( + self, + Self::Broker | Self::BrokerCancel | Self::BrokerMutation + ) } } @@ -165,6 +172,13 @@ mod probe { mode: Mode, ) -> Vec { let mut rows = Vec::new(); + if mode == Mode::BrokerMutation && label == "outside" { + let observed = super::cancellation::observe_mutation(&policy); + rows.push( + json!({"mode":"mutation","socket":label,"fs_read":policy.fs_read, + "fs_write":policy.fs_write,"shell":false,"observed":observed}), + ); + } if mode == Mode::BrokerCancel && label == "outside" { let observed = super::cancellation::observe(&policy); rows.push( @@ -192,6 +206,7 @@ mod probe { Some("--candidate") => Mode::Seccomp, Some("--broker") => Mode::Broker, Some("--broker-cancel") => Mode::BrokerCancel, + Some("--broker-mutation") => Mode::BrokerMutation, Some("--landlock9") => Mode::Unix9, _ => panic!("unknown candidate mode"), }; @@ -302,6 +317,7 @@ mod probe { "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", "crates/cyber-sandbox/src/plugin_policy.rs", "crates/cyber-sandbox/src/plugin_policy/linux.rs", @@ -316,6 +332,7 @@ mod probe { ".github/workflows/plugin-linux-ipc.yml", "scripts/probes/plugin-native-entry/linux_socket_candidate.h", "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", "scripts/probes/plugin-native-entry/linux_unix9_policy.h", "scripts/probes/plugin-native-entry/build_socket_candidate.py", @@ -328,7 +345,7 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "mutation_probe": mode == Mode::BrokerMutation, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), "rows": rows, "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance."}); std::fs::write( diff --git a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs index bfea4330..840f3135 100644 --- a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs +++ b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs @@ -12,6 +12,19 @@ enum Message { Stderr(String), Fault(String), } +#[derive(Clone, Copy)] +enum Case { + Cancellation, + Mutation, +} +impl Case { + fn argument(self) -> &'static str { + match self { + Self::Cancellation => "cancel", + Self::Mutation => "mutation", + } + } +} fn reader( input: impl Read + Send + 'static, @@ -54,6 +67,7 @@ struct Trace { ready: Option, finished: Option, cancelled: Option, + pinned: Option, started: BTreeSet, reaped: BTreeSet, audit: Vec, @@ -69,8 +83,8 @@ impl Trace { fn output(&mut self, line: &str) -> Result<(), String> { let value: Value = serde_json::from_str(line).map_err(|e| e.to_string())?; let slot = match value["phase"].as_str() { - Some("cancel-ready") => &mut self.ready, - Some("cancel-finished") => &mut self.finished, + Some("cancel-ready" | "mutation-ready") => &mut self.ready, + Some("cancel-finished" | "mutation-finished") => &mut self.finished, _ => return Err(format!("unexpected fixture phase: {value}")), }; if slot.is_some() { @@ -97,6 +111,9 @@ impl Trace { "BCANCEL" => { self.cancelled = Some(pid); } + "BPIN" => { + self.pinned = Some(pid); + } "BREAP" => { self.reaped.insert(pid); } @@ -104,7 +121,16 @@ impl Trace { } Ok(()) } - fn can_release(&self) -> bool { + fn can_release(&self, case: Case) -> bool { + if matches!(case, Case::Mutation) { + let Some(tid) = self.pinned else { + return false; + }; + return self + .ready + .as_ref() + .is_some_and(|v| v["request_tid"].as_u64() == Some(u64::from(tid))); + } let Some(pid) = self.cancelled else { return false; }; @@ -116,7 +142,57 @@ impl Trace { } fn report(&self) -> Value { json!({"ready":self.ready,"finished":self.finished,"cancelled_pid":self.cancelled, - "started":self.started,"reaped":self.reaped,"audit":self.audit}) + "pinned_tid":self.pinned,"started":self.started,"reaped":self.reaped,"audit":self.audit}) + } + fn verify(&self, case: Case, released: bool, status: ExitStatus) { + match case { + Case::Cancellation => self.verify_cancel(released, status), + Case::Mutation => self.verify_mutation(released, status), + } + } + fn verify_cancel(&self, released: bool, status: ExitStatus) { + let ready = self.ready.as_ref().expect("missing cancellation readiness"); + assert!( + ready["client_syn_sent"] == true && ready["cancelled"] == true, + "missing actual blocked-connect interruption: {}", + self.report() + ); + assert!( + released, + "cancelled socket worker was not reaped: {}", + self.report() + ); + assert!( + status.success(), + "cancel fixture failed: {status}: {}", + self.report() + ); + assert_eq!( + self.finished.as_ref().unwrap()["tcp_after_cancel"], + true, + "broker did not serve after cancellation" + ); + } + fn verify_mutation(&self, released: bool, status: ExitStatus) { + assert!( + released, + "missing post-pin mutation handshake: {}", + self.report() + ); + let finished = self.finished.as_ref().expect("missing mutation result"); + let correct = [ + "bound_original", + "fd_replaced", + "address_changed", + "result_ok", + ] + .iter() + .all(|key| finished[key] == true); + assert!( + status.success() && correct, + "socket/address mutation changed the admitted operation: {}", + self.report() + ); } } @@ -126,13 +202,14 @@ fn drive( rx: &Receiver, trace: &mut Trace, released: &mut bool, + case: Case, ) -> Result, String> { let deadline = Instant::now() + Duration::from_secs(30); while Instant::now() < deadline { if let Ok(message) = rx.recv_timeout(Duration::from_millis(30)) { trace.record(message)?; } - if !*released && trace.can_release() { + if !*released && trace.can_release(case) { input .take() .ok_or("fixture input already closed")? @@ -149,10 +226,16 @@ fn drive( } pub fn observe(policy: &LinuxPolicy<'_>) -> Value { + observe_case(policy, Case::Cancellation) +} +pub fn observe_mutation(policy: &LinuxPolicy<'_>) -> Value { + observe_case(policy, Case::Mutation) +} +fn observe_case(policy: &LinuxPolicy<'_>, case: Case) -> Value { let args = [ policy.target_args[0].clone(), policy.target_args[1].clone(), - "cancel".into(), + case.argument().into(), ]; let policy = LinuxPolicy { target_args: &args, @@ -175,7 +258,7 @@ pub fn observe(policy: &LinuxPolicy<'_>) -> Value { drop(tx); let mut trace = Trace::default(); let mut released = false; - let outcome = drive(&mut child, &mut input, &rx, &mut trace, &mut released); + let outcome = drive(&mut child, &mut input, &rx, &mut trace, &mut released, case); // This single owner is the only waiter. An unreaped bwrap child retains its // identity; terminating its private PID namespace settles the owned fixture // before any assertion, including the intentionally missing-cancel control. @@ -197,29 +280,6 @@ pub fn observe(policy: &LinuxPolicy<'_>) -> Value { "fixture protocol failed: {outcome:?} {errors:?}: {}", trace.report() ); - let ready = trace - .ready - .as_ref() - .expect("missing cancellation readiness"); - assert!( - ready["client_syn_sent"] == true && ready["cancelled"] == true, - "missing actual blocked-connect interruption: {}", - trace.report() - ); - assert!( - released, - "cancelled socket worker was not reaped: {}", - trace.report() - ); - assert!( - status.success(), - "cancel fixture failed: {status}: {}", - trace.report() - ); - assert_eq!( - trace.finished.as_ref().unwrap()["tcp_after_cancel"], - true, - "broker did not serve after cancellation" - ); + trace.verify(case, released, status); trace.report() } diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h b/crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h new file mode 100644 index 00000000..aae36d2c --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h @@ -0,0 +1,61 @@ +/* Controlled post-inspection mutation; the admitted endpoint is private IP. */ +#ifdef __linux__ +#include + +struct mutation_call { + int fd; + int result; + struct sockaddr_storage address; +}; +static void *mutation_bind(void *argument) { + struct mutation_call *call = argument; + printf("{\"phase\":\"mutation-ready\",\"request_tid\":%ld}\n", syscall(SYS_gettid)); + fflush(stdout); + /* The audit-only broker pauses after copying this entire address and + * pinning this descriptor. Its BPIN receipt releases the mutating thread. */ + call->result = bind(call->fd, (struct sockaddr *)&call->address, sizeof(call->address)); + return NULL; +} +static int mutation_probe(void) { + int parent = syscall(SYS_pidfd_open, getppid(), 0); + int socket_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (parent < 0 || socket_fd < 0) return 3; + int fd = fcntl(socket_fd, F_DUPFD_CLOEXEC, 128); + int kept = fcntl(socket_fd, F_DUPFD_CLOEXEC, 129); + close(socket_fd); + if (fd < 0 || kept < 0) return 3; + struct mutation_call call = { .fd = fd, .result = -1 }; + struct sockaddr_in *ip = (struct sockaddr_in *)&call.address; + ip->sin_family = AF_INET; + ip->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + pthread_t thread; + if (pthread_create(&thread, NULL, mutation_bind, &call) != 0) return 3; + if (getchar() != 'R') _exit(3); + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0 || dup2(pair[0], fd) != fd) _exit(3); + close(pair[0]); + memset(&call.address, 0, sizeof(call.address)); + struct sockaddr_un *unix_address = (struct sockaddr_un *)&call.address; + unix_address->sun_family = AF_UNIX; + memcpy(unix_address->sun_path, "mutation-owned", sizeof("mutation-owned")); + atomic_thread_fence(memory_order_seq_cst); + /* Signal a pinned parent identity, not a PID that could have been reused. */ + if (syscall(SYS_pidfd_send_signal, parent, SIGUSR2, NULL, 0) != 0 || + pthread_join(thread, NULL) != 0) _exit(3); + struct sockaddr_in observed = {0}; + socklen_t size = sizeof(observed); + int bound = getsockname(kept, (struct sockaddr *)&observed, &size) == 0 && + observed.sin_family == AF_INET && observed.sin_port != 0; + int domain = 0; + size = sizeof(domain); + int replaced = getsockopt(fd, SOL_SOCKET, SO_DOMAIN, &domain, &size) == 0 && domain == AF_UNIX; + int changed = call.address.ss_family == AF_UNIX; + printf("{\"phase\":\"mutation-finished\",\"bound_original\":%s,\"fd_replaced\":%s," + "\"address_changed\":%s,\"result_ok\":%s}\n", bound ? "true" : "false", + replaced ? "true" : "false", changed ? "true" : "false", call.result == 0 ? "true" : "false"); + close(parent); close(pair[1]); close(fd); close(kept); + return bound && replaced && changed && call.result == 0 ? 0 : 3; +} +#else +static int mutation_probe(void) { return 3; } +#endif diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 4d6c1052..58efbce2 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -150,6 +150,7 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, } #include "plugin_socket_cancel.h" +#include "plugin_socket_mutation.h" int main(int argc, char **argv) { if (argc != 3 && argc != 4 && argc != 5) return 2; @@ -158,6 +159,7 @@ int main(int argc, char **argv) { if (length == 0 || length >= sizeof(address.sun_path)) return 2; memcpy(address.sun_path, argv[1], length + 1); if (argc == 4 && strcmp(argv[3], "cancel") == 0) return cancel_probe(); + if (argc == 4 && strcmp(argv[3], "mutation") == 0) return mutation_probe(); if (argc >= 4) { if (strcmp(argv[3], "pair") != 0) return 2; return pair_probe(&address, diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md index 276bb638..55119733 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -75,7 +75,9 @@ single-thread TCP exchange still succeeds. Descriptor/address mutation during an already admitted operation and cancellation/saturation cases remain open. The [cancellation proof fixture](linux-socket-broker-cancellation.md) now tests a real blocked connect, targeted interruption and a normal-reap acknowledgement -before target exit. Native execution of that new fixture is still required. +before target exit, with source-bound native receipts on both architectures. +The [post-inspection mutation fixture](linux-socket-broker-mutation.md) and its +three controls still require native execution. The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) requires copied tracee arguments to avoid policy decisions on mutable memory. @@ -91,7 +93,7 @@ shell blocks and embedded Python parse; formatting/diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, kernel behavior for each revision requires corresponding hosted x64/arm64 jobs. -The new cancellation case/control still requires hosted results. The separate +The new post-inspection mutation case/controls still require hosted results. The separate thread-specific receipts below establish that earlier case and corrected report label; the first candidate receipts cannot prove those later changes. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md new file mode 100644 index 00000000..f295cb81 --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md @@ -0,0 +1,51 @@ +# Post-inspection socket/address mutation proof + +This extends the isolated [broker candidate](linux-socket-broker-candidate.md). +It is fixture instrumentation, not production activation or a support revision. + +The target creates an IP socket, duplicates it to retain an independent reference, +then calls bind from a pthread with a full sockaddr_storage-sized address. An +audit-only helper pauses after the real notification's exact-thread socket pin, +address copy, family validation and notification-identity check. It blocks SIGUSR2 +before installing the fixture handler and emitting BPIN, avoiding a lost wakeup. +Only the 128-byte bind fixture takes this hook; ordinary candidate cases do not. + +The trusted Rust driver matches BPIN to the target's requesting TID before sending +one release byte. The target then replaces the original FD number with a UNIX +pair and overwrites the original address with AF_UNIX. A memory fence precedes +signalling a pidfd for its parent, resuming the paused broker without signalling +a potentially reused PID. The helper must operate on the original IP object and +owned address. The target requires a successful bind and a newly assigned IP +port through its retained socket, while confirming its original FD now denotes +UNIX and its original memory has changed. No arbitrary endpoint or credential is +contacted; all operations occur within the composed private namespaces. + +Three copied-helper controls must fail that specific final assertion after the +mutation handshake: + +- Re-resolve the FD after the pause instead of retaining the inspected object. +- Re-read the guest address after the pause instead of retaining the owned copy. +- Perform the original operation, then incorrectly reply with CONTINUE for that + notification, causing the kernel to re-read the changed FD/address. + +The last control must show that the original IP object was bound but the guest's +result failed; the first two must show the original object remained unbound. +All controls must confirm both FD and address replacement. A startup failure or +missing handshake cannot count as a successful missing-safeguard counterexample. +The changed UNIX address retains the 128-byte argument length, which exceeds its +valid pathname structure; the fixture does not create an ambient UNIX listener. + +The shared bounded audit driver uses a 30-second owner deadline, cleans up and +waits its private-namespace process before asserting, and drains both readers. +The deliberately paused broker is confined to temporary test builds and relies +on that outer deadline. Its markers are source-bound fixture observations, not +an authenticated plugin protocol. The base candidate and production helpers have +no pause or marker hook. + +Local checks cover the Rust diagnostic Clippy, portable C syntax (excluding the +Linux branch), twelve temporary builder transformations with compiler interception, +workflow syntax, formatting/diff and OpenSpec. Native x64/arm64 execution of the +new positive case and all three controls is still required. Existing thread and +cancellation receipts cannot establish this mutation proof. Saturation, other +signal/cancellation races, descendants, admission, network/proxy/secret ceilings +and the full native support/design gate remain open. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index ef298471..a4e0bd26 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -39,6 +39,7 @@ descriptor-number replacement, copied arguments and actual private TCP exchange. The [broker candidate](linux-socket-broker-candidate.md) has source-bound initial socket/runtime and real thread-local descriptor receipts on x64 and arm64, with the precise missing-thread control. Its [cancellation case and control](linux-socket-broker-cancellation.md) +also have source-bound native receipts. Its [post-inspection mutation case and controls](linux-socket-broker-mutation.md) still require fresh native execution. These experiments change no production policy or accepted support floor. diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py index 05cb283d..7556af09 100644 --- a/scripts/probes/plugin-native-entry/build_socket_candidate.py +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -8,7 +8,9 @@ root = Path(__file__).resolve().parents[3] output = Path(sys.argv[1]).resolve() mode = sys.argv[2] if len(sys.argv) == 3 else "seccomp" -assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "broker-audit", "broker-audit-no-cancel", "landlock9", "landlock9-no-resolve"} +assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "broker-audit", "broker-audit-no-cancel", + "broker-mutation", "broker-mutation-no-pin", "broker-mutation-no-copy", "broker-mutation-continue", + "landlock9", "landlock9-no-resolve"} unix9 = mode.startswith("landlock9") with tempfile.TemporaryDirectory(prefix="cyber-socket-candidate-") as temporary: target = Path(temporary) @@ -24,7 +26,7 @@ before = "syscall(SYS_pidfd_open, request->pid, PIDFD_THREAD)" assert text.count(before) == 1 broker.write_text(text.replace(before, "syscall(SYS_pidfd_open, request->pid, 0)")) - if mode.startswith("broker-audit"): + if mode.startswith("broker-audit") or mode.startswith("broker-mutation"): # Fixture-only receipts from the actual worker owner. Logging is # injected solely into this temporary copy, never the base helper. broker = target / "socket_broker.h" @@ -40,6 +42,43 @@ assert text.count(before) == 1 text = text.replace(before, ' fprintf(stderr, "BREAP %d\\n", (int)job->pid);\n' + before) broker.write_text(text) + if mode.startswith("broker-mutation"): + shutil.copyfile(Path(__file__).with_name("linux_socket_broker_mutation.h"), target / "socket_mutation.h") + broker = target / "socket_broker.h" + text = broker.read_text() + before = "static void broker_tick(int signal_number)" + assert text.count(before) == 1 + text = text.replace(before, '#include "socket_mutation.h"\n\n' + before) + before = " call->socket = socket;\n return 0;" + assert text.count(before) == 1 + mutation = ''' int mutated = broker_mutation_pause(request); + if (mutated < 0) { close(socket); return EIO; } +''' + if mode == "broker-mutation-no-pin": + mutation += ''' if (mutated) { + int thread = syscall(SYS_pidfd_open, request->pid, PIDFD_THREAD); + int replaced = thread < 0 ? -1 : syscall(SYS_pidfd_getfd, thread, (int)request->data.args[0], 0); + if (thread >= 0) close(thread); + close(socket); + if (replaced < 0) return EIO; + socket = replaced; + } +''' + if mode == "broker-mutation-no-copy": + mutation += ''' if (mutated) { + struct iovec local = { .iov_base = &call->address, .iov_len = call->length }; + struct iovec remote = { .iov_base = (void *)(uintptr_t)request->data.args[1], .iov_len = call->length }; + if (process_vm_readv(request->pid, &local, 1, &remote, 1, 0) != (ssize_t)call->length) { + close(socket); return EIO; + } + } +''' + text = text.replace(before, mutation + before) + if mode == "broker-mutation-continue": + before = " struct seccomp_notif_resp response = { .id = id, .val = value, .error = -error };\n" + assert text.count(before) == 1 + text = text.replace(before, before + ' if (broker_mutation_seen && id == broker_mutation_id) response.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;\n') + broker.write_text(text) # Preserve family/pair restrictions, replacing the blanket IP server # denial with the candidate's pinned-object syscall mediation. socket_header = target / "socket_candidate.h" diff --git a/scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h b/scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h new file mode 100644 index 00000000..73319d22 --- /dev/null +++ b/scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h @@ -0,0 +1,32 @@ +/* Included only in temporary mutation-fixture helpers. No production hook. */ +static volatile sig_atomic_t broker_mutation_released; +static uint64_t broker_mutation_id; +static int broker_mutation_seen; +static void broker_mutation_signal(int number) { + (void)number; + broker_mutation_released = 1; +} +static int broker_mutation_pause(const struct seccomp_notif *request) { + if (request->data.nr != SYS_bind || request->data.args[2] != sizeof(struct sockaddr_storage)) return 0; + sigset_t blocked, previous, waiting; + sigemptyset(&blocked); + sigaddset(&blocked, SIGUSR2); + if (sigprocmask(SIG_BLOCK, &blocked, &previous) != 0) return -1; + struct sigaction action = { .sa_handler = broker_mutation_signal }, old; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR2, &action, &old) != 0) { + (void)sigprocmask(SIG_SETMASK, &previous, NULL); + return -1; + } + broker_mutation_released = 0; + broker_mutation_id = request->id; + broker_mutation_seen = 1; + waiting = previous; + sigdelset(&waiting, SIGUSR2); + int result = 1; + if (fprintf(stderr, "BPIN %u\n", request->pid) < 0 || fflush(stderr) != 0) result = -1; + else while (!broker_mutation_released) sigsuspend(&waiting); + int restored_action = sigaction(SIGUSR2, &old, NULL); + int restored_mask = sigprocmask(SIG_SETMASK, &previous, NULL); + return restored_action == 0 && restored_mask == 0 ? result : -1; +} From 7ca80587bd58ab7319992c49e18a16a3cf75d551 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 07:04:36 -0400 Subject: [PATCH 46/50] docs: retain socket mutation proof receipts --- .../linux-socket-broker-mutation.md | 22 + .../arm64/broker-mutation-continue.log | 4 + .../arm64/broker-mutation-no-copy.log | 4 + .../arm64/broker-mutation-no-pin.log | 4 + .../arm64/linux-ipc-broker-mutation.json | 844 ++++++++++++++++++ .../x64/broker-mutation-continue.log | 4 + .../x64/broker-mutation-no-copy.log | 4 + .../x64/broker-mutation-no-pin.log | 4 + .../x64/linux-ipc-broker-mutation.json | 844 ++++++++++++++++++ 9 files changed, 1734 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-continue.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-copy.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-pin.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/linux-ipc-broker-mutation.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-continue.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-copy.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-pin.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/linux-ipc-broker-mutation.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md index f295cb81..4ee06eb4 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-mutation.md @@ -49,3 +49,25 @@ new positive case and all three controls is still required. Existing thread and cancellation receipts cannot establish this mutation proof. Saturation, other signal/cancellation races, descendants, admission, network/proxy/secret ceilings and the full native support/design gate remain open. + +## Native receipts + +[Run 41011901810](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41011901810) +passed on x64 (job 123919430419) and arm64 (job 123919430221). Retained +[x64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/linux-ipc-broker-mutation.json) +and [arm64](../../../scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/linux-ipc-broker-mutation.json) +reports each contain four successful post-inspection mutations across read/write +combinations and 32 ordinary socket cases. Each matches the requesting TID to the +post-pin marker, confirms the original object was bound and both originals were +changed, and observes a successful caller result. All three adjacent controls +fail the precise final assertion: re-resolution/re-read leave the original object +unbound; incorrect CONTINUE binds it but fails the guest result. + +Merge checkout `893b883655dd7ede255e992a99f99bd0bf1859d2` binds source SHA-256 +`0ea7e634b39ae67a292044c4846d939c5deb8e54847f60e3839deafc996d55d8`, +independently recomputed from the checkout. Recorded instrumented helper hashes +are x64 `90d204e45347be22ecbdd361a6c3984c7a3de9dde2afa94375037ad037fca19b` +and arm64 `0ac7234c48e23229db1e78cc0b5bd1184e5a2daa7768eb50ac39aac9f99f797c`. +These source-bound observations establish this controlled mutation case, not all +possible concurrent changes or production admission. Subsequent shared-driver +changes still need fresh native checks. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-continue.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-continue.log new file mode 100644 index 00000000..ce00edee --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-continue.log @@ -0,0 +1,4 @@ + +thread 'main' (4402) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":true,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-copy.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-copy.log new file mode 100644 index 00000000..654990e6 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-copy.log @@ -0,0 +1,4 @@ + +thread 'main' (4381) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":false,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-pin.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-pin.log new file mode 100644 index 00000000..9f03fb5d --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/broker-mutation-no-pin.log @@ -0,0 +1,4 @@ + +thread 'main' (4361) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":false,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/linux-ipc-broker-mutation.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/linux-ipc-broker-mutation.json new file mode 100644 index 00000000..4886a6ed --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/arm64/linux-ipc-broker-mutation.json @@ -0,0 +1,844 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "893b883655dd7ede255e992a99f99bd0bf1859d2", + "run_id": "41011901810", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "0ea7e634b39ae67a292044c4846d939c5deb8e54847f60e3839deafc996d55d8", + "helper_sha256": "0ac7234c48e23229db1e78cc0b5bd1184e5a2daa7768eb50ac39aac9f99f797c", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-continue.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-continue.log new file mode 100644 index 00000000..416da17b --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-continue.log @@ -0,0 +1,4 @@ + +thread 'main' (4224) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":true,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-copy.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-copy.log new file mode 100644 index 00000000..b3b836e2 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-copy.log @@ -0,0 +1,4 @@ + +thread 'main' (4204) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":false,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-pin.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-pin.log new file mode 100644 index 00000000..d3b3168c --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/broker-mutation-no-pin.log @@ -0,0 +1,4 @@ + +thread 'main' (4184) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:191:9: +socket/address mutation changed the admitted operation: {"ready":{"phase":"mutation-ready","request_tid":4},"finished":{"phase":"mutation-finished","bound_original":false,"fd_replaced":true,"address_changed":true,"result_ok":false},"cancelled_pid":null,"pinned_tid":4,"started":[5],"reaped":[5],"audit":["BPIN 4","BSTART 5","BREAP 5"]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/linux-ipc-broker-mutation.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/linux-ipc-broker-mutation.json new file mode 100644 index 00000000..6981fe1b --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-mutation-d47cfe27/x64/linux-ipc-broker-mutation.json @@ -0,0 +1,844 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "893b883655dd7ede255e992a99f99bd0bf1859d2", + "run_id": "41011901810", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "0ea7e634b39ae67a292044c4846d939c5deb8e54847f60e3839deafc996d55d8", + "helper_sha256": "90d204e45347be22ecbdd361a6c3984c7a3de9dde2afa94375037ad037fca19b", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "mutation", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "mutation-ready", + "request_tid": 4 + }, + "finished": { + "phase": "mutation-finished", + "bound_original": true, + "fd_replaced": true, + "address_changed": true, + "result_ok": true + }, + "cancelled_pid": null, + "pinned_tid": 4, + "started": [ + 5 + ], + "reaped": [ + 5 + ], + "audit": [ + "BPIN 4", + "BSTART 5", + "BREAP 5" + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} From 71d019969c7957907dfe4f43fb302e60825653e4 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 07:04:36 -0400 Subject: [PATCH 47/50] test(plugins): exercise escaped descendant namespace lifetime --- .github/workflows/plugin-linux-ipc.yml | 19 ++++ .../examples/plugin_unix_socket_probe.rs | 34 +++++- .../examples/support/plugin_socket_cancel.rs | 107 ++++++++++++++++-- .../tests/fixtures/plugin_process_escape.h | 40 +++++++ .../tests/fixtures/plugin_unix_socket.c | 2 + .../linux-plugin-descendant-proof.md | 43 +++++++ .../linux-socket-broker-candidate.md | 6 +- .../complete-plugin-hook-runtime/tasks.md | 3 +- 8 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 4d6211b3..976b18a1 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -7,6 +7,7 @@ on: - crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c - crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h - crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h + - crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h - crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs - crates/cyber-sandbox/native/** - crates/cyber-sandbox/src/plugin_policy.rs @@ -101,6 +102,7 @@ jobs: target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker.json" --broker target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-audit" "$RUNNER_TEMP/linux-ipc-broker-cancel.json" --broker-cancel target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-mutation" "$RUNNER_TEMP/linux-ipc-broker-mutation.json" --broker-mutation + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker-escape.json" --broker-escape - name: Reject blanket bind denial as broker acceptance shell: bash run: | @@ -170,6 +172,22 @@ jobs: assert '"bound_original":false' in text, text PY done + - name: Reject missing private PID namespace settlement + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker-escape-control.json" --broker-escape-no-pid > "$RUNNER_TEMP/broker-no-pid.log" 2>&1; then + echo 'Missing private PID namespace unexpectedly settled the escaped descendant' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/broker-no-pid.log" <<'PY' + from pathlib import Path + import sys + text = Path(sys.argv[1]).read_text() + assert 'escaped descendant retained stdout after namespace owner exited' in text, text + for token in ['"escaped_session":true', '"controlled_stdin":true', '"stdout_closed_before_cleanup":false', '"owner_exited_before_cleanup":true']: + assert token in text, text + PY - name: Record pathname policy kernel availability id: abi shell: bash @@ -281,5 +299,6 @@ jobs: ${{ runner.temp }}/broker-no-thread.log ${{ runner.temp }}/broker-no-cancel.log ${{ runner.temp }}/broker-mutation-*.log + ${{ runner.temp }}/broker-no-pid.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index 01741e73..af23cd83 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -69,21 +69,39 @@ mod probe { Broker, BrokerCancel, BrokerMutation, + BrokerEscape, + BrokerEscapeNoPid, Unix9, } impl Mode { fn ip_servers(self) -> bool { matches!( self, - Self::Unix9 | Self::Broker | Self::BrokerCancel | Self::BrokerMutation + Self::Unix9 + | Self::Broker + | Self::BrokerCancel + | Self::BrokerMutation + | Self::BrokerEscape + | Self::BrokerEscapeNoPid ) } fn broker(self) -> bool { matches!( self, - Self::Broker | Self::BrokerCancel | Self::BrokerMutation + Self::Broker + | Self::BrokerCancel + | Self::BrokerMutation + | Self::BrokerEscape + | Self::BrokerEscapeNoPid ) } + fn escape(self) -> Option { + match self { + Self::BrokerEscape => Some(true), + Self::BrokerEscapeNoPid => Some(false), + _ => None, + } + } } fn observe(policy: &LinuxPolicy<'_>, mode: Mode) -> Value { @@ -172,6 +190,13 @@ mod probe { mode: Mode, ) -> Vec { let mut rows = Vec::new(); + if let Some(private_pid) = mode.escape() + && label == "outside" + { + let observed = super::cancellation::observe_escape(&policy, private_pid); + rows.push(json!({"mode":"escape","socket":label,"fs_read":policy.fs_read, + "fs_write":policy.fs_write,"shell":true,"private_pid_namespace":private_pid,"observed":observed})); + } if mode == Mode::BrokerMutation && label == "outside" { let observed = super::cancellation::observe_mutation(&policy); rows.push( @@ -207,6 +232,8 @@ mod probe { Some("--broker") => Mode::Broker, Some("--broker-cancel") => Mode::BrokerCancel, Some("--broker-mutation") => Mode::BrokerMutation, + Some("--broker-escape") => Mode::BrokerEscape, + Some("--broker-escape-no-pid") => Mode::BrokerEscapeNoPid, Some("--landlock9") => Mode::Unix9, _ => panic!("unknown candidate mode"), }; @@ -318,6 +345,7 @@ mod probe { "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", "crates/cyber-sandbox/src/plugin_policy.rs", "crates/cyber-sandbox/src/plugin_policy/linux.rs", @@ -345,7 +373,7 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "mutation_probe": mode == Mode::BrokerMutation, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "mutation_probe": mode == Mode::BrokerMutation, "escape_probe": mode.escape(), "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), "rows": rows, "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance."}); std::fs::write( diff --git a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs index 840f3135..b83dcd44 100644 --- a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs +++ b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs @@ -11,17 +11,20 @@ enum Message { Stdout(String), Stderr(String), Fault(String), + Eof(bool), } #[derive(Clone, Copy)] enum Case { Cancellation, Mutation, + Escape, } impl Case { fn argument(self) -> &'static str { match self { Self::Cancellation => "cancel", Self::Mutation => "mutation", + Self::Escape => "escape", } } } @@ -36,7 +39,10 @@ fn reader( for _ in 0..128 { let mut bytes = Vec::new(); match (&mut input).take(2049).read_until(b'\n', &mut bytes) { - Ok(0) => return, + Ok(0) => { + let _ = tx.send(Message::Eof(stdout)); + return; + } Ok(_) if bytes.len() <= 2048 && bytes.ends_with(b"\n") => {} result => { let _ = tx.send(Message::Fault(format!( @@ -71,6 +77,9 @@ struct Trace { started: BTreeSet, reaped: BTreeSet, audit: Vec, + stdout_closed: bool, + closed_before_cleanup: Option, + owner_exited_before_cleanup: Option, } impl Trace { fn record(&mut self, message: Message) -> Result<(), String> { @@ -78,12 +87,16 @@ impl Trace { Message::Stdout(line) => self.output(&line), Message::Stderr(line) => self.event(&line), Message::Fault(error) => Err(error), + Message::Eof(stdout) => { + self.stdout_closed |= stdout; + Ok(()) + } } } fn output(&mut self, line: &str) -> Result<(), String> { let value: Value = serde_json::from_str(line).map_err(|e| e.to_string())?; let slot = match value["phase"].as_str() { - Some("cancel-ready" | "mutation-ready") => &mut self.ready, + Some("cancel-ready" | "mutation-ready" | "escape-ready") => &mut self.ready, Some("cancel-finished" | "mutation-finished") => &mut self.finished, _ => return Err(format!("unexpected fixture phase: {value}")), }; @@ -122,6 +135,9 @@ impl Trace { Ok(()) } fn can_release(&self, case: Case) -> bool { + if matches!(case, Case::Escape) { + return false; + } if matches!(case, Case::Mutation) { let Some(tid) = self.pinned else { return false; @@ -142,12 +158,14 @@ impl Trace { } fn report(&self) -> Value { json!({"ready":self.ready,"finished":self.finished,"cancelled_pid":self.cancelled, - "pinned_tid":self.pinned,"started":self.started,"reaped":self.reaped,"audit":self.audit}) + "pinned_tid":self.pinned,"started":self.started,"reaped":self.reaped,"audit":self.audit, + "stdout_closed_before_cleanup":self.closed_before_cleanup,"owner_exited_before_cleanup":self.owner_exited_before_cleanup}) } fn verify(&self, case: Case, released: bool, status: ExitStatus) { match case { Case::Cancellation => self.verify_cancel(released, status), Case::Mutation => self.verify_mutation(released, status), + Case::Escape => self.verify_escape(status), } } fn verify_cancel(&self, released: bool, status: ExitStatus) { @@ -194,6 +212,49 @@ impl Trace { self.report() ); } + fn verify_escape(&self, status: ExitStatus) { + assert_eq!( + self.ready.as_ref().expect("missing descendant receipt")["controlled_stdin"], + true, + "fixture stdin was not the controlled blocking pipe" + ); + assert_eq!( + self.ready.as_ref().expect("missing descendant receipt")["escaped_session"], + true, + "fixture did not escape its original process group" + ); + assert!( + status.success() && self.owner_exited_before_cleanup == Some(true), + "namespace owner did not finish after plugin leader exit: {}", + self.report() + ); + assert!( + self.closed_before_cleanup == Some(true), + "escaped descendant retained stdout after namespace owner exited: {}", + self.report() + ); + } +} + +fn settled( + child: &mut Child, + trace: &Trace, + case: Case, + exit: &mut Option<(ExitStatus, Instant)>, +) -> Result, String> { + if exit.is_none() + && let Some(status) = child.try_wait().map_err(|e| e.to_string())? + { + *exit = Some((status, Instant::now())); + } + let Some((status, at)) = exit.as_ref() else { + return Ok(None); + }; + let complete = match case { + Case::Escape => trace.stdout_closed || at.elapsed() >= Duration::from_secs(10), + _ => true, + }; + Ok(complete.then_some(*status)) } fn drive( @@ -205,6 +266,7 @@ fn drive( case: Case, ) -> Result, String> { let deadline = Instant::now() + Duration::from_secs(30); + let mut exit = None; while Instant::now() < deadline { if let Ok(message) = rx.recv_timeout(Duration::from_millis(30)) { trace.record(message)?; @@ -217,7 +279,7 @@ fn drive( .map_err(|e| e.to_string())?; *released = true; } - if let Some(status) = child.try_wait().map_err(|e| e.to_string())? { + if let Some(status) = settled(child, trace, case, &mut exit)? { return Ok(Some(status)); } std::thread::sleep(Duration::from_millis(1)); @@ -226,12 +288,15 @@ fn drive( } pub fn observe(policy: &LinuxPolicy<'_>) -> Value { - observe_case(policy, Case::Cancellation) + observe_case(policy, Case::Cancellation, true) } pub fn observe_mutation(policy: &LinuxPolicy<'_>) -> Value { - observe_case(policy, Case::Mutation) + observe_case(policy, Case::Mutation, true) } -fn observe_case(policy: &LinuxPolicy<'_>, case: Case) -> Value { +pub fn observe_escape(policy: &LinuxPolicy<'_>, private_pid: bool) -> Value { + observe_case(policy, Case::Escape, private_pid) +} +fn observe_case(policy: &LinuxPolicy<'_>, case: Case, private_pid: bool) -> Value { let args = [ policy.target_args[0].clone(), policy.target_args[1].clone(), @@ -239,9 +304,30 @@ fn observe_case(policy: &LinuxPolicy<'_>, case: Case) -> Value { ]; let policy = LinuxPolicy { target_args: &args, + shell: matches!(case, Case::Escape) || policy.shell, ..*policy }; - let plan = policy.compose().unwrap(); + let mut plan = policy.compose().unwrap(); + if !private_pid { + assert_eq!( + plan.arguments + .iter() + .filter(|arg| *arg == "--unshare-pid") + .count(), + 1 + ); + plan.arguments.retain(|arg| arg != "--unshare-pid"); + let proc = plan + .arguments + .iter() + .position(|arg| arg == "--proc") + .unwrap(); + assert_eq!(plan.arguments[proc + 1], "/proc"); + plan.arguments.splice( + proc..proc + 2, + ["--ro-bind".into(), "/proc".into(), "/proc".into()], + ); + } std::fs::write(policy.profile, plan.landlock).unwrap(); let mut child = Command::new("/usr/bin/bwrap") .args(plan.arguments) @@ -259,6 +345,11 @@ fn observe_case(policy: &LinuxPolicy<'_>, case: Case) -> Value { let mut trace = Trace::default(); let mut released = false; let outcome = drive(&mut child, &mut input, &rx, &mut trace, &mut released, case); + if matches!(case, Case::Escape) { + trace.closed_before_cleanup = Some(trace.stdout_closed); + trace.owner_exited_before_cleanup = + Some(outcome.as_ref().is_ok_and(|status| status.is_some())); + } // This single owner is the only waiter. An unreaped bwrap child retains its // identity; terminating its private PID namespace settles the owned fixture // before any assertion, including the intentionally missing-cancel control. diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h b/crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h new file mode 100644 index 00000000..b564904d --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h @@ -0,0 +1,40 @@ +/* A controlled escaped descendant remains releasable by closing owner stdin. */ +#ifdef __linux__ +#include +static int escape_probe(void) { + int ready[2]; + if (pipe2(ready, O_CLOEXEC) != 0) return 3; + pid_t child = fork(); + if (child < 0) return 3; + if (child == 0) { + close(ready[0]); + int escaped = setsid() == getpid(); + struct stat input; + int flags = fcntl(STDIN_FILENO, F_GETFL); + int controlled = flags >= 0 && (flags & O_ACCMODE) == O_RDONLY && !(flags & O_NONBLOCK) && + fstat(STDIN_FILENO, &input) == 0 && S_ISFIFO(input.st_mode); + printf("{\"phase\":\"escape-ready\",\"escaped_session\":%s,\"controlled_stdin\":%s}\n", + escaped ? "true" : "false", controlled ? "true" : "false"); + fflush(stdout); + char receipt = escaped && controlled ? 'S' : 'F'; + if (write(ready[1], &receipt, 1) != 1) _exit(3); + close(ready[1]); + /* No PID lookup or signal is needed to clean the missing-namespace + * control. The exact pipe owner closes this after measuring settlement. */ + char command; + ssize_t count; + do { count = read(STDIN_FILENO, &command, 1); } while (count > 0 || (count < 0 && errno == EINTR)); + _exit(count == 0 && escaped && controlled ? 0 : 3); + } + close(ready[1]); + char receipt = 0; + ssize_t count; + do { count = read(ready[0], &receipt, 1); } while (count < 0 && errno == EINTR); + close(ready[0]); + /* Exit only after the descendant really owns another session and has + * flushed its stdout receipt. It intentionally retains stdout and stdin. */ + return count == 1 && receipt == 'S' ? 0 : 3; +} +#else +static int escape_probe(void) { return 3; } +#endif diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 58efbce2..7187c41c 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -151,6 +151,7 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, #include "plugin_socket_cancel.h" #include "plugin_socket_mutation.h" +#include "plugin_process_escape.h" int main(int argc, char **argv) { if (argc != 3 && argc != 4 && argc != 5) return 2; @@ -160,6 +161,7 @@ int main(int argc, char **argv) { memcpy(address.sun_path, argv[1], length + 1); if (argc == 4 && strcmp(argv[3], "cancel") == 0) return cancel_probe(); if (argc == 4 && strcmp(argv[3], "mutation") == 0) return mutation_probe(); + if (argc == 4 && strcmp(argv[3], "escape") == 0) return escape_probe(); if (argc >= 4) { if (strcmp(argv[3], "pair") != 0) return 2; return pair_probe(&address, diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md new file mode 100644 index 00000000..eb9cc06a --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md @@ -0,0 +1,43 @@ +# Private PID namespace descendant-settlement fixture + +This tests the lifetime requirement of the existing [Linux broker candidate](linux-socket-broker-candidate.md). +It does not activate native hosting, select a support floor or replace production +resource/generation admission. + +With shell enabled, the fixed C target forks a descendant that successfully calls +setsid. The descendant checks that its stdin is the controlled blocking pipe, +flushes a stdout receipt, and acknowledges readiness over an internal pipe. Only +then does the plugin leader exit. The descendant retains standard input/output +and does not exit voluntarily while the driver holds its input open. + +The driver requires the owned bwrap process to exit successfully and stdout to +reach EOF before it closes that input or signals anything. It keeps reading for +up to ten seconds after owner exit, within its overall 30-second deadline. This +is an observed closure condition, not a fixed sleep considered successful cleanup. +The fixture establishes a live descendant in another session; original-process- +group termination alone cannot settle it. The kernel's +[PID namespace documentation](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html) +describes termination of remaining namespace processes when its init exits; +native execution must establish how the actual composed bwrap launch behaves. + +The precise control removes only the private PID namespace. It replaces the +private proc mount with a read-only bind of proc so lack of mount permission does +not masquerade as the expected counterexample. All other composition remains. +It must show successful owner exit and a descendant in its own session retaining +stdout before cleanup. A startup failure, failed setsid, uncontrolled stdin or +owner that never exited cannot count as that counterexample. + +The controlled descendant remains safely releasable without PID lookup/signals: +after capturing the pre-cleanup facts, the owner closes its exact input pipe. +The fixture's only remaining path reads EOF and exits, closing output. Both reader +threads are drained before any assertion and fixture-path cleanup. The normal +private-namespace case must settle before this fallback release. This fallback +is specific to the fixed source-bound fixture, not a termination protocol for +arbitrary plugins. + +No production composer/helper or ordinary tool behavior changes. The positive +case runs across all four read/write combinations with shell true. Other signal, +fork/exec, nested namespace, saturation and owner/admission obligations remain +separately gated. Local Rust example Clippy, portable C syntax, workflow syntax, +formatting/diff and OpenSpec can run on macOS; the Linux branch, positive native +settlement and missing-namespace control require hosted x64/arm64 execution. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md index 55119733..06ba3878 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -77,7 +77,9 @@ The [cancellation proof fixture](linux-socket-broker-cancellation.md) now tests a real blocked connect, targeted interruption and a normal-reap acknowledgement before target exit, with source-bound native receipts on both architectures. The [post-inspection mutation fixture](linux-socket-broker-mutation.md) and its -three controls still require native execution. +three controls have source-bound native receipts on both architectures. +The [escaped-descendant namespace fixture](linux-plugin-descendant-proof.md) +still requires native execution. The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) requires copied tracee arguments to avoid policy decisions on mutable memory. @@ -93,7 +95,7 @@ shell blocks and embedded Python parse; formatting/diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, kernel behavior for each revision requires corresponding hosted x64/arm64 jobs. -The new post-inspection mutation case/controls still require hosted results. The separate +The new escaped-descendant case/control still requires hosted results. The separate thread-specific receipts below establish that earlier case and corrected report label; the first candidate receipts cannot prove those later changes. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index a4e0bd26..b5e7c5a7 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -39,7 +39,8 @@ descriptor-number replacement, copied arguments and actual private TCP exchange. The [broker candidate](linux-socket-broker-candidate.md) has source-bound initial socket/runtime and real thread-local descriptor receipts on x64 and arm64, with the precise missing-thread control. Its [cancellation case and control](linux-socket-broker-cancellation.md) -also have source-bound native receipts. Its [post-inspection mutation case and controls](linux-socket-broker-mutation.md) +also have source-bound native receipts, as do the [post-inspection mutation case and controls](linux-socket-broker-mutation.md). +Its [escaped-descendant namespace case and control](linux-plugin-descendant-proof.md) still require fresh native execution. These experiments change no production policy or accepted support floor. From 4760de763391cb37e5e22355d5e40c1d1c8ede87 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 07:36:09 -0400 Subject: [PATCH 48/50] docs: retain escaped descendant settlement evidence --- .../linux-plugin-descendant-proof.md | 21 + .../arm64/broker-no-pid.log | 4 + .../arm64/linux-ipc-broker-escape.json | 806 ++++++++++++++++++ .../x64/broker-no-pid.log | 4 + .../x64/linux-ipc-broker-escape.json | 806 ++++++++++++++++++ 5 files changed, 1641 insertions(+) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/broker-no-pid.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/linux-ipc-broker-escape.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/broker-no-pid.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/linux-ipc-broker-escape.json diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md b/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md index eb9cc06a..39f33cde 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-plugin-descendant-proof.md @@ -41,3 +41,24 @@ fork/exec, nested namespace, saturation and owner/admission obligations remain separately gated. Local Rust example Clippy, portable C syntax, workflow syntax, formatting/diff and OpenSpec can run on macOS; the Linux branch, positive native settlement and missing-namespace control require hosted x64/arm64 execution. + +## Native receipts + +[Run 41045437205](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41045437205) +passed on x64 (job 124028263381) and arm64 (job 124028263252). Retained +[x64](../../../scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/linux-ipc-broker-escape.json) +and [arm64](../../../scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/linux-ipc-broker-escape.json) +reports each contain four shell-enabled lifetime cases across read/write +combinations, plus 32 ordinary socket cases. Every lifetime case confirms setsid, +controlled blocking stdin, successful namespace-owner exit and stdout closure +before cleanup. The adjacent missing-PID logs confirm successful owner exit with +stdout still held by that escaped, controlled descendant before fallback release. + +Merge checkout `735898a3f82adfce0bae07898c75ec74d535e7e8` binds source SHA-256 +`699c2318f5f7209db724cb9d3f97e0279f7dbadc60e0fa456d1ea3a58861589f`, +independently recomputed from that checkout. Recorded base-helper hashes remain +x64 `4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179` +and arm64 `c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d`. +These receipts establish the exercised setsid/namespace lifetime case, not all +possible process races or production resource admission. Further shared-driver +changes need their own fresh native verification. diff --git a/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/broker-no-pid.log b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/broker-no-pid.log new file mode 100644 index 00000000..d8da86cb --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/broker-no-pid.log @@ -0,0 +1,4 @@ + +thread 'main' (7608) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:231:9: +escaped descendant retained stdout after namespace owner exited: {"ready":{"phase":"escape-ready","escaped_session":true,"controlled_stdin":true},"finished":null,"cancelled_pid":null,"pinned_tid":null,"started":[],"reaped":[],"audit":[],"stdout_closed_before_cleanup":false,"owner_exited_before_cleanup":true} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/linux-ipc-broker-escape.json b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/linux-ipc-broker-escape.json new file mode 100644 index 00000000..ab77d38a --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/arm64/linux-ipc-broker-escape.json @@ -0,0 +1,806 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "735898a3f82adfce0bae07898c75ec74d535e7e8", + "run_id": "41045437205", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": false, + "escape_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "699c2318f5f7209db724cb9d3f97e0279f7dbadc60e0fa456d1ea3a58861589f", + "helper_sha256": "c3f587c520b222abb506e026074fa980a8992eb20bcf039fa2d808adbd50624d", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/broker-no-pid.log b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/broker-no-pid.log new file mode 100644 index 00000000..6fa3351d --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/broker-no-pid.log @@ -0,0 +1,4 @@ + +thread 'main' (7835) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:231:9: +escaped descendant retained stdout after namespace owner exited: {"ready":{"phase":"escape-ready","escaped_session":true,"controlled_stdin":true},"finished":null,"cancelled_pid":null,"pinned_tid":null,"started":[],"reaped":[],"audit":[],"stdout_closed_before_cleanup":false,"owner_exited_before_cleanup":true} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/linux-ipc-broker-escape.json b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/linux-ipc-broker-escape.json new file mode 100644 index 00000000..12f20eeb --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-plugin-descendant-71d01996/x64/linux-ipc-broker-escape.json @@ -0,0 +1,806 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "735898a3f82adfce0bae07898c75ec74d535e7e8", + "run_id": "41045437205", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": false, + "escape_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "699c2318f5f7209db724cb9d3f97e0279f7dbadc60e0fa456d1ea3a58861589f", + "helper_sha256": "4ee0a18077a90e3dc2db1a990ba8bc52b7a3a380e89dec2ea550f1d7b9abe179", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "escape", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": true, + "private_pid_namespace": true, + "observed": { + "ready": { + "phase": "escape-ready", + "escaped_session": true, + "controlled_stdin": true + }, + "finished": null, + "cancelled_pid": null, + "pinned_tid": null, + "started": [], + "reaped": [], + "audit": [], + "stdout_closed_before_cleanup": true, + "owner_exited_before_cleanup": true + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance." +} From ceecb47911e565b11662285a3c32d22a1e578af4 Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 07:36:09 -0400 Subject: [PATCH 49/50] test(plugins): exercise socket worker capacity and recovery --- .github/workflows/plugin-linux-ipc.yml | 24 ++++ .../examples/plugin_unix_socket_probe.rs | 16 ++- .../examples/support/plugin_socket_cancel.rs | 113 ++++++++++++++++-- .../tests/fixtures/plugin_socket_saturation.h | 62 ++++++++++ .../tests/fixtures/plugin_unix_socket.c | 2 + .../linux-socket-broker-candidate.md | 4 +- .../linux-socket-broker-capacity.md | 43 +++++++ .../complete-plugin-hook-runtime/tasks.md | 1 + .../build_socket_candidate.py | 8 +- 9 files changed, 258 insertions(+), 15 deletions(-) create mode 100644 crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h create mode 100644 openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md diff --git a/.github/workflows/plugin-linux-ipc.yml b/.github/workflows/plugin-linux-ipc.yml index 976b18a1..abcd41e6 100644 --- a/.github/workflows/plugin-linux-ipc.yml +++ b/.github/workflows/plugin-linux-ipc.yml @@ -8,6 +8,7 @@ on: - crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h - crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h - crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h + - crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h - crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs - crates/cyber-sandbox/native/** - crates/cyber-sandbox/src/plugin_policy.rs @@ -87,6 +88,7 @@ jobs: python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-thread" broker-no-thread python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-audit" broker-audit python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-no-cancel" broker-audit-no-cancel + python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-wrong-cap" broker-audit-wrong-cap for mode in mutation mutation-no-pin mutation-no-copy mutation-continue; do python3 scripts/probes/plugin-native-entry/build_socket_candidate.py "$RUNNER_TEMP/socket-broker-$mode" "broker-$mode" done @@ -103,6 +105,7 @@ jobs: target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-audit" "$RUNNER_TEMP/linux-ipc-broker-cancel.json" --broker-cancel target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-mutation" "$RUNNER_TEMP/linux-ipc-broker-mutation.json" --broker-mutation target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker" "$RUNNER_TEMP/linux-ipc-broker-escape.json" --broker-escape + target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-audit" "$RUNNER_TEMP/linux-ipc-broker-saturation.json" --broker-saturation - name: Reject blanket bind denial as broker acceptance shell: bash run: | @@ -188,6 +191,26 @@ jobs: for token in ['"escaped_session":true', '"controlled_stdin":true', '"stdout_closed_before_cleanup":false', '"owner_exited_before_cleanup":true']: assert token in text, text PY + - name: Reject an over-capacity socket worker pool + shell: bash + run: | + set -euo pipefail + if target/debug/examples/plugin_unix_socket_probe "$RUNNER_TEMP/socket-broker-wrong-cap" "$RUNNER_TEMP/linux-ipc-broker-capacity-control.json" --broker-saturation > "$RUNNER_TEMP/broker-wrong-cap.log" 2>&1; then + echo 'Wrong socket worker limit unexpectedly passed' >&2 + exit 1 + fi + python3 - "$RUNNER_TEMP/broker-wrong-cap.log" <<'PY' + from pathlib import Path + import json, sys + text = Path(sys.argv[1]).read_text() + marker = 'socket-worker cap did not backpressure: ' + assert marker in text, text + receipt = json.loads(text.split(marker, 1)[1].splitlines()[0]) + assert receipt['ready']['setup_complete'] is True, receipt + assert receipt['pressure']['blocked_count'] == 16, receipt + assert len(receipt['started']) == 20, receipt + assert receipt['pressure_cancelled'] is None, receipt + PY - name: Record pathname policy kernel availability id: abi shell: bash @@ -300,5 +323,6 @@ jobs: ${{ runner.temp }}/broker-no-cancel.log ${{ runner.temp }}/broker-mutation-*.log ${{ runner.temp }}/broker-no-pid.log + ${{ runner.temp }}/broker-wrong-cap.log if-no-files-found: warn retention-days: 14 diff --git a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs index af23cd83..8bf69eec 100644 --- a/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs +++ b/crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs @@ -71,6 +71,7 @@ mod probe { BrokerMutation, BrokerEscape, BrokerEscapeNoPid, + BrokerSaturation, Unix9, } impl Mode { @@ -83,6 +84,7 @@ mod probe { | Self::BrokerMutation | Self::BrokerEscape | Self::BrokerEscapeNoPid + | Self::BrokerSaturation ) } fn broker(self) -> bool { @@ -93,6 +95,7 @@ mod probe { | Self::BrokerMutation | Self::BrokerEscape | Self::BrokerEscapeNoPid + | Self::BrokerSaturation ) } fn escape(self) -> Option { @@ -190,6 +193,13 @@ mod probe { mode: Mode, ) -> Vec { let mut rows = Vec::new(); + if mode == Mode::BrokerSaturation && label == "outside" { + let observed = super::cancellation::observe_saturation(&policy); + rows.push( + json!({"mode":"saturation","socket":label,"fs_read":policy.fs_read, + "fs_write":policy.fs_write,"shell":false,"observed":observed}), + ); + } if let Some(private_pid) = mode.escape() && label == "outside" { @@ -234,6 +244,7 @@ mod probe { Some("--broker-mutation") => Mode::BrokerMutation, Some("--broker-escape") => Mode::BrokerEscape, Some("--broker-escape-no-pid") => Mode::BrokerEscapeNoPid, + Some("--broker-saturation") => Mode::BrokerSaturation, Some("--landlock9") => Mode::Unix9, _ => panic!("unknown candidate mode"), }; @@ -346,6 +357,7 @@ mod probe { "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h", "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", "crates/cyber-sandbox/src/plugin_policy.rs", "crates/cyber-sandbox/src/plugin_policy/linux.rs", @@ -373,9 +385,9 @@ mod probe { } let report = json!({"platform": std::env::consts::OS, "architecture": std::env::consts::ARCH, "checkout": std::env::var("GITHUB_SHA").ok(), "run_id": std::env::var("GITHUB_RUN_ID").ok(), - "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "mutation_probe": mode == Mode::BrokerMutation, "escape_probe": mode.escape(), "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), + "candidate": candidate, "landlock9": mode == Mode::Unix9, "socket_broker": mode.broker(), "cancellation_probe": mode == Mode::BrokerCancel, "mutation_probe": mode == Mode::BrokerMutation, "escape_probe": mode.escape(), "saturation_probe": mode == Mode::BrokerSaturation, "sources": sources, "source_sha256": format!("{:x}", hash.finalize()), "helper_sha256": format!("{:x}", Sha256::digest(std::fs::read(helper).unwrap())), - "rows": rows, "scope": "Fixture-owned UNIX reachability and candidate private IP exchange only. Experimental syscall broker when selected; no ambient service contact or production capability acceptance."}); + "rows": rows, "scope": "Fixture-owned socket, caller and worker observations in experimental helpers only. Proof rows are case-specific; no ambient service contact or production capability acceptance."}); std::fs::write( report_path, format!("{}\n", serde_json::to_string_pretty(&report).unwrap()), diff --git a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs index b83dcd44..969584d1 100644 --- a/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs +++ b/crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs @@ -18,6 +18,7 @@ enum Case { Cancellation, Mutation, Escape, + Saturation, } impl Case { fn argument(self) -> &'static str { @@ -25,6 +26,14 @@ impl Case { Self::Cancellation => "cancel", Self::Mutation => "mutation", Self::Escape => "escape", + Self::Saturation => "saturation", + } + } + fn handshakes(self) -> u8 { + match self { + Self::Escape => 0, + Self::Saturation => 2, + _ => 1, } } } @@ -73,6 +82,7 @@ struct Trace { ready: Option, finished: Option, cancelled: Option, + cancelled_workers: BTreeSet, pinned: Option, started: BTreeSet, reaped: BTreeSet, @@ -80,6 +90,8 @@ struct Trace { stdout_closed: bool, closed_before_cleanup: Option, owner_exited_before_cleanup: Option, + pressure: Option, + pressure_cancelled: Option, } impl Trace { fn record(&mut self, message: Message) -> Result<(), String> { @@ -96,8 +108,14 @@ impl Trace { fn output(&mut self, line: &str) -> Result<(), String> { let value: Value = serde_json::from_str(line).map_err(|e| e.to_string())?; let slot = match value["phase"].as_str() { - Some("cancel-ready" | "mutation-ready" | "escape-ready") => &mut self.ready, - Some("cancel-finished" | "mutation-finished") => &mut self.finished, + Some("cancel-ready" | "mutation-ready" | "escape-ready" | "saturation-ready") => { + &mut self.ready + } + Some("cancel-finished" | "mutation-finished" | "saturation-finished") => { + &mut self.finished + } + Some("saturation-filled") => &mut self.pressure, + Some("saturation-cancelled") => &mut self.pressure_cancelled, _ => return Err(format!("unexpected fixture phase: {value}")), }; if slot.is_some() { @@ -123,6 +141,7 @@ impl Trace { } "BCANCEL" => { self.cancelled = Some(pid); + self.cancelled_workers.insert(pid); } "BPIN" => { self.pinned = Some(pid); @@ -134,7 +153,10 @@ impl Trace { } Ok(()) } - fn can_release(&self, case: Case) -> bool { + fn can_release(&self, case: Case, handshake: u8) -> bool { + if matches!(case, Case::Saturation) { + return self.release_pressure(handshake); + } if matches!(case, Case::Escape) { return false; } @@ -156,16 +178,38 @@ impl Trace { && self.started.contains(&pid) && self.reaped.contains(&pid) } + fn release_pressure(&self, handshake: u8) -> bool { + match handshake { + 0 => { + self.ready + .as_ref() + .is_some_and(|v| v["setup_complete"] == true) + && self.started.len() == 3 + && self.started.is_subset(&self.reaped) + } + 1 => { + self.pressure_cancelled + .as_ref() + .is_some_and(|v| v["backpressure"] == true && v["interrupted_count"] == 16) + && self.started.len() == 19 + && self.cancelled_workers.len() == 16 + && self.cancelled_workers.is_subset(&self.reaped) + } + _ => false, + } + } fn report(&self) -> Value { json!({"ready":self.ready,"finished":self.finished,"cancelled_pid":self.cancelled, "pinned_tid":self.pinned,"started":self.started,"reaped":self.reaped,"audit":self.audit, - "stdout_closed_before_cleanup":self.closed_before_cleanup,"owner_exited_before_cleanup":self.owner_exited_before_cleanup}) + "stdout_closed_before_cleanup":self.closed_before_cleanup,"owner_exited_before_cleanup":self.owner_exited_before_cleanup, + "pressure":self.pressure,"pressure_cancelled":self.pressure_cancelled,"cancelled_workers":self.cancelled_workers}) } fn verify(&self, case: Case, released: bool, status: ExitStatus) { match case { Case::Cancellation => self.verify_cancel(released, status), Case::Mutation => self.verify_mutation(released, status), Case::Escape => self.verify_escape(status), + Case::Saturation => self.verify_pressure(released, status), } } fn verify_cancel(&self, released: bool, status: ExitStatus) { @@ -234,6 +278,40 @@ impl Trace { self.report() ); } + fn verify_pressure(&self, released: bool, status: ExitStatus) { + assert_eq!( + self.ready.as_ref().expect("missing pressure setup")["setup_complete"], + true + ); + assert_eq!( + self.pressure + .as_ref() + .expect("missing actual saturation wave")["blocked_count"], + 16, + "fixture did not establish 16 real blocked connects" + ); + assert!( + self.pressure_cancelled + .as_ref() + .is_some_and(|v| v["backpressure"] == true), + "socket-worker cap did not backpressure: {}", + self.report() + ); + assert!( + released, + "saturation cancellation wave was not reaped: {}", + self.report() + ); + assert!( + status.success() + && self + .finished + .as_ref() + .is_some_and(|v| v["tcp_after_saturation"] == true), + "socket-worker capacity was not recovered: {}", + self.report() + ); + } } fn settled( @@ -262,7 +340,7 @@ fn drive( input: &mut Option, rx: &Receiver, trace: &mut Trace, - released: &mut bool, + handshakes: &mut u8, case: Case, ) -> Result, String> { let deadline = Instant::now() + Duration::from_secs(30); @@ -271,13 +349,16 @@ fn drive( if let Ok(message) = rx.recv_timeout(Duration::from_millis(30)) { trace.record(message)?; } - if !*released && trace.can_release(case) { + if *handshakes < case.handshakes() && trace.can_release(case, *handshakes) { input - .take() + .as_mut() .ok_or("fixture input already closed")? .write_all(b"R") .map_err(|e| e.to_string())?; - *released = true; + *handshakes += 1; + if *handshakes == case.handshakes() { + drop(input.take()); + } } if let Some(status) = settled(child, trace, case, &mut exit)? { return Ok(Some(status)); @@ -296,6 +377,9 @@ pub fn observe_mutation(policy: &LinuxPolicy<'_>) -> Value { pub fn observe_escape(policy: &LinuxPolicy<'_>, private_pid: bool) -> Value { observe_case(policy, Case::Escape, private_pid) } +pub fn observe_saturation(policy: &LinuxPolicy<'_>) -> Value { + observe_case(policy, Case::Saturation, true) +} fn observe_case(policy: &LinuxPolicy<'_>, case: Case, private_pid: bool) -> Value { let args = [ policy.target_args[0].clone(), @@ -343,8 +427,15 @@ fn observe_case(policy: &LinuxPolicy<'_>, case: Case, private_pid: bool) -> Valu let stderr = reader(child.stderr.take().unwrap(), tx.clone(), false); drop(tx); let mut trace = Trace::default(); - let mut released = false; - let outcome = drive(&mut child, &mut input, &rx, &mut trace, &mut released, case); + let mut handshakes = 0; + let outcome = drive( + &mut child, + &mut input, + &rx, + &mut trace, + &mut handshakes, + case, + ); if matches!(case, Case::Escape) { trace.closed_before_cleanup = Some(trace.stdout_closed); trace.owner_exited_before_cleanup = @@ -371,6 +462,6 @@ fn observe_case(policy: &LinuxPolicy<'_>, case: Case, private_pid: bool) -> Valu "fixture protocol failed: {outcome:?} {errors:?}: {}", trace.report() ); - trace.verify(case, released, status); + trace.verify(case, handshakes == case.handshakes(), status); trace.report() } diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h b/crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h new file mode 100644 index 00000000..6e1f62d6 --- /dev/null +++ b/crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h @@ -0,0 +1,62 @@ +/* Independent fixture expectation; the wrong-cap control changes only the helper. */ +#ifdef __linux__ +#define FIXTURE_WORKER_LIMIT 16 +static int saturation_probe(void) { + struct sigaction action = { .sa_handler = cancel_signal }; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR1, &action, NULL) != 0) return 3; + int server = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + int filler = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (server < 0 || filler < 0) return 3; + struct sockaddr_in address = { .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + socklen_t length = sizeof(address); + if (bind(server, (struct sockaddr *)&address, length) != 0 || listen(server, 0) != 0 || + getsockname(server, (struct sockaddr *)&address, &length) != 0 || + connect(filler, (struct sockaddr *)&address, length) != 0) return 3; + puts("{\"phase\":\"saturation-ready\",\"setup_complete\":true}"); + fflush(stdout); + /* Wait until the owner has reaped all three setup workers. Otherwise a + * just-completed setup request could transiently consume one of the slots. */ + if (getchar() != 'R') return 3; + struct cancel_call calls[FIXTURE_WORKER_LIMIT] = {0}; + pthread_t threads[FIXTURE_WORKER_LIMIT]; + for (int i = 0; i < FIXTURE_WORKER_LIMIT; i++) { + calls[i].fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + int retries = 10; + if (calls[i].fd < 0 || setsockopt(calls[i].fd, IPPROTO_TCP, TCP_SYNCNT, &retries, sizeof(retries)) != 0) _exit(3); + calls[i].address = address; + if (pthread_create(&threads[i], NULL, cancel_connect, &calls[i]) != 0) _exit(3); + } + int blocked = 0; + for (int i = 0; i < FIXTURE_WORKER_LIMIT; i++) blocked += syn_pending(calls[i].fd); + printf("{\"phase\":\"saturation-filled\",\"blocked_count\":%d}\n", blocked); + fflush(stdout); + int extra = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (extra < 0) _exit(3); + int value = connect(extra, (struct sockaddr *)&address, length); + int backpressure = value == -1 && errno == EAGAIN; + close(extra); + for (int i = 0; i < FIXTURE_WORKER_LIMIT; i++) { + if (pthread_kill(threads[i], SIGUSR1) != 0) _exit(3); + } + int interrupted = 0; + for (int i = 0; i < FIXTURE_WORKER_LIMIT; i++) { + if (pthread_join(threads[i], NULL) != 0) _exit(3); + interrupted += calls[i].result == -1 && calls[i].error == EINTR; + } + printf("{\"phase\":\"saturation-cancelled\",\"backpressure\":%s,\"interrupted_count\":%d}\n", + backpressure ? "true" : "false", interrupted); + fflush(stdout); + /* Hold the socket preimages until every cancelled worker is acknowledged + * reaped. A fresh TCP exchange then verifies capacity is available again. */ + if (getchar() != 'R') _exit(3); + for (int i = 0; i < FIXTURE_WORKER_LIMIT; i++) close(calls[i].fd); + close(filler); close(server); + int alive = local_exchange(AF_INET, NULL, -1); + printf("{\"phase\":\"saturation-finished\",\"tcp_after_saturation\":%s}\n", alive ? "true" : "false"); + return blocked == FIXTURE_WORKER_LIMIT && backpressure && interrupted == FIXTURE_WORKER_LIMIT && alive ? 0 : 3; +} +#else +static int saturation_probe(void) { return 3; } +#endif diff --git a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c index 7187c41c..e646ba2f 100644 --- a/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c +++ b/crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c @@ -150,6 +150,7 @@ static int pair_probe(const struct sockaddr_un *address, socklen_t length, } #include "plugin_socket_cancel.h" +#include "plugin_socket_saturation.h" #include "plugin_socket_mutation.h" #include "plugin_process_escape.h" @@ -162,6 +163,7 @@ int main(int argc, char **argv) { if (argc == 4 && strcmp(argv[3], "cancel") == 0) return cancel_probe(); if (argc == 4 && strcmp(argv[3], "mutation") == 0) return mutation_probe(); if (argc == 4 && strcmp(argv[3], "escape") == 0) return escape_probe(); + if (argc == 4 && strcmp(argv[3], "saturation") == 0) return saturation_probe(); if (argc >= 4) { if (strcmp(argv[3], "pair") != 0) return 2; return pair_probe(&address, diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md index 06ba3878..216f1617 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-candidate.md @@ -79,6 +79,8 @@ before target exit, with source-bound native receipts on both architectures. The [post-inspection mutation fixture](linux-socket-broker-mutation.md) and its three controls have source-bound native receipts on both architectures. The [escaped-descendant namespace fixture](linux-plugin-descendant-proof.md) +has source-bound native receipts on both architectures. +The [trusted-worker capacity/recovery fixture](linux-socket-broker-capacity.md) still requires native execution. The kernel's [seccomp notification documentation](https://docs.kernel.org/userspace-api/seccomp_filter.html) @@ -95,7 +97,7 @@ shell blocks and embedded Python parse; formatting/diff checks, all 107 strict OpenSpec items and spec lint (zero errors, 25 warnings) pass. The native C SDK build emits two Seatbelt deprecation warnings. Linux candidate compilation, kernel behavior for each revision requires corresponding hosted x64/arm64 jobs. -The new escaped-descendant case/control still requires hosted results. The separate +The new trusted-worker capacity/recovery case/control still requires hosted results. The separate thread-specific receipts below establish that earlier case and corrected report label; the first candidate receipts cannot prove those later changes. diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md new file mode 100644 index 00000000..edf7809e --- /dev/null +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md @@ -0,0 +1,43 @@ +# Trusted socket-worker capacity and recovery fixture + +This extends the isolated [broker candidate](linux-socket-broker-candidate.md). +Its current internal pool limit is 16; this proof does not select a production +resource policy or claim to bound all guest processes, threads or kernel memory. + +The fixed C target fills its private loopback listener queue. It waits for the +Rust driver's first acknowledgement after all three setup workers are observed +normally reaped, so residual setup work cannot consume a capacity slot. It then +starts 16 pthread connects and requires TCP_INFO to report SYN_SENT for each. +The sockets select ten SYN retries and remain open throughout the held phase. + +A seventeenth blocking connect must return EAGAIN without another trusted worker. +The target interrupts all 16 callers with targeted non-restarting SIGUSR1 and +joins them, requiring 16 EINTR results. The driver retains each cancellation PID, +matches all 16 to normal waitpid receipts and confirms the expected 19 starts +(three setup plus 16 blocked requests) before sending a second acknowledgement. +Only then may the target close the held sockets and require a fresh private TCP +token exchange, establishing that capacity was recovered while it remains alive. + +The independent C fixture expects 16; it never reads the helper's compiled limit. +The precise control changes only the copied helper's limit to 17. That control +remains bounded and must show all 16 genuine blocked connects plus 20 worker +starts (three setup plus 17 blocked requests), then fail the named backpressure +assertion. A setup failure, fewer real blocked operations or failure to create +the extra worker cannot count as that counterexample. + +The shared audit driver now supports two fixed-byte acknowledgements. Its readers +retain their per-line and line-count bounds, and process/pipe cleanup occurs +before assertions, including the wrong-cap control's timeout. Existing scalar +cancellation, mutation and escaped-descendant cases still run in the same workflow +to check the shared driver changes. Audit markers remain temporary copied-helper +instrumentation for a fixed source-bound fixture, not an authenticated protocol +for arbitrary plugins. No ambient endpoint or credential is contacted. + +No production helper/composer, ordinary tool policy or native activation changes. +Network-side effects are not rolled back by worker cancellation. Admission, +other concurrency/signal/resource cases, runtime/platform support and the full +native design gate remain open. Local checks cover Rust example Clippy, portable +C syntax excluding Linux-specific code, thirteen temporary builder transformations +with compiler interception, workflow syntax, formatting/diff and OpenSpec. +Actual capacity/backpressure/recovery and the precise control still require +hosted Linux x64/arm64 execution. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index b5e7c5a7..8d5222c9 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -41,6 +41,7 @@ socket/runtime and real thread-local descriptor receipts on x64 and arm64, with the precise missing-thread control. Its [cancellation case and control](linux-socket-broker-cancellation.md) also have source-bound native receipts, as do the [post-inspection mutation case and controls](linux-socket-broker-mutation.md). Its [escaped-descendant namespace case and control](linux-plugin-descendant-proof.md) +also have source-bound native receipts. Its [trusted-worker capacity/recovery case and control](linux-socket-broker-capacity.md) still require fresh native execution. These experiments change no production policy or accepted support floor. diff --git a/scripts/probes/plugin-native-entry/build_socket_candidate.py b/scripts/probes/plugin-native-entry/build_socket_candidate.py index 7556af09..418e7f24 100644 --- a/scripts/probes/plugin-native-entry/build_socket_candidate.py +++ b/scripts/probes/plugin-native-entry/build_socket_candidate.py @@ -8,7 +8,7 @@ root = Path(__file__).resolve().parents[3] output = Path(sys.argv[1]).resolve() mode = sys.argv[2] if len(sys.argv) == 3 else "seccomp" -assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "broker-audit", "broker-audit-no-cancel", +assert mode in {"seccomp", "broker", "broker-no-bind", "broker-no-thread", "broker-audit", "broker-audit-no-cancel", "broker-audit-wrong-cap", "broker-mutation", "broker-mutation-no-pin", "broker-mutation-no-copy", "broker-mutation-continue", "landlock9", "landlock9-no-resolve"} unix9 = mode.startswith("landlock9") @@ -20,6 +20,12 @@ shutil.copyfile(Path(__file__).with_name(family), target / "socket_candidate.h") if mode.startswith("broker"): shutil.copyfile(Path(__file__).with_name("linux_socket_broker.h"), target / "socket_broker.h") + if mode == "broker-audit-wrong-cap": + broker = target / "socket_broker.h" + text = broker.read_text() + before = "#define BROKER_SLOTS 16" + assert text.count(before) == 1 + broker.write_text(text.replace(before, "#define BROKER_SLOTS 17")) if mode == "broker-no-thread": broker = target / "socket_broker.h" text = broker.read_text() From e6fd3d2f868c9e6680a80feb437334c385b879db Mon Sep 17 00:00:00 2001 From: Edvane Cristina Santos Daltro Date: Fri, 30 Oct 2026 07:52:33 -0400 Subject: [PATCH 50/50] fix(plugins): retain unconfirmed package worker ownership --- crates/cyber-sandbox/src/plugin_image.rs | 81 +- docs/goal-delivery.md | 8 +- .../image-preparation.md | 16 + .../linux-socket-broker-capacity.md | 20 +- .../complete-plugin-hook-runtime/tasks.md | 7 +- .../arm64/broker-wrong-cap.log | 4 + .../arm64/linux-ipc-broker-saturation.json | 1348 +++++++++++++++++ .../x64/broker-wrong-cap.log | 4 + .../x64/linux-ipc-broker-saturation.json | 1348 +++++++++++++++++ 9 files changed, 2823 insertions(+), 13 deletions(-) create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/broker-wrong-cap.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/linux-ipc-broker-saturation.json create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/broker-wrong-cap.log create mode 100644 scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/linux-ipc-broker-saturation.json diff --git a/crates/cyber-sandbox/src/plugin_image.rs b/crates/cyber-sandbox/src/plugin_image.rs index 5b752bd3..d94682c8 100644 --- a/crates/cyber-sandbox/src/plugin_image.rs +++ b/crates/cyber-sandbox/src/plugin_image.rs @@ -21,6 +21,8 @@ pub enum Error { Request, #[error("plugin package staging or helper IO failed")] Io, + #[error("plugin package helper exit could not be confirmed")] + Unconfirmed, #[error("plugin package helper refused the requested copy")] Refused, #[error("plugin package helper exceeded its output limit")] @@ -187,7 +189,7 @@ async fn supervise( let stderr = child.stderr.take().expect("piped helper stderr"); let capture = async { let (stdout, _) = tokio::try_join!(bounded(stdout, MAX_RESPONSE), bounded(stderr, 8192))?; - let status = child.wait().await.map_err(|_| Error::Io)?; + let status = child.wait().await.map_err(|_| Error::Unconfirmed)?; if !status.success() { return Err(Error::Refused); } @@ -202,13 +204,14 @@ async fn supervise( }; // On every terminal path, settle the exact process before releasing its // writable parent. The trusted worker cannot spawn descendants. - if child.try_wait().ok().flatten().is_none() { - let _ = child.start_kill(); - } - if child.wait().await.is_err() { + let unconfirmed = matches!(result, Some(Err(Error::Unconfirmed))); + if settle_helper(child, unconfirmed, tokio::process::Child::start_kill) + .await + .is_err() + { // Do not delete a namespace that might still have a live writer. Leaking // this protected parent is safer than removing it before a proven reap. - let _ = reply.send(Err(Error::Io)); + let _ = reply.send(Err(Error::Unconfirmed)); return; } let parent = retained.0.take().expect("owned helper parent"); @@ -218,6 +221,35 @@ async fn supervise( } } +#[cfg(unix)] +async fn settle_helper( + mut child: tokio::process::Child, + unconfirmed: bool, + terminate: impl FnOnce(&mut tokio::process::Child) -> std::io::Result<()>, +) -> Result<(), Error> { + // An observed wait error ends our authority to signal a cached PID. Do not + // repoll it or let Tokio's kill-on-drop/orphan reaper act on that identity. + if unconfirmed { + std::mem::forget(child); + return Err(Error::Unconfirmed); + } + match child.try_wait() { + Ok(Some(_)) => return Ok(()), + Ok(None) => { + let _ = terminate(&mut child); + } + Err(_) => { + std::mem::forget(child); + return Err(Error::Unconfirmed); + } + } + if child.wait().await.is_err() { + std::mem::forget(child); + return Err(Error::Unconfirmed); + } + Ok(()) +} + #[cfg(unix)] async fn bounded(stream: impl AsyncRead + Unpin, limit: usize) -> Result, Error> { let mut bytes = Vec::new(); @@ -310,6 +342,43 @@ mod tests { use super::*; use std::os::unix::fs::MetadataExt; + #[tokio::test] + #[allow(unsafe_code)] + async fn lost_wait_ownership_retains_staging_and_refuses_settlement() { + let parent = tempfile::tempdir().unwrap(); + let path = parent.path().to_path_buf(); + let retained = RetainUntilReaped(Some(parent)); + let child = tokio::process::Command::new("/usr/bin/true") + .kill_on_drop(true) + .spawn() + .unwrap(); + let pid = child.id().unwrap() as libc::pid_t; + // Reproduce a real ECHILD without polling Tokio's child future. Only + // this test's exact direct child is reaped; no process lookup is used. + let mut status = 0; + loop { + // SAFETY: status is writable and pid names our unreaped direct child. + let result = unsafe { libc::waitpid(pid, &mut status, 0) }; + if result == pid { + break; + } + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::EINTR) + ); + } + assert_eq!( + settle_helper(child, false, |_| panic!("must not signal an uncertain PID")).await, + Err(Error::Unconfirmed) + ); + drop(retained); + assert!( + path.is_dir(), + "unknown settlement must retain the namespace" + ); + std::fs::remove_dir(path).unwrap(); + } + #[test] fn workspace_boundary_refuses_existing_files_and_symlinks_without_modifying_them() { let namespace = tempfile::tempdir().unwrap(); diff --git a/docs/goal-delivery.md b/docs/goal-delivery.md index b41fed1c..1cbdd8f6 100644 --- a/docs/goal-delivery.md +++ b/docs/goal-delivery.md @@ -17,7 +17,10 @@ The user requested fewer simultaneously open PRs. Keep normally **one open goal PR**, with a **maximum of two** only when an independent change genuinely needs separate review. This changes delivery granularity, not feature scope or acceptance. -- Finish and verify a coherent production increment before opening the next PR. +- Finish and verify the agreed delivery increment before opening the next PR. + On 2026-10-30 the user authorized merging #76 as a foundations milestone once + its CI is green and merge review is satisfied, then continuing the full goal + in one follow-up PR. This milestone does not complete production enforcement. - Keep related experiments, evidence, implementation fixes and CI fixes in the active PR. Do not open a PR for each probe or supporting component. - Cancel superseded PR/revision CI runs after verifying their source is retained. @@ -39,7 +42,8 @@ separate review. This changes delivery granularity, not feature scope or accepta cap does not authorize closing unrelated PRs. - Preserve native proof and explicit support/design revision gates. Passing probes are not production feature acceptance. Never weaken the agreed contract merely - to finish a PR, and never merge automatically. + to finish a PR. Merge only with user authorization; the conditional approval + above applies to #76. As of consolidation, native confinement foundations and regressions exist, but production enforcement and the remaining plugin/hook scope are incomplete. The diff --git a/openspec/changes/complete-plugin-hook-runtime/image-preparation.md b/openspec/changes/complete-plugin-hook-runtime/image-preparation.md index 457d7f2d..fc4e073f 100644 --- a/openspec/changes/complete-plugin-hook-runtime/image-preparation.md +++ b/openspec/changes/complete-plugin-hook-runtime/image-preparation.md @@ -136,6 +136,22 @@ This exceptional retention is not normal cancellation cleanup or successful preparation, and requires later owner startup housekeeping after authority to remove stale generations is established. +An observed helper wait error is reported as `Error::Unconfirmed`. The supervisor +does not retry that uncertain wait or signal the cached PID, including through +Tokio's kill-on-drop or orphan reaper. It intentionally retains the child handle +and protected staging namespace. A real externally reaped direct-child regression +produces `ECHILD`, verifies that the termination action is never called, and checks +that the namespace survives. Ordinary live-worker cancellation still kills and +reaps before removing staging. This is a preparation-owner safeguard, not native +host admission or registry-wide quarantine. + +The merge-readiness fix passed all 66 sandbox library tests on local macOS, +including the real lost-wait regression, and sandbox library/test Clippy with +warnings denied. The native C build emits two deprecated Seatbelt SDK warnings. +Formatting, diff checks and 107 strict OpenSpec items passed; spec lint reported +zero errors and 25 warnings. Hosted platform checks must pass on the new head +before merging the foundations milestone. + A successful response must match the request destination, approved digest and bounds. The owner opens only directories, with no-follow on the image child, and compares the returned native directory identity. The resulting `Package` diff --git a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md index edf7809e..4402d2b0 100644 --- a/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md +++ b/openspec/changes/complete-plugin-hook-runtime/linux-socket-broker-capacity.md @@ -39,5 +39,21 @@ other concurrency/signal/resource cases, runtime/platform support and the full native design gate remain open. Local checks cover Rust example Clippy, portable C syntax excluding Linux-specific code, thirteen temporary builder transformations with compiler interception, workflow syntax, formatting/diff and OpenSpec. -Actual capacity/backpressure/recovery and the precise control still require -hosted Linux x64/arm64 execution. + +## Hosted result for ceecb479 + +[Run 41049265434](https://github.com/CyberdyneCorp/cybercode-cli/actions/runs/41049265434) +passed on Linux x64 and arm64, including the four capacity/backpressure/recovery +cases per architecture and the exact wrong-cap control. The shared cancellation, +mutation and escaped-descendant checks also passed in that run. These are isolated +candidate results, not production admission acceptance. + +The original saturation reports and wrong-cap logs are retained under +`scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/{x64,arm64}`. +They identify tested merge `7c357b89e0f40b5df6110710c31cc41651770e6e` and source +manifest SHA-256 `2f4e8a8a8e42dc07fb60cc6505887c3f4c81988b5d8e14ffc7eee491eb634887`. +The manifest was independently recomputed from that exact Git tree. Each report +has 36 rows, including four saturation rows with 16 blocked requests, backpressure, +16 interrupted and normally reaped workers, and a successful subsequent TCP +exchange. Each control shows 16 blocked callers and 20 starts before the named +backpressure failure. These receipts are not assigned to later revisions. diff --git a/openspec/changes/complete-plugin-hook-runtime/tasks.md b/openspec/changes/complete-plugin-hook-runtime/tasks.md index 8d5222c9..826a7af7 100644 --- a/openspec/changes/complete-plugin-hook-runtime/tasks.md +++ b/openspec/changes/complete-plugin-hook-runtime/tasks.md @@ -1,8 +1,9 @@ ## 1. Review and baseline Delivery rule updated by the user on 2026-10-29: normally one open goal PR, -maximum two only for independently reviewable work. Consolidate #70–#76 into -draft #76 against `main`; keep related probes, fixes and evidence in that PR. +maximum two only for independently reviewable work. #70–#75 were consolidated into +#76 against `main`. On 2026-10-30 the user authorized merging that foundations +milestone after green CI and merge review, then continuing in one follow-up PR. The full goal scope, order and acceptance gates remain unchanged. See [goal delivery rules](../../../docs/goal-delivery.md). @@ -42,7 +43,7 @@ the precise missing-thread control. Its [cancellation case and control](linux-so also have source-bound native receipts, as do the [post-inspection mutation case and controls](linux-socket-broker-mutation.md). Its [escaped-descendant namespace case and control](linux-plugin-descendant-proof.md) also have source-bound native receipts. Its [trusted-worker capacity/recovery case and control](linux-socket-broker-capacity.md) -still require fresh native execution. These +passed on x64/arm64 in run 41049265434, with source-bound receipts retained. These experiments change no production policy or accepted support floor. ## 3. Host contribution lifecycle diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/broker-wrong-cap.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/broker-wrong-cap.log new file mode 100644 index 00000000..cd5f2c99 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/broker-wrong-cap.log @@ -0,0 +1,4 @@ + +thread 'main' (8050) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:293:9: +socket-worker cap did not backpressure: {"ready":{"phase":"saturation-ready","setup_complete":true},"finished":null,"cancelled_pid":null,"pinned_tid":null,"started":[4,5,6,13,19,24,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"reaped":[4,5,6],"audit":["BSTART 4","BSTART 5","BREAP 4","BSTART 6","BREAP 5","BREAP 6","BSTART 13","BSTART 19","BSTART 24","BSTART 26","BSTART 27","BSTART 28","BSTART 29","BSTART 30","BSTART 31","BSTART 32","BSTART 33","BSTART 34","BSTART 35","BSTART 36","BSTART 37","BSTART 38","BSTART 39"],"stdout_closed_before_cleanup":null,"owner_exited_before_cleanup":null,"pressure":{"phase":"saturation-filled","blocked_count":16},"pressure_cancelled":null,"cancelled_workers":[]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/linux-ipc-broker-saturation.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/linux-ipc-broker-saturation.json new file mode 100644 index 00000000..f3ce17e9 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/arm64/linux-ipc-broker-saturation.json @@ -0,0 +1,1348 @@ +{ + "platform": "linux", + "architecture": "aarch64", + "checkout": "7c357b89e0f40b5df6110710c31cc41651770e6e", + "run_id": "41049265434", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": false, + "escape_probe": null, + "saturation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "2f4e8a8a8e42dc07fb60cc6505887c3f4c81988b5d8e14ffc7eee491eb634887", + "helper_sha256": "9a87557fa36eab0e716e5a2a67a2ae51979f97aff66cc437528dd03fdf56d139", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 13, + 19, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 13, + 19, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 13", + "BSTART 19", + "BSTART 25", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 13", + "BCANCEL 19", + "BCANCEL 25", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 13", + "BREAP 19", + "BREAP 25", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 13, + 19, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 13, + 19, + 24, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 13, + 19, + 24, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 13", + "BSTART 19", + "BSTART 24", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 13", + "BCANCEL 19", + "BCANCEL 24", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 13", + "BREAP 19", + "BREAP 24", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 13, + 19, + 24, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 14, + 21, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 14, + 21, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 14", + "BSTART 21", + "BSTART 25", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 14", + "BCANCEL 21", + "BCANCEL 25", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 14", + "BREAP 21", + "BREAP 25", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 14, + 21, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 13, + 18, + 23, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 13, + 18, + 23, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 13", + "BSTART 18", + "BSTART 23", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 13", + "BCANCEL 18", + "BCANCEL 23", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 13", + "BREAP 18", + "BREAP 23", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 13, + 18, + 23, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned socket, caller and worker observations in experimental helpers only. Proof rows are case-specific; no ambient service contact or production capability acceptance." +} diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/broker-wrong-cap.log b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/broker-wrong-cap.log new file mode 100644 index 00000000..2161b0bb --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/broker-wrong-cap.log @@ -0,0 +1,4 @@ + +thread 'main' (8165) panicked at crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs:293:9: +socket-worker cap did not backpressure: {"ready":{"phase":"saturation-ready","setup_complete":true},"finished":null,"cancelled_pid":null,"pinned_tid":null,"started":[4,5,6,12,17,21,25,27,28,29,30,31,32,33,34,35,36,37,38,39],"reaped":[4,5,6],"audit":["BSTART 4","BSTART 5","BREAP 4","BSTART 6","BREAP 5","BREAP 6","BSTART 12","BSTART 17","BSTART 21","BSTART 25","BSTART 27","BSTART 28","BSTART 29","BSTART 30","BSTART 31","BSTART 32","BSTART 33","BSTART 34","BSTART 35","BSTART 36","BSTART 37","BSTART 38","BSTART 39"],"stdout_closed_before_cleanup":null,"owner_exited_before_cleanup":null,"pressure":{"phase":"saturation-filled","blocked_count":16},"pressure_cancelled":null,"cancelled_workers":[]} +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/linux-ipc-broker-saturation.json b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/linux-ipc-broker-saturation.json new file mode 100644 index 00000000..77dc0081 --- /dev/null +++ b/scripts/probes/plugin-native-entry/evidence/linux-socket-broker-capacity-ceecb479/x64/linux-ipc-broker-saturation.json @@ -0,0 +1,1348 @@ +{ + "platform": "linux", + "architecture": "x86_64", + "checkout": "7c357b89e0f40b5df6110710c31cc41651770e6e", + "run_id": "41049265434", + "candidate": true, + "landlock9": false, + "socket_broker": true, + "cancellation_probe": false, + "mutation_probe": false, + "escape_probe": null, + "saturation_probe": true, + "sources": [ + "crates/cyber-sandbox/examples/plugin_unix_socket_probe.rs", + "crates/cyber-sandbox/tests/fixtures/plugin_unix_socket.c", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_cancel.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_mutation.h", + "crates/cyber-sandbox/tests/fixtures/plugin_process_escape.h", + "crates/cyber-sandbox/tests/fixtures/plugin_socket_saturation.h", + "crates/cyber-sandbox/examples/support/plugin_socket_cancel.rs", + "crates/cyber-sandbox/src/plugin_policy.rs", + "crates/cyber-sandbox/src/plugin_policy/linux.rs", + "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/src/native_exec.rs", + "crates/cyber-sandbox/src/bin/cyber-sandbox-exec.rs", + "crates/cyber-sandbox/build.rs", + "crates/cyber-sandbox/Cargo.toml", + "Cargo.lock", + ".github/workflows/plugin-linux-ipc.yml", + "scripts/probes/plugin-native-entry/linux_socket_candidate.h", + "scripts/probes/plugin-native-entry/linux_socket_broker.h", + "scripts/probes/plugin-native-entry/linux_socket_broker_mutation.h", + "scripts/probes/plugin-native-entry/linux_socket_landlock9.h", + "scripts/probes/plugin-native-entry/linux_unix9_policy.h", + "scripts/probes/plugin-native-entry/build_socket_candidate.py" + ], + "source_sha256": "2f4e8a8a8e42dc07fb60cc6505887c3f4c81988b5d8e14ffc7eee491eb634887", + "helper_sha256": "ca1c090ee7a3eb939f9591bfa247bc89e9cc4f09efa1a07f93d703f5818fe5c1", + "rows": [ + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 13, + 18, + 21, + 25, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 13, + 18, + 21, + 25, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BREAP 4", + "BSTART 5", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 13", + "BSTART 18", + "BSTART 21", + "BSTART 25", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 13", + "BCANCEL 18", + "BCANCEL 21", + "BCANCEL 25", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 13", + "BREAP 18", + "BREAP 21", + "BREAP 25", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 13, + 18, + 21, + 25, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 13, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 13, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 13", + "BSTART 18", + "BSTART 22", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 13", + "BCANCEL 18", + "BCANCEL 22", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 13", + "BREAP 18", + "BREAP 22", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BSTART 41", + "BREAP 40", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 13, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 12, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 12, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 12", + "BSTART 18", + "BSTART 22", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 12", + "BCANCEL 18", + "BCANCEL 22", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 12", + "BREAP 18", + "BREAP 22", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BREAP 40", + "BSTART 41", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 12, + 18, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "mode": "saturation", + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "observed": { + "ready": { + "phase": "saturation-ready", + "setup_complete": true + }, + "finished": { + "phase": "saturation-finished", + "tcp_after_saturation": true + }, + "cancelled_pid": 38, + "pinned_tid": null, + "started": [ + 4, + 5, + 6, + 12, + 14, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "reaped": [ + 4, + 5, + 6, + 12, + 14, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41 + ], + "audit": [ + "BSTART 4", + "BSTART 5", + "BREAP 4", + "BSTART 6", + "BREAP 5", + "BREAP 6", + "BSTART 12", + "BSTART 14", + "BSTART 22", + "BSTART 26", + "BSTART 27", + "BSTART 28", + "BSTART 29", + "BSTART 30", + "BSTART 31", + "BSTART 32", + "BSTART 33", + "BSTART 34", + "BSTART 35", + "BSTART 36", + "BSTART 37", + "BSTART 38", + "BCANCEL 12", + "BCANCEL 14", + "BCANCEL 22", + "BCANCEL 26", + "BCANCEL 27", + "BCANCEL 28", + "BCANCEL 29", + "BCANCEL 30", + "BCANCEL 31", + "BCANCEL 32", + "BCANCEL 33", + "BCANCEL 34", + "BCANCEL 35", + "BCANCEL 36", + "BCANCEL 37", + "BCANCEL 38", + "BREAP 12", + "BREAP 14", + "BREAP 22", + "BREAP 26", + "BREAP 27", + "BREAP 28", + "BREAP 29", + "BREAP 30", + "BREAP 31", + "BREAP 32", + "BREAP 33", + "BREAP 34", + "BREAP 35", + "BREAP 36", + "BREAP 37", + "BREAP 38", + "BSTART 39", + "BSTART 40", + "BREAP 39", + "BSTART 41", + "BREAP 40", + "BREAP 41" + ], + "stdout_closed_before_cleanup": null, + "owner_exited_before_cleanup": null, + "pressure": { + "phase": "saturation-filled", + "blocked_count": 16 + }, + "pressure_cancelled": { + "phase": "saturation-cancelled", + "backpressure": true, + "interrupted_count": 16 + }, + "cancelled_workers": [ + 12, + 14, + 22, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + } + }, + { + "socket": "outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "public", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "private-mask", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": false, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": false, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": false, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": false, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "errno": 1, + "project_read": true, + "landlock_abi": 8 + } + }, + { + "socket": "symlink-outside", + "fs_read": true, + "fs_write": true, + "shell": false, + "mode": "pair", + "host_received_fixture": false, + "observed": { + "started": true, + "connected": false, + "sent": false, + "project_read": true, + "pair_roundtrip": true, + "pair_disconnect": false, + "pair_reconnect": false, + "pair_bind": false, + "pair_listen": false, + "pair_sendto": false, + "pair_sendmsg": false, + "datagram_pair": false, + "seqpacket_pair": false, + "landlock_abi": 8, + "tcp_roundtrip": true, + "thread_tcp_roundtrip": true, + "scratch_unix_roundtrip": false + } + } + ], + "scope": "Fixture-owned socket, caller and worker observations in experimental helpers only. Proof rows are case-specific; no ambient service contact or production capability acceptance." +}