mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0006 (20260517T044708Z-e058)
This commit is contained in:
parent
0ec9a9b425
commit
356fcaf71e
5 changed files with 133 additions and 26 deletions
|
|
@ -12,6 +12,7 @@
|
|||
//! Failed-build retry policy (§12 Q4): one retry on `BuildFailed` with
|
||||
//! backoff (1s, 4s), then `Inconclusive(BuildFailed, attempts: 2)`.
|
||||
|
||||
use crate::dynamic::sandbox::ProcessHardeningProfile;
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
use blake3::Hasher;
|
||||
use directories::ProjectDirs;
|
||||
|
|
@ -817,8 +818,13 @@ fn compute_php_lockfile_hash(workdir: &Path) -> String {
|
|||
/// `cc -O0 -g -o nyx_harness main.c` in `workdir`.
|
||||
///
|
||||
/// Build isolation is NOT yet implemented (deferred). `cc` runs on the host.
|
||||
pub fn prepare_c(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, BuildError> {
|
||||
let source_hash = compute_c_source_hash(workdir);
|
||||
pub fn prepare_c(
|
||||
spec: &HarnessSpec,
|
||||
workdir: &Path,
|
||||
profile: ProcessHardeningProfile,
|
||||
) -> Result<BuildResult, BuildError> {
|
||||
let static_link = static_link_for_profile(profile);
|
||||
let source_hash = compute_c_source_hash(workdir, static_link);
|
||||
let cache_path = build_cache_path(&source_hash, "c", &spec.toolchain_id)?;
|
||||
|
||||
let binary = cache_path.join("nyx_harness");
|
||||
|
|
@ -842,7 +848,7 @@ pub fn prepare_c(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, Buil
|
|||
let _ = std::fs::remove_dir_all(&cache_path);
|
||||
std::fs::create_dir_all(&cache_path)?;
|
||||
|
||||
match try_build_c_binary(workdir, &binary) {
|
||||
match try_build_c_binary(workdir, &binary, static_link) {
|
||||
Ok(()) => {
|
||||
return Ok(BuildResult {
|
||||
venv_path: cache_path,
|
||||
|
|
@ -860,18 +866,18 @@ pub fn prepare_c(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, Buil
|
|||
Err(BuildError::BuildFailed { stderr: last_err, attempts: MAX_ATTEMPTS })
|
||||
}
|
||||
|
||||
fn try_build_c_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> {
|
||||
fn try_build_c_binary(workdir: &Path, binary_dest: &Path, static_link: bool) -> Result<(), String> {
|
||||
let cc_bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
|
||||
// When `NYX_BUILD_STATIC=1` (typically set by the Linux Strict-profile
|
||||
// path so the harness survives `chroot(workdir)`), try `cc -static`
|
||||
// first. Fall back to the dynamic link if static fails — the host may
|
||||
// lack `libc.a` (musl-cross or `libc6-dev` are the usual sources) and
|
||||
// a dynamic-linked binary still works for non-chroot runs. The
|
||||
// fallback is announced via `NYX_BUILD_STATIC_FALLBACK=1` so downstream
|
||||
// chroot-acceptance tests can skip the leg they need static linking
|
||||
// for instead of asserting against a broken harness.
|
||||
if static_link_requested() {
|
||||
// When the Linux Strict-profile path requests it (or an operator sets
|
||||
// `NYX_BUILD_STATIC=1`), try `cc -static` first so the harness survives
|
||||
// `chroot(workdir)`. Fall back to the dynamic link if static fails —
|
||||
// the host may lack `libc.a` (musl-cross or `libc6-dev` are the usual
|
||||
// sources) and a dynamic-linked binary still works for non-chroot runs.
|
||||
// The fallback is announced via `NYX_BUILD_STATIC_FALLBACK=1` so
|
||||
// downstream chroot-acceptance tests can skip the leg they need static
|
||||
// linking for instead of asserting against a broken harness.
|
||||
if static_link {
|
||||
match run_cc(&cc_bin, workdir, binary_dest, &["-static", "-O0", "-g"]) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(stderr) => {
|
||||
|
|
@ -885,7 +891,25 @@ fn try_build_c_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String>
|
|||
run_cc(&cc_bin, workdir, binary_dest, &["-O0", "-g"])
|
||||
}
|
||||
|
||||
fn static_link_requested() -> bool {
|
||||
/// Decide whether the C harness should be linked with `-static`.
|
||||
///
|
||||
/// Returns `true` when the caller's hardening profile is
|
||||
/// [`ProcessHardeningProfile::Strict`] — chroot to the workdir hides the
|
||||
/// host's `/lib`/`/lib64` from the dynamic loader, so a dynamic-linked
|
||||
/// binary aborts before `main()`. Operators can also force the static
|
||||
/// path on a `Standard` run via `NYX_BUILD_STATIC=1` (or `=true`) without
|
||||
/// flipping the wider hardening profile.
|
||||
pub(crate) fn static_link_for_profile(profile: ProcessHardeningProfile) -> bool {
|
||||
if profile == ProcessHardeningProfile::Strict {
|
||||
return true;
|
||||
}
|
||||
static_link_env_override()
|
||||
}
|
||||
|
||||
/// Manual operator override read from `NYX_BUILD_STATIC`. Lives separately
|
||||
/// from [`static_link_for_profile`] so the env-var contract stays testable
|
||||
/// without standing up a full `ProcessHardeningProfile` plumb.
|
||||
pub(crate) fn static_link_env_override() -> bool {
|
||||
matches!(
|
||||
std::env::var("NYX_BUILD_STATIC").as_deref(),
|
||||
Ok("1") | Ok("true")
|
||||
|
|
@ -912,7 +936,7 @@ fn run_cc(cc_bin: &str, workdir: &Path, binary_dest: &Path, leading_flags: &[&st
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_c_source_hash(workdir: &Path) -> String {
|
||||
fn compute_c_source_hash(workdir: &Path, static_link: bool) -> String {
|
||||
let mut h = Hasher::new();
|
||||
for fname in &["main.c", "entry.c", "Makefile"] {
|
||||
if let Ok(content) = std::fs::read(workdir.join(fname)) {
|
||||
|
|
@ -923,7 +947,7 @@ fn compute_c_source_hash(workdir: &Path) -> String {
|
|||
// Fold the static-link toggle into the cache key so a single workdir
|
||||
// can produce both a static and a dynamic binary without one shadowing
|
||||
// the other in the cache (`prepare_c` keys on this hash).
|
||||
if static_link_requested() {
|
||||
if static_link {
|
||||
h.update(b"static");
|
||||
}
|
||||
let out = h.finalize();
|
||||
|
|
@ -1377,17 +1401,19 @@ mod tests {
|
|||
fn unset_env_means_dynamic_link() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let _g = EnvGuard::set(None);
|
||||
assert!(!static_link_requested());
|
||||
assert!(!static_link_env_override());
|
||||
assert!(!static_link_for_profile(ProcessHardeningProfile::Standard));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truthy_env_requests_static_link() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let _g = EnvGuard::set(Some("1"));
|
||||
assert!(static_link_requested());
|
||||
assert!(static_link_env_override());
|
||||
assert!(static_link_for_profile(ProcessHardeningProfile::Standard));
|
||||
|
||||
let _g2 = EnvGuard::set(Some("true"));
|
||||
assert!(static_link_requested());
|
||||
assert!(static_link_env_override());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1396,28 +1422,67 @@ mod tests {
|
|||
for value in &["0", "false", "yes", "static", ""] {
|
||||
let _g = EnvGuard::set(Some(value));
|
||||
assert!(
|
||||
!static_link_requested(),
|
||||
!static_link_env_override(),
|
||||
"value {value:?} must not request static link",
|
||||
);
|
||||
assert!(
|
||||
!static_link_for_profile(ProcessHardeningProfile::Standard),
|
||||
"value {value:?} must not request static link via Standard profile",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_profile_forces_static_link() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
// Even with the env var absent, Strict must pick the static
|
||||
// leg so chroot(workdir) does not strand the dynamic loader.
|
||||
let _g = EnvGuard::set(None);
|
||||
assert!(static_link_for_profile(ProcessHardeningProfile::Strict));
|
||||
|
||||
// Env var off should not flip Strict back to dynamic.
|
||||
let _g2 = EnvGuard::set(Some("0"));
|
||||
assert!(static_link_for_profile(ProcessHardeningProfile::Strict));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_hash_includes_static_marker() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(dir.path().join("main.c"), "int main(){return 0;}").unwrap();
|
||||
|
||||
let _g = EnvGuard::set(None);
|
||||
let dyn_hash = compute_c_source_hash(dir.path());
|
||||
|
||||
let _g2 = EnvGuard::set(Some("1"));
|
||||
let static_hash = compute_c_source_hash(dir.path());
|
||||
let dyn_hash = compute_c_source_hash(dir.path(), false);
|
||||
let static_hash = compute_c_source_hash(dir.path(), true);
|
||||
|
||||
assert_ne!(
|
||||
dyn_hash, static_hash,
|
||||
"static and dynamic builds must key into different cache slots",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_profile_and_standard_profile_produce_distinct_cache_keys() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(dir.path().join("main.c"), "int main(){return 0;}").unwrap();
|
||||
|
||||
// No env override; the static bit is derived from the profile.
|
||||
let _g = EnvGuard::set(None);
|
||||
let standard_hash = compute_c_source_hash(
|
||||
dir.path(),
|
||||
static_link_for_profile(ProcessHardeningProfile::Standard),
|
||||
);
|
||||
let strict_hash = compute_c_source_hash(
|
||||
dir.path(),
|
||||
static_link_for_profile(ProcessHardeningProfile::Strict),
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
standard_hash, strict_hash,
|
||||
"Strict-profile builds must key into a different cache slot \
|
||||
from Standard-profile builds so a chroot-bound static binary \
|
||||
does not shadow the dynamic one (or vice versa)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,7 +257,10 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
|||
}
|
||||
Lang::C => {
|
||||
// Compile the harness binary with `cc -o nyx_harness main.c`.
|
||||
match build_sandbox::prepare_c(spec, &harness.workdir) {
|
||||
// Pass the sandbox profile so the build chooses `-static` when
|
||||
// the run will chroot into `harness.workdir` and the dynamic
|
||||
// loader would otherwise miss `/lib*`.
|
||||
match build_sandbox::prepare_c(spec, &harness.workdir, opts.process_hardening) {
|
||||
Ok(build_result) => {
|
||||
let binary = build_result.venv_path.join("nyx_harness");
|
||||
if binary.exists() {
|
||||
|
|
|
|||
|
|
@ -168,4 +168,24 @@ mod tests {
|
|||
assert!(nrs.contains(&write));
|
||||
assert!(nrs.contains(&close));
|
||||
}
|
||||
|
||||
/// `BASE` carries the interpreter cold-start trio:
|
||||
/// `socketpair` (Node worker init), `umask` (Python tempfile init),
|
||||
/// `setrlimit` (older glibc fallback for `prlimit64`). Without these
|
||||
/// a Python or Node harness aborts before printing a single line and
|
||||
/// the Confirmed-via-`verify_finding` path is structurally
|
||||
/// unreachable, so a regression that drops one is a load-bearing
|
||||
/// outage rather than a code-cleanliness slip.
|
||||
#[test]
|
||||
fn base_allows_interpreter_cold_start_syscalls() {
|
||||
let nrs = allowed_syscall_numbers(0);
|
||||
for name in ["socketpair", "umask", "setrlimit"] {
|
||||
let nr = syscall_number(name)
|
||||
.unwrap_or_else(|| panic!("{name} missing from per-arch syscall map"));
|
||||
assert!(
|
||||
nrs.contains(&nr),
|
||||
"BASE allowlist must include {name} (interpreter cold-start)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,19 @@ allow = [
|
|||
"sched_yield",
|
||||
"prctl",
|
||||
"membarrier",
|
||||
# Interpreter cold-start additions. These are universal enough that
|
||||
# cap-gating them buys nothing while breaking real harnesses:
|
||||
# - `socketpair(AF_UNIX, ...)` — Node v18+ binds an internal worker
|
||||
# thread via an anonymous Unix-domain pair; not a network reach.
|
||||
# - `umask` — Python's `tempfile` calls it during stdlib init; only
|
||||
# mutates the calling process's file-creation mask.
|
||||
# - `setrlimit` — older glibc `__libc_setrlimit` shims fall through to
|
||||
# the legacy syscall instead of `prlimit64`; the caller can only
|
||||
# lower its own limits (raise is gated by the hard limit set by the
|
||||
# parent before exec).
|
||||
"socketpair",
|
||||
"umask",
|
||||
"setrlimit",
|
||||
]
|
||||
|
||||
[cap.SQL_QUERY]
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ pub fn syscall_number(name: &str) -> Option<u32> {
|
|||
"listen" => 50,
|
||||
"getsockname" => 51,
|
||||
"getpeername" => 52,
|
||||
"socketpair" => 53,
|
||||
"setsockopt" => 54,
|
||||
"getsockopt" => 55,
|
||||
"clone" => 56,
|
||||
|
|
@ -77,11 +78,13 @@ pub fn syscall_number(name: &str) -> Option<u32> {
|
|||
"readlink" => 89,
|
||||
"fchmod" => 91,
|
||||
"fchown" => 93,
|
||||
"umask" => 95,
|
||||
"getuid" => 102,
|
||||
"getgid" => 104,
|
||||
"geteuid" => 107,
|
||||
"getegid" => 108,
|
||||
"sigaltstack" => 131,
|
||||
"setrlimit" => 160,
|
||||
"arch_prctl" => 158,
|
||||
"gettid" => 186,
|
||||
"futex" => 202,
|
||||
|
|
@ -231,6 +234,8 @@ pub fn syscall_number(name: &str) -> Option<u32> {
|
|||
"wait4" => 260,
|
||||
"prlimit64" => 261,
|
||||
"getrlimit" => 163,
|
||||
"setrlimit" => 164,
|
||||
"umask" => 166,
|
||||
"prctl" => 167,
|
||||
"fchmod" => 52,
|
||||
"fchmodat" => 53,
|
||||
|
|
@ -241,6 +246,7 @@ pub fn syscall_number(name: &str) -> Option<u32> {
|
|||
"getgid" => 176,
|
||||
"getegid" => 177,
|
||||
"socket" => 198,
|
||||
"socketpair" => 199,
|
||||
"bind" => 200,
|
||||
"listen" => 201,
|
||||
"accept" => 202,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue