Commit Graph

1432 Commits

Author SHA1 Message Date
Robert Swiecki
30d8e4013f Merge branch 'master' of github.com:google/nsjail 2026-08-26 20:09:35 +02:00
Robert Swiecki
5d53b5c28b Merge pull request #282 from carrerasdarren-cell/nstun-cloud-local-destinations
nstun: block cloud-local service destinations
2026-08-26 20:08:34 +02:00
robertswiecki
8133324414 Merge pull request #288 from carrerasdarren-cell/fix/fail-closed-setgroups
user: fail closed when supplementary groups cannot be set
2026-08-26 20:05:57 +02:00
robertswiecki
16970c3fd9 Merge pull request #285 from h1-mrz/fail-loud-seccomp-unotify
sandbox: fail loudly when seccomp_unotify is requested but unusable
2026-08-26 20:02:59 +02:00
Robert Swiecki
79d12b54f2 Merge pull request #289 from carrerasdarren-cell/fix/nstun-policy-validation
nstun: reject invalid policy rule fields
2026-08-26 11:48:27 +02:00
Robert Swiecki
1a923df739 nstun: drop IPv6 unspecified and multicast packets in SSRF gate 2026-08-26 11:45:35 +02:00
robertswiecki
2ed350a0f2 Merge pull request #302 from mustafagozhamam/fix/nstun-parent-lifecycle
Fix NSTUN parent lifecycle cleanup
2026-08-26 09:23:13 +02:00
robertswiecki
c398ceda2f Merge pull request #300 from skaiea13-ai/codex/nstun-aggregate-budget
Limit nstun TCP receive buffers with a shared payload budget
2026-08-25 21:20:12 +02:00
robertswiecki
62f41cbfb7 Merge pull request #306 from Trithem90/fix/nstun-any-proxy-icmp
nstun: fail closed for proxy actions matched by ICMP
2026-08-25 21:19:00 +02:00
robertswiecki
3bd39a9acf Merge pull request #303 from Trithem90/fix/nstun-host-to-guest-policy
nstun: enforce HOST_TO_GUEST policy on inbound TCP and UDP
2026-08-25 21:17:38 +02:00
robertswiecki
f9a32c024b Merge pull request #308 from Nexory/unotify-trace-path-siblings
unotify: emit syscall numbers in the generated policy, and trace 11 missing path syscalls
2026-08-25 21:17:02 +02:00
robertswiecki
a36f97f24d Merge pull request #278 from sravan27/harden-cgroup-memory-cleanup
cgroup: clean up swap-only memory cgroups
2026-08-25 21:16:05 +02:00
robertswiecki
40a34d0170 Merge pull request #286 from srkyn/harden-no-new-privs-failure
contain: stop setup if PR_SET_NO_NEW_PRIVS fails
2026-08-25 21:15:26 +02:00
robertswiecki
c39f43ddcb Merge pull request #304 from Trithem90/fix/nstun-zero-network-cidr
nstun: preserve zero-network CIDR selectors
2026-08-25 21:14:35 +02:00
robertswiecki
d0363ed7d2 Merge pull request #280 from g0w6y/nstun_tcp_ack_window_validation
nstun/tcp: validate guest ACK numbers against the send window
2026-08-25 15:54:34 +02:00
Nexory
935cd6e0b8 unotify: trace faccessat2, truncate, statfs and the xattr syscalls
These path based syscalls are not in kTracedSyscalls, and the generated policy
ends in DEFAULT ALLOW, so they run unobserved and never reach the report.

Measured against the report of a run under --seccomp_unotify, using the glibc
wrappers rather than raw syscall numbers, on a file and on a symlink to it:

  truncate() shrank the file from 21 to 3 bytes inside the jail, verified from
  outside. The report attributed no truncate to that path.

  setfattr set an attribute on a file inside the jail; the attribute was still
  there after the run, and the report attributed nothing to that path.

  glibc faccessat(AT_EACCESS) produced no entry at all, and no faccessat
  appeared either, so glibc had issued faccessat2.

faccessat2 is the same case as the statx entry added in c20246a: modern glibc
reaches for it first and only falls back to the traced faccessat on older
kernels. A plain access(2) is still traced by its own entry; what goes unseen
is the check made through faccessat().

truncate changes a file's contents without opening it, so nothing in the report
marks that path as written.

The l* variants are listed separately, the same way stat and lstat are, and are
exercised through a symlink so that all eleven entries are covered.

The flags argument of faccessat2 is left undecoded, the same as every other
AT_* entry in the table.
2026-08-25 05:17:02 +02:00
Nexory
c4a2631477 unotify: emit syscall numbers in the generated kafel policy
buildKafelPolicy() lists the kafel_name of every table entry. kafel resolves
those names from a per-architecture table, and those tables are not equally
complete, so a name that resolves on one architecture can be unknown on
another. kafel_compile() then fails, preparePolicy() returns false, and nsjail
does not start at all. Reproduced on amd64 by replacing one kafel_name with a
name kafel does not know:

  [E] preparePolicy():160 Could not compile the default unotify seccomp policy:
      3:275: Undefined identifier `no_such_syscall'
  [F] main():386 Couldn't prepare sandboxing policy
  exit 255

No report file is written; the process never runs.

Two entries are in that position today. Checked by taking __NR_ from the cross
compiler for each target and looking each kafel_name up in that target's kafel
table, counting only the entries the #ifdef keeps:

  amd64    38 kept,  0 skipped, none unknown to kafel
  aarch64  23 kept, 15 skipped, none unknown to kafel
  riscv64  22 kept, 16 skipped, none unknown to kafel
  i386     37 kept,  1 skipped, openat2 unknown to kafel
  arm      37 kept,  1 skipped, openat2 and statx unknown to kafel

openat2 has been in the table since ee2d05e and statx since c20246a. m68k and
the mips targets were not checked, no cross compiler at hand. debian/control
says Architecture: any, so i386 and armhf are built.

The syscall number is a compile time constant of the architecture being built
for, so emitting it removes the dependency on how complete kafel's name table
happens to be. kafel accepts SYSCALL[<nr>] (parser.y) and C style comments
(lexer.l), so the name stays in the generated policy as a comment and keeps it
readable.

Measured on amd64 with one workload and a fresh build per form: the name based
policy, the numeric one, and the numeric one with the comment produce identical
syscall distributions in the report. With the numeric form a kafel_name that
kafel does not know no longer prevents startup, and the syscall is still
reported under its display_name.
2026-08-25 05:15:15 +02:00
skaiea13-ai
b4cff719c2 Limit nstun TCP receive buffers with a shared payload budget
Cap retained guest-to-host TCP payload across flows so a stalled sink cannot grow the supervisor toward the per-flow limit times the maximum flow count.
2026-08-22 04:30:29 +09:00
Trithem90
8a751f0166 nstun: fail closed for proxy actions matched by ICMP 2026-08-21 13:57:16 +02:00
Trithem90
6673fe6758 nstun: preserve zero-network CIDR selectors 2026-08-21 12:15:24 +02:00
Trithem90
70094631bb nstun: enforce HOST_TO_GUEST policy on inbound TCP and UDP 2026-08-21 11:01:22 +02:00
Mustafa
9f166b999f Fix NSTUN parent lifecycle cleanup 2026-08-18 21:31:15 +03:00
robertswiecki
5ebcc30bef Merge pull request #291 from yexinw-ctrl/statx-unotify-trace
unotify: trace statx
2026-07-27 09:09:09 +02:00
yexinw
c20246ad0a unotify: trace statx
Add statx to the seccomp user-notify traced-syscall table
(unotify/syscall_defs.h::kTracedSyscalls). That table is the single source
of truth -- the kafel policy string and the argument decoder are both
derived from it -- so this one entry is the whole change.

Modern glibc/coreutils (ls, stat) issue statx before falling back to
newfstatat/openat when probing a path. Because statx was not traced, a
stat of a path that is not present in the jail's mount namespace was never
observed at all: under a restrictive mount set, `ls /opt` and `stat /opt`
produced no access record, while openat-based reads (cat, head) were
recorded normally. Tracing statx closes that gap.

statx(dirfd, path, flags, mask, statxbuf) has arg0 = dirfd and arg1 =
path, the same shape as newfstatat, so it reuses the existing DIRFD/PATH
arg roles. Guarded by #ifdef __NR_statx (defined on x86_64 and arm64).
kafel already knows the statx syscall, so no policy-side change is needed.

Tested by running a command under --seccomp_unotify with a mount set that
excludes /opt. Before: `stat /opt` and `ls /opt` did not appear in the
report. After: /opt appears with exists_in_jail:false, exists_in_main:true
via a statx syscall, while a genuinely missing path still reports
exists_in_main:false and an allowlisted path still reports
exists_in_jail:true.
2026-07-27 00:10:11 +00:00
Darren Carreras
93c3cadb81 user: fail closed when supplementary groups cannot be set
An expected EPERM after denying setgroups in a new user namespace can still leave inherited supplementary groups attached to the child. Query the remaining group count and abort before exec unless the credential state is actually clean.

Add regression coverage for both an inherited-group failure and a clean no-groups launch.
2026-07-23 21:52:17 -04:00
Darren Carreras
0d905f1da2 nstun: reject invalid policy rule fields 2026-07-21 19:59:04 -04:00
David Sarkisyan
3c9e848c61 contain: fail closed when no_new_privs cannot be set 2026-07-20 14:41:33 -04:00
h1-mrz
09dd8be0c0 sandbox: fail loudly when seccomp_unotify is requested but unusable
applyPolicy() installed the seccomp-unotify listener only when pipefd != -1.
In modes that pass pipefd == -1 (e.g. MODE_STANDALONE_EXECVE) the install was
silently skipped, and prepareAndCommit() then returned early because the classic
seccomp filter is empty -- so a sandboxee for which seccomp_unotify was
explicitly requested ran with NO seccomp policy at all, with no error emitted.

Refuse to continue instead of silently dropping the requested policy: if
seccomp_unotify is set but there is no supervisor to receive the notification fd
(pipefd == -1), log an error and fail.

Verified: `nsjail -Me --seccomp_unotify --seccomp_string 'DEFAULT ALLOW' -- ...`
now aborts with a clear error instead of launching the process unfiltered.
2026-07-18 08:40:07 -04:00
Darren Carreras
bd9439b3f2 nstun: block cloud-local service destinations 2026-07-16 18:42:09 -04:00
g0w6y
a460c1b6a0 nstun/tcp: validate guest ACK numbers against the send window
The guest is untrusted and fully controls the acknowledgement number of
every TCP segment it emits. tcp_process_data() accepted any forward ACK
(acked_bytes > 0) without checking that it stayed within the send window,
so the guest could acknowledge data that was never sent.

Each such ACK advances tx_acked_offset by up to 2^31-1. When tx_buffer is
empty the erase step below is a no-op, so tx_acked_offset accumulates and
can be driven past 2^32 with a few pure ACK segments. push_to_guest() then
computes:

    int32_t available = tx_buffer.size() - tx_acked_offset;
    const uint8_t* data = tx_buffer.data() + tx_acked_offset + in_flight;

available is an int32_t truncation of a size_t subtraction, so an oversized
tx_acked_offset wraps it back positive and defeats the in_flight >= available
guard. data then points at least 2 GB past the heap buffer and
tcp_send_packet() reads up to NSTUN_MTU bytes from it.

The nstun network loop runs in the nsjail parent (supervisor) process, and
SIGSEGV is not handled, so the out of bounds read lets an untrusted jailed
process crash its own supervisor. The minimum out of bounds distance is 2 GB,
so under ASLR the access reliably faults rather than returning data.

Add the RFC 793 receive check (SEG.ACK <= SND.NXT): reject ACKs whose
sequence is beyond seq_to_guest. This keeps tx_acked_offset within
tx_buffer.size(), which the framing in push_to_guest() relies on.
2026-07-15 20:17:22 +05:30
sravan27
b190cd94db cgroup: clean up swap-only memory cgroups 2026-07-13 21:23:02 +05:30
robertswiecki
d6454b4640 Merge pull request #271 from Alvov1/use-core-scheduling
contain: add core scheduling support (PR_SCHED_CORE)
2026-06-18 08:48:14 +02:00
Alexander Lvov
81784bfbac contain: add core scheduling support (PR_SCHED_CORE)
Adds --use_core_scheduling proto flag that calls prctl(PR_SCHED_CORE_CREATE) in the child process, giving each jail its own SMT scheduling group. Prevents sharing a physical core with other tenants and mitigates L1TF/MDS side-channel attacks in multi-tenant environments. Requires Linux >= 5.14; guarded with #ifdef PR_SCHED_CORE for compatibility with older kernel headers.
2026-06-11 10:33:53 +03:00
robertswiecki
1595ebcdf1 Merge pull request #270 from philwo/fix-unused-result
unotify: check return value of TextFormat::PrintToString
2026-05-28 22:27:26 +02:00
robertswiecki
5674c622c4 Merge pull request #269 from philwo/fix/exec-file-clobbered-by-positional-argv
cmdline: stop overwriting --exec_file with positional argv[0]
2026-05-28 22:26:15 +02:00
Philipp Wollermann
7e4a6fb5a6 unotify: check return value of TextFormat::PrintToString
The protobuf API marks PrintToString as nodiscard, so ignoring its
return value broke the build under -Werror=unused-result. Check the
result and bail out with a warning if formatting fails.
2026-05-27 07:24:41 +09:00
Philipp Wollermann
c4f20c7dae cmdline: stop overwriting --exec_file with positional argv[0]
setupArgv() unconditionally called set_path(argv[optind]) when positional
arguments were present after --, clobbering any path previously set via
the -x / --exec_file flag. The intended behavior, documented in the help
text ("File to exec (default: argv[0])"), is for --exec_file to win and
fall back to argv[0] only when not given.

Remove the unconditional set_path; the existing fallback a few lines
below already sets the path from argv[0] when --exec_file was not
provided.
2026-05-24 10:26:35 +09:00
rsc
66ad78dc36 unotify: ignore ENOENT and EINPROGRESS when logging SECCOMP_IOCTL_NOTIF_SEND failures
ENOENT means the thread has moved on (killed or interrupted).
EINPROGRESS indicates either misuse by sending again after
a successful send (which we're not doing) but we also see it
when a process is exiting, perhaps related to thread shutdown.

Authored-by: rsc@google.com
Tested-by: rsc@google.com
2026-05-08 09:41:43 +02:00
rsc
0c95a1f7a9 unotify: fix unotify implementation
Most of the system calls involved in the unotify poll loop were subtly wrong.
The biggest problem was the reuse of the previous message's req.id with
isTargetAlive to decide whether to exit the loop. The loop can be watching
many threads and many processes, any one of which might exit at any time
(especially if the last observed system call was exit!), so it does not make
sense to focus on a specific thread except within the context of a pending
syscall being evaluated.

SECCOMP_IOCTL_NOTIF_ID_VALID is for one purpose and one purpose only:
checking after an access by pid that the pid was not reused, invalidating
whatever was just read. The only time that purpose applies is between
parseSyscall and addStat, which is now the only time that the loop calls
isTargetAlive.

The loop was misusing isTargetAlive to decide when to exit the loop.
Now it correctly waits until a POLLHUP event.

The loop was misusing isTargetAlive to decide whether to call
SECCOMP_IOCTL_NOTIF_SEND. That's going to do the same check itself,
because otherwise there would be a race between the check and the send.
That redundant check is deleted.

The loop was also misusing isTargetAlive after a failed
SECCOMP_IOCTL_NOTIF_SEND to decide whether to exit the loop.
As before, one target being dead does not imply that all the traced targets
are dead, so that check is deleted. On failure, if the error is EINTR,
the send is tried again. If the error is ENOENT, that means the thread is
no longer blocked in the system call (either due to an interrupt or signal),
so we stop the send attempt but continue the processing loop.
Otherwise we print the error error, stop the send, but continue the processing loop.

The hangs seem to have been caused by a few different failure modes:

 1. The loop could have been exiting prematurely, hanging the syscalls
     waiting for judgement.

 2. In one strace log I read carefully, the loop kept running after POLLHUP,
    hanging in SECCOMP_IOCTL_NOTIF_RECV. I believe this may have been
    fixed between 6.6 and 6.12.

 3. The loop was not reacting well at all to send failing because the syscall
    had been interrupted. This manifested as needing to run Go programs
    with GODEBUG=asyncpreemptoff=1 to let them run at all.
    With these changes, Go programs using signals work just fine.

Authored-by: rsc@google.com
Tested-by: rsc@google.com
2026-05-07 06:51:10 +02:00
robertswiecki
9853352c7c Merge pull request #265 from sharadboni/fix/double-free-nl-cache-initParent
net: fix double-free of nl_cache in initParent error paths
2026-05-05 10:22:25 +02:00
robertswiecki
5b52bf10f0 Merge pull request #264 from kiyoungkim-gg/patch-1
Fix ESRCH in sched_setaffinity due to glibc TID caching with clone3
2026-05-05 10:21:58 +02:00
Sharad Boni
831ddd215b net: remove redundant nl_cache_free calls to fix double-free in initParent
initParent registers a defer{nl_cache_free(link_cache)} RAII guard that
runs on all exit paths, but the error returns inside the iface_own loop
and the cloneIface block also called nl_cache_free explicitly, causing
a double-free when moveToNs or cloneIface fails.

Remove the redundant explicit frees; the defer guard is sufficient.
2026-04-27 15:26:17 -07:00
kiyoungkim-gg
a1dd160283 Fix ESRCH in sched_setaffinity due to glibc TID caching with clone3
When nsjail creates a new process in a new PID namespace (CLONE_NEWPID) using the direct kernel syscall clone/clone3 (introduced in d1f332b), glibc's internal PID/TID cache is not updated for the child process.

As a result, calling the glibc wrapper `sched_setaffinity(0, ...)` inside the child process causes glibc to inadvertently pass the cached parent's TID to the kernel instead of 0 (current thread). Since the parent's TID does not exist within the new PID namespace, the kernel returns ESRCH (No such process).

This commit fixes the issue by bypassing the glibc wrapper and invoking the `sched_setaffinity` syscall directly via `util::syscall`. This ensures that `0` is passed accurately to the kernel, referring to the current thread.
2026-04-27 15:02:38 +09:00
Robert Swiecki
6356dee0c6 Makefile/all: make libnl3 optional 2026-04-14 14:26:28 +02:00
Robert Swiecki
bcf9eca738 make depend 2026-04-11 22:09:01 +02:00
Robert Swiecki
57277f38fd configs/znc-with-net.cfg - removing unneeded spaces 2026-04-11 22:07:43 +02:00
Robert Swiecki
a79c21ff9f nsjail.h: restore prematurely removed struct fields 2026-04-11 15:44:46 +02:00
Robert Swiecki
91010b3cdf logs: added tid, plus a help function 2026-04-09 22:48:18 +02:00
Robert Swiecki
b7ff9f3018 util/missing_defs: new support functions + a file defining missing syscall numbers/defs 2026-04-09 22:44:32 +02:00
Robert Swiecki
9043b9da73 unotify: surround syscall table by guards, as they're not implemented on all platforms 2026-04-03 11:38:38 +02:00