Skip to content

Keep a contained process's signals inside the Linux sandbox, and let corepack fetch Yarn 2 and later - #185

Merged
fstubner merged 3 commits into
mainfrom
fix/linux-signal-scope
Oct 7, 2026
Merged

fstubner merged 3 commits into
mainfrom
fix/linux-signal-scope

Conversation

@fstubner

@fstubner fstubner commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

What changes

A contained process on Linux can no longer signal nvx or anything else outside the sandbox. The contained process shares nvx's process group so that Ctrl-C at a terminal reaches everything it starts. A process group reaches across the sandbox's PID namespace, so a contained kill(0, SIGKILL) reached nvx and every process beside it.

  • On Linux 6.12 and later (Landlock ABI v6) the ruleset now scopes signals to the sandbox. A signal sent into the sandbox is not restricted, so the terminal's Ctrl-C and Ctrl-Z, nvx's forwarding and npm stopping its own scripts work as before.
  • Below ABI v6 nothing can scope signals. The contained process gets a process group of its own again, and the supervisor sends each signal it passes on to that whole group, the terminal's SIGINT included, followed by SIGCONT. Containment comes first. On those kernels a contained process that reads the terminal is stopped. Ctrl-C still ends it, though a process that catches Ctrl-C, as Node and Go programs do, may need it more than once. Ctrl-Z stops nvx while the contained process runs on. This is one branch, ownGroup in runLandlockExecChild.
  • The ruleset also scopes abstract UNIX sockets. They have no path for the sandbox's filesystem view to hide, and network.mode: open shares the host's network namespace, so a contained process could reach them there. The other modes already had a network namespace of their own.

corepack can fetch Yarn 2 and later. default_allow now names repo.yarnpkg.com:443. corepack downloads Yarn 2 and later from there and looks up Yarn's versions there for a range such as yarn@4 or yarn@1. It downloads pnpm and Yarn 1 from the two registries the list already named.

Evidence

Measured 2026-10-07 on WSL2 kernel 6.18 (Landlock ABI 7) in a privileged container, with the real nvx binary.

Check main this branch this branch, built to take the ABI v5 path
npm preinstall runs kill(0, SIGKILL), with a sleep in nvx's group sleep, nvx and the shell killed sleep and shell alive, nvx exits 137 sleep and shell alive, nvx exits 137
Ctrl-C in a pty, nvx npx -y http-server stopped 0.14 s after stopped 0.18 s and 0.32 s after stopped 0.13 s to 0.16 s after
Ctrl-C in a pty, nvx --strict npm run serve stopped 0.07 s after stopped 0.07 s and 0.10 s after stopped 0.06 s after
nvx --strict node REPL answers typed input yes yes no, within 8 s
Ctrl-C on a contained process the terminal stopped for reading it sh -c 'read x' ends on one, a Node script that read a line on two. Without the SIGCONT, still running 10 s on in 3 runs of 3
Ctrl-Z at an interactive bash stops the run, fg resumes it stops nvx, the contained process keeps running
network.mode: open, connect to a host abstract socket and to Xvfb's both connect both EPERM
network.mode: proxy, the same two connections both ECONNREFUSED both ECONNREFUSED
contained corepack yarn@4 install (corepack 0.36.0) refused repo.yarnpkg.com:443, exit 77 Yarn 4.18.1 installs, exit 0
contained corepack yarn@1 install refused repo.yarnpkg.com:443, exit 77 installs, exit 0
contained corepack pnpm@10 install installs

A Go program stopped on a terminal read and signalled the same way outside nvx needed one to four Ctrl-C in 40 runs. A contained process can still signal the supervisor, PID 1 in the sandbox, on main and on this branch. The supervisor passes a signal it handles back to the contained process, and in 3 runs of 3 the kernel dropped a SIGKILL sent to it from inside.

Tests

New privileged tests, all named TestContainedProcess... so the CI filter picks them up:

  • TestContainedProcessCannotSignalOutsideItsSandbox checks a contained kill(0, SIGKILL) beside a sentinel in nvx's group, on the kernel's ABI and on ABI v5. It fails on main, where nvx and the sentinel are killed.
  • TestContainedProcessCannotReachAHostAbstractSocket checks open mode. It fails on main, where the connection succeeds.
  • TestContainedProcessStopsOnCtrlCUnderNpm sends Ctrl-C through a real pty to a real npm run script and an npx bin on both paths. It fails with the target in a group of its own and signalled alone, which is how it was before Fix Ctrl-C, /tmp and port publishing in the Linux sandbox #178.
  • TestContainedProcessNpmPassesSignalsToItsScript checks that npm passes SIGTERM to an exec'd script. It fails when the contained process may not call kill(2).
  • TestContainedProcessGetsTheTerminalsInterruptOnce now runs on both paths, and on the ABI v5 path checks that Ctrl-C ends a target the terminal stopped. That case fails without the SIGCONT.
  • TestTheDefaultAllowlistLetsCorepackFetchYarn fails without the allowlist entry.

Verification

  • The CI privileged filter (TestNetns|TestReap|TestSupervisorClone|TestContainedProcess|TestNvxTermination|TestGitMetadata) as root in the container gave 25 top-level PASS, 0 FAIL and 0 SKIP. main gave 21 PASS. The new and changed tests passed 5 runs in a row.
  • go test -race ./... on Linux in the container passed. go test ./internal/nvx -count=1 on Windows passed.
  • gofmt -l internal cmd prints nothing. go vet ./... passed for windows, linux and darwin.
  • For the site, npm run check:content passed, astro check found 0 errors and npm run build built 9 pages, in a Linux container.

The contained process shares nvx's process group so that Ctrl-C at a
terminal reaches everything it starts. A process group reaches across the
sandbox's PID namespace, so the contained process could signal nvx and every
process beside it. Measured on Linux 6.18 in a privileged container, an npm
preinstall that ran kill(0, SIGKILL) killed nvx, the shell that started it
and a sleep that shell had started.

From Landlock ABI v6, Linux 6.12, the ruleset now scopes signals to the
sandbox. The same preinstall ends only its own install, and the sleep and the
shell run on. A signal sent into the sandbox is not restricted, so the
terminal's Ctrl-C and Ctrl-Z, nvx's forwarding and npm stopping its own
scripts work as before. In a pseudo-terminal with the scope in place,
nvx npx -y http-server and nvx --strict npm run stopped 0.07 to 0.32 seconds
after Ctrl-C, a contained node REPL answered, and Ctrl-Z and fg stopped and
resumed the run.

Below ABI v6 nothing can scope signals. There the contained process gets a
process group of its own again, and the supervisor sends each signal it
passes on to that whole group, the terminal's SIGINT included. Built to take
that path on the same kernel, the two commands stopped 0.06 and 0.16 seconds
after Ctrl-C. On those kernels a contained process that reads the terminal
is stopped, and Ctrl-Z stops nvx while the contained process runs on.

The ruleset also scopes abstract UNIX sockets. They have no path for the
sandbox's filesystem view to hide, and network.mode open shares the host's
network namespace. In open mode a contained process connected to an abstract
socket a host process listened on, and to the one Xvfb listens on. Both now
get EPERM. In proxy mode both got ECONNREFUSED before and after, because the
sandbox has a network namespace of its own there.

The supervisor reads the ABI through supervisorLandlockABI, so a test can run
the older path on a newer kernel. The new privileged tests cover a contained
kill(0, SIGKILL) beside a sentinel in nvx's group, an abstract socket in open
mode, Ctrl-C through a pseudo-terminal under real npm run and npx on both
paths, and npm passing SIGTERM to its script. The first two fail on the
previous code. The Ctrl-C tests fail when the target is signalled alone, and
the npm test fails when the contained process may not call kill(2).
corepack downloads Yarn 2 and later from repo.yarnpkg.com, and looks up
Yarn's versions there when it is given a range such as yarn@4 or yarn@1. The
default allowlist did not name the host. With corepack 0.36.0 in a Linux
container, a contained corepack yarn@4 install and corepack yarn@1 install
were both refused a connection to it and exited 77. default_allow now names
repo.yarnpkg.com:443. Both commands install, and the audit log records the
host under default_allow. corepack downloads pnpm and Yarn 1 from
registry.npmjs.org and registry.yarnpkg.com, which the list already named,
and a contained corepack pnpm@10 install worked before.

The policy page, the FAQ and SECURITY.md now list the host.
Below Landlock ABI v6 the contained process runs in a process group of its
own, so the terminal stops it with SIGTTIN when it reads the terminal. A
stopped process acts on no signal but SIGKILL and SIGCONT, so the SIGINT the
supervisor passed on for Ctrl-C stayed pending and the run went on. Measured
on Linux 6.18 with the older path forced, a contained sh -c 'read x' stopped
that way was still running 10 seconds after Ctrl-C in 3 runs of 3.

The supervisor now sends SIGCONT to the group after each signal it passes on.
The same process ends on one Ctrl-C. A Node script that had read a line
needed two, and a Go program reading stdin, signalled the same way outside
nvx, needed one to four in 40 runs. Both catch Ctrl-C, and the read they
restart can stop them again before they have exited. The docs now say so.

The new terminal test case runs only on the older path. It waits until the
reader is stopped before it types Ctrl-C, and it fails without the SIGCONT.

The abstract socket test decides whether to skip from the ABI the kernel
reports. It used landlockScopesForABI, the function under test. The comment on
supervisorInterruptSignal now covers a target in a group of its own.
@fstubner
fstubner merged commit f0446e8 into main Oct 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant