mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
[pitboss] sweep after phase 04: 3 deferred items resolved
This commit is contained in:
parent
3ffe480660
commit
84638e7d57
6 changed files with 184 additions and 28 deletions
|
|
@ -0,0 +1,26 @@
|
|||
"""Positive control: verify the escape-detection mechanism itself.
|
||||
|
||||
Writes to /proc/sysrq-trigger, which requires CAP_SYS_ADMIN. This fixture
|
||||
is expected to SUCCEED (print NYX_ESCAPE_SUCCESS) when run in a privileged
|
||||
container (--cap-add=SYS_ADMIN) and to FAIL when run with --cap-drop=ALL.
|
||||
|
||||
CI usage (escape-positive-control job):
|
||||
Run under a container started with --cap-add=SYS_ADMIN.
|
||||
Assert NYX_ESCAPE_SUCCESS appears in output.
|
||||
If it does NOT appear, the test mechanism is broken or the capability
|
||||
was not granted, and the CI job fails.
|
||||
|
||||
Normal escape suite: this fixture is NOT included. The positive control test
|
||||
(positive_control_cap_sys_admin in dynamic_sandbox_escape.rs) is #[ignore]d
|
||||
and only un-ignored in the dedicated escape-positive-control CI job.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open("/proc/sysrq-trigger", "w") as f:
|
||||
f.write("h") # 'h' = print help, harmless
|
||||
print("NYX_ESCAPE_SUCCESS: CAP_SYS_ADMIN active, wrote to /proc/sysrq-trigger")
|
||||
sys.exit(0)
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"BLOCKED: CAP_SYS_ADMIN not present: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
"""Escape attempt: open /dev/mem to read physical memory.
|
||||
|
||||
Requires CAP_SYS_RAWIO. Expected outcome: PermissionError.
|
||||
Requires CAP_SYS_RAWIO (and typically root). With --cap-drop=ALL and
|
||||
--user 65534:65534, both privilege escalation (setuid) and device access
|
||||
are blocked by distinct kernel checks, exercising two security layers.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Attempt privilege escalation first (tests CAP_SETUID independently).
|
||||
# With --cap-drop=ALL, setuid(0) requires CAP_SETUID — also dropped.
|
||||
if os.getuid() != 0:
|
||||
try:
|
||||
os.setuid(0)
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"BLOCKED (setuid): {e}", file=sys.stderr)
|
||||
# Fall through: also probe /dev/mem directly to test CAP_SYS_RAWIO.
|
||||
|
||||
try:
|
||||
with open("/dev/mem", "rb") as f:
|
||||
data = f.read(64)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
"""Escape attempt: trigger kernel SysRq action via /proc/sysrq-trigger.
|
||||
|
||||
Expected outcome: PermissionError (read-only rootfs or missing CAP_SYS_ADMIN).
|
||||
Requires CAP_SYS_ADMIN (and typically root). With --cap-drop=ALL and
|
||||
--user 65534:65534, both privilege escalation (setuid) and the sysrq
|
||||
write are blocked by distinct kernel checks.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Attempt privilege escalation first (tests CAP_SETUID independently).
|
||||
# With --cap-drop=ALL, setuid(0) requires CAP_SETUID — also dropped.
|
||||
if os.getuid() != 0:
|
||||
try:
|
||||
os.setuid(0)
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"BLOCKED (setuid): {e}", file=sys.stderr)
|
||||
# Fall through: also probe /proc/sysrq-trigger to test CAP_SYS_ADMIN.
|
||||
|
||||
try:
|
||||
with open("/proc/sysrq-trigger", "w") as f:
|
||||
f.write("h") # 'h' = print help (harmless but requires access)
|
||||
|
|
|
|||
|
|
@ -226,6 +226,95 @@ mod escape_tests {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Positive control test ─────────────────────────────────────────────────
|
||||
|
||||
/// Positive control: verify the escape-detection mechanism itself.
|
||||
///
|
||||
/// Runs `cap_sys_admin_positive_control.py` inside a container started with
|
||||
/// `--cap-add=SYS_ADMIN` and asserts that `NYX_ESCAPE_SUCCESS` is detected
|
||||
/// in the output. If it is not detected, either the test mechanism is broken
|
||||
/// or the capability was not granted.
|
||||
///
|
||||
/// This test is `#[ignore]`d in the normal escape suite. It is un-ignored
|
||||
/// in the dedicated `escape-positive-control` CI job:
|
||||
///
|
||||
/// cargo nextest run --all-features --test dynamic_sandbox_escape \
|
||||
/// -- --include-ignored positive_control_cap_sys_admin
|
||||
#[test]
|
||||
#[ignore = "positive control: run only under --cap-add=SYS_ADMIN (escape-positive-control CI job)"]
|
||||
fn positive_control_cap_sys_admin() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (_tmpdir, _harness) = harness_for_fixture("cap_sys_admin_positive_control.py");
|
||||
let workdir_str = _tmpdir.path().to_string_lossy().to_string();
|
||||
|
||||
// Start a container with CAP_SYS_ADMIN to validate escape detection.
|
||||
// This is intentionally privileged — it IS the escape we're detecting.
|
||||
let container_name = format!("nyx-posctl-{}", std::process::id());
|
||||
let status = std::process::Command::new("docker")
|
||||
.args([
|
||||
"run", "-d", "--rm",
|
||||
"--name", &container_name,
|
||||
"--cap-add=SYS_ADMIN",
|
||||
"--network", "none",
|
||||
"python:3-slim",
|
||||
"sleep", "60",
|
||||
])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.expect("docker run");
|
||||
|
||||
if !status.success() {
|
||||
// Container failed to start (image unavailable or docker error).
|
||||
// Accept — this is a best-effort gate, not a hard requirement here.
|
||||
return;
|
||||
}
|
||||
|
||||
// Create /workdir and copy the fixture in.
|
||||
let _ = std::process::Command::new("docker")
|
||||
.args(["exec", &container_name, "mkdir", "-p", "/workdir"])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
|
||||
let cp_src = format!("{workdir_str}/.");
|
||||
let cp_dst = format!("{container_name}:/workdir");
|
||||
let _ = std::process::Command::new("docker")
|
||||
.args(["cp", &cp_src, &cp_dst])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
|
||||
// Run the fixture and capture output.
|
||||
let out = std::process::Command::new("docker")
|
||||
.args([
|
||||
"exec", &container_name,
|
||||
"python3", "/workdir/cap_sys_admin_positive_control.py",
|
||||
])
|
||||
.output()
|
||||
.expect("docker exec positive control");
|
||||
|
||||
// Cleanup the container immediately.
|
||||
let _ = std::process::Command::new("docker")
|
||||
.args(["stop", "--time=0", &container_name])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
|
||||
let stdout = std::str::from_utf8(&out.stdout).unwrap_or("");
|
||||
let stderr = std::str::from_utf8(&out.stderr).unwrap_or("");
|
||||
|
||||
assert!(
|
||||
stdout.contains("NYX_ESCAPE_SUCCESS") || stderr.contains("NYX_ESCAPE_SUCCESS"),
|
||||
"positive control failed: NYX_ESCAPE_SUCCESS not detected with CAP_SYS_ADMIN\n\
|
||||
This means the test mechanism cannot detect actual escapes.\n\
|
||||
stdout: {stdout}\nstderr: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Docker exec reuse test ────────────────────────────────────────────────
|
||||
|
||||
/// Verify that the second payload for the same spec_hash reuses the running
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue