Skip to content

Keep the network rules unchangeable inside the sandbox and read them only as a regular file - #30

Merged
MarkusPaulsen merged 2 commits into
mainfrom
fix/code-scanning-netblocker-config
Sep 13, 2026
Merged

MarkusPaulsen merged 2 commits into
mainfrom
fix/code-scanning-netblocker-config

Conversation

@MarkusPaulsen

@MarkusPaulsen MarkusPaulsen commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The network filter now reads its rules only from the regular file it is given, never through a link or from a FIFO, and no longer rereads them when a process receives a hang-up signal. The rules file lives in a private directory the sandbox can read but not change, and that directory is removed after each run. The test helper that wrote rules files is made safe too.

Linked issues

No linked issues

1. Problem

Code scanning reports three findings. The test probe for the network filter created its rules file with a mode that could leave it writable by anyone, at a path taken from the environment. The filter opens the rules file NETBLOCKER_CONF names: it followed a symbolic link, blocked on a FIFO, and reread the file whenever a process received SIGHUP.

In phobos.sh that file lay in a directory under /tmp, which both shipped policies make writable, so a submission could rewrite the network rules of every process it started afterwards. The legacy phobos_wrapper.sh did not grant the library or its rules file at all, so the loader skipped the filter there. The directory was never removed.

The layers at fault are the LD_PRELOAD network filter and the policy the filesystem layer hands to Landlock. Phobos let a submission change what it may reach.

2. Improvement from the user's perspective

The network rules a run starts with are the rules it keeps: a submission can read them but can no longer change them. A policy under which the rules file or the filter itself could be changed is refused with exit code 11 and a message naming the write path. No policy directory is left behind after a run, whether it succeeds, fails, is refused or times out.

3. Improvement from the maintainer's perspective

CodeQL alerts #1 and #3, both in the test probe, are fixed at their cause: the probe creates a private directory with mkdtemp, writes the rules exclusively with mode 0600, and hands the directory to its second phase, which removes it. Alert #2, the filter reading NETBLOCKER_CONF, stays by design: the variable is the interface, and a process can step around a preload library anyway, as SECURITY.md says. At its new location it is alert #4, dismissed with that reason.

One function, append_netblocker_rules, gives both entry points the same rules for the library and its rules file, and one, finish_owned_spec_dir, removes a policy directory Phobos created and nothing else. A separate commit corrects a comment #29 put in test.yml: the pruner's Bubblewrap checks do run on the hosted runner.

4. Testing manual

Prerequisites

  1. Docker, and a checkout of this branch. Nothing below uses --privileged, --cap-add or --security-opt.

Steps

  1. The filter, the policy and the cleanup, from the repository root:
    docker run --rm -v "$PWD:/repo:ro" ubuntu:26.04 bash -c 'apt-get update -qq && apt-get install -y -qq gcc-14 libc6-dev binutils >/dev/null && cp -a /repo /w && bash /w/tests/network_cache_ports.sh'
  2. The same checks against the filter as it was on main, to see that they catch the old behaviour:
    git show origin/main:ld_preloader/netblocker.c > /tmp/old-netblocker.c, then step 1 with -v /tmp/old-netblocker.c:/old.c:ro added and cp /old.c /w/ld_preloader/netblocker.c && placed before bash.
  3. The three acceptance suites exactly as tests/landlock-acceptance/README.md gives them, on a machine that runs the image natively.

Expected result

  1. Every line is ok, among them the sections the rules file, SIGHUP, the library and its rules file stay out of the sandbox's reach and a run leaves no specification behind, and the suite ends with 51 passed, 0 failed, 0 skipped.
  2. FAIL on the symbolic link, the FIFO, both SIGHUP dispositions, rewriting the rules and sending SIGHUP widens nothing, and the probe's cleanup, because the old filter blocks on the FIFO until the run is killed. The directory check passes against the old filter too, since reading a directory yields no rules; it guards the behaviour rather than telling the two apart.
  3. bestanden: 11, bestanden: 27 and bestanden: 13, each with fehlgeschlagen: 0. Section I. shows three PASS lines: the rules file readable but not writable inside the sandbox, a policy directory beneath a write path refused, and no policy directory left.

Negative case (what must still be rejected)

Inside the sandbox the rules file cannot be written (step 3, section I.). A write path equal to or above the rules file's directory or the library, directly or through a symbolic link, is refused with exit code 11 and the command does not run (step 1). A rules file reached through a link, a directory or a FIFO grants no port. Port 18081 stays denied while 18080 works (step 3, run-tests.sh).

Layers exercised

  • Filesystem layer
  • Network layer
  • Timeout layer
  • All three together, as a run uses them by default

5. Test case coverage regarding this PR

Suite Passed Failed Skipped What it covers regarding this PR
tests/network_cache_ports.sh, Ubuntu 26.04 container, arm64 and amd64 51 0 0 23 new checks. Denied: rules through a link, a directory or a FIFO; a widened rule after rewrite and SIGHUP; rules file or library beneath a write path, directly, as an ancestor, through a link, or on a last line without a newline. Permitted: an unchangeable library and rules file reach Landlock readable; inherited SIGHUP dispositions kept. Cleanup after success, failure, both refusals and a timeout; a foreign directory kept; a directory that cannot be removed fails the run.
The same suite against netblocker.c from main 45 6 0 The new filter checks fail there, as they must.
tests/timeout_units.sh, Ubuntu 26.04 container, amd64 58 0 0 The legacy wrapper with the new rules for the library and its rules file.
tests/landlock-acceptance/run-tests.sh, arm64 image, --network none 11 0 0 The filter reads its rules from the new place: 18080 allowed, 18081 denied.
tests/landlock-acceptance/extra-tests.sh, same 27 0 0 Three new checks under real Landlock: rules readable, not writable; directory beneath a write path refused; nothing left.
tests/landlock-acceptance/phase-test.sh, same 13 0 0 Unchanged.

All runs used kernel 7.0 under Docker Desktop.

Alerts #1 and #3 no longer appear for this pull request. The remaining finding, the filter opening the file NETBLOCKER_CONF names, is the one reported as #2 on main; moving the open into its own function gave it a new location, which code scanning counted as the new alert #4. It stays by design, as section 3 says, and #4 is dismissed as "won't fix" with that reason. The netblocker refactor will move this code again, and the alert it then reports gets the same treatment.

Breaking changes and migration

  • A run's policy directory moves from /tmp to /var/tmp, or to PHOBOS_SPEC_PARENT. A policy whose write paths cover it, or the directory holding libnetblocker.so, now stops with exit code 11.
  • The filter no longer rereads its rules on SIGHUP, and a process whose SIGHUP is at its default is ended by it, as without the filter.
  • This now permits reading libnetblocker.so and allowedList.cfg in phobos_wrapper.sh, which it did not permit before, and reading net.rules at its new place, which it did not permit before. Both are needed to load the filter; neither can be changed.

Checklist

  • The title of this pull request describes the change, not the implementation.
  • I have self-reviewed the diff of this pull request.
  • Tests were added or updated for the behaviour changed here, in both directions: the forbidden case stays denied and the permitted case still works.
  • Any weakening of the sandbox boundary is stated explicitly above, including what it now permits that it did not permit before.
  • The change was exercised in a container started without --privileged, --cap-add or --security-opt, or the manual says why that was not possible.
  • Documentation (README.md, the comments in core/) was updated where the change is user-facing.
  • CI is green, or every remaining failure is explained above.
  • No secrets, tokens or absolute local paths are contained in the diff.

Review progress

  • Code review
  • Manual test

Markus Paulsen added 2 commits September 13, 2026 13:27
…e it is

Code scanning reported three findings. The test probe for the network filter
wrote its rules file at a path taken from the environment, with a mode that could
leave it writable by anyone. It now creates a private directory with mkdtemp,
writes the rules exclusively with mode 0600 and without following a link, and
hands the directory to its second phase, which removes it.

The filter opened the rules file NETBLOCKER_CONF names with fopen: it followed a
symbolic link, blocked every process on a FIFO, and reread the file on SIGHUP. It
now opens it without following a link and without blocking, accepts only a
regular file, and no longer installs a SIGHUP handler, so a process keeps the
disposition it inherited.

In phobos.sh the rules file lay in a directory under /tmp, a write path in both
shipped policies, so a submission could rewrite the rules every later process
reads. The directory now lives under PHOBOS_SPEC_PARENT, /var/tmp by default, and
append_netblocker_rules gives both entry points a read rule for the rules file
and the library while refusing a policy under which either lies beneath a write
path. The legacy wrapper granted neither before. Every layer removes the directory
phobos.sh created, and only that one, when the run ends.

Both committed libnetblocker.so copies are rebuilt with the pinned toolchain.
The comment beside the prune_sandbox.sh step claimed those checks skip in CI for
want of bubblewrap. The run logs show all fifteen passing there.
@MarkusPaulsen
MarkusPaulsen requested a review from a team September 13, 2026 11:27
@MarkusPaulsen
MarkusPaulsen requested review from a team and krusche as code owners September 13, 2026 11:27
Comment thread ld_preloader/netblocker.c Dismissed
@MarkusPaulsen
MarkusPaulsen merged commit bbff146 into main Sep 13, 2026
1 check passed
@MarkusPaulsen
MarkusPaulsen deleted the fix/code-scanning-netblocker-config branch September 13, 2026 12:35
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.

2 participants