Skip to content

fix(security): getAllJails infinite loop, stack sh -c injection + traversal, gui /tmp symlink, safePath prefix bug, socket_proxy confinement (1.1.25) - #228

Merged
click0 merged 3 commits into
mainfrom
claude/analyze-test-coverage-nCOJW
Sep 8, 2026
Merged

click0 merged 3 commits into
mainfrom
claude/analyze-test-coverage-nCOJW

Conversation

@click0

@click0 click0 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Five fixes from a third-pass audit of the modules no earlier pass had covered (GUI/session, VM stack, lifecycle/runtime, audit/util/parsers). Everything here ships without FreeBSD hardware — pure parts are unit-tested, runtime parts are compile-gated by the FreeBSD lite build.

1. Infinite loop in getAllJails(crateOnly=true)lib/jail_query.cpp (HIGH, DoS)

The lastjid cursor advance sat after the crateOnly filter, so a non-crate jail hit continue without moving the cursor and the next jailparam_get returned the very same jail forever — 100% CPU the moment any foreign jail (bastille/pot/plain jail(8)) coexisted with crate. That wedged every crateOnly caller — crate top/clean/doctor/info/list/console/stackand the crated control-socket jail listing. No attacker input needed. The cursor now advances before the filter.

2. Command injection into a root sh -c via stack-file fields — lib/stack.cpp (HIGH)

Container name, its static IP, and a network's gateway were interpolated unescaped into printf '…' >> /etc/hosts / printf 'nameserver …' > /etc/resolv.conf fragments stored in run:before-start-services and executed by /bin/sh -c as root. A container keyed x';touch /tmp/pwned;' ran arbitrary commands as root on crate stack up. New StackPure::validateStackName ([A-Za-z0-9._-], ≤64, no leading -) and validateStackIp (charset-gated inet_pton, CIDR tolerated) are applied at parse time and re-applied as a sink-guard right where each value enters the shell string.

3. Path traversal via stack network name → root write + delete — lib/stack.cpp (HIGH)

confDir = dnsBaseDir()/dns-<network.name> was create_directories'd, written, passed to unbound -c, and remove_all'd as root with an unvalidated YAML key. ../../../etc/cron.d let root write under an attacker-chosen path and recursively delete an attacker-chosen tree. Closed by the same validateStackName (no /, no ..).

4. Predictable /tmp screenshot files → symlink attack — lib/gui.cpp (HIGH when root)

/tmp/crate-screenshot-<displayNum>.{ppm,xwd} (displayNum guessable, starts at 10) opened via fopen/xwd -out with no O_EXCL/O_NOFOLLOW; gui screenshot runs with root's EUID, so a local user could pre-plant a symlink and have root overwrite an arbitrary file (CWE-59). Scratch files now live in a private mkdtemp(3) dir (0700, random name) removed on every exit path.

5. Util::safePath over-rejected every path when the prefix ends in /lib/util_pure.cpp (MED)

The separator check demanded canonical[prefix.size()] == '/', but a prefix that already ends in / (root "/" is the degenerate case) has consumed that separator, so the check always failed → socketProxy.share aborted on every real path. Separator now demanded only when the prefix doesn't supply it.

6. socketProxy jail-side confinement — lib/run_services.cpp (closes deferred TODO item)

With safePath fixed, the share loop's safePath(sock, "/", …) would pass everything (prefix / can't confine; return discarded while the raw .. path still reached J()), and proxy had no guard at all. Both now validate the concatenated jail-side path stays under jailPath — the same guard run.cpp uses for dirsShare. (proxy.host is the operator's host-side target, deliberately not jail-confined.)

Tests

  • stack_test: name/IP injection + traversal rejected, clean values accepted (a..b single component OK, 10.0.0.5/24 CIDR tolerated).
  • util_security_test: root prefix accepts absolute paths; trailing-slash prefix still rejects siblings.
  • Both pass locally. jail_query/gui/run_services are runtime-only → FreeBSD lite compile gate.

Also noted (not changed)

lib/vm_spec.cpp / vm_run.cpp / vm_stack.cpp carry libvirt-XML injection + traversal sinks (vmName, vol.tag, disk, sharedBridge) that would be HIGH if reachable — but createVm/parseVmOptions/generateDomainXml have no callers. Recorded in TODO to harden before that code is wired up. Remaining deferred: the network_lease flock race (needs a real multi-process test on FreeBSD).

Version

Bumps to 1.1.25; CHANGELOG.md + docs/trust-model.{md,uk.md} updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK


Generated by Claude Code

…versal, gui /tmp symlink, safePath prefix bug, socket_proxy confinement (1.1.25)

Five fixes from a third-pass audit of modules no earlier pass covered:

- lib/jail_query.cpp getAllJails(crateOnly=true): the lastjid cursor
  advance sat after the crateOnly filter, so a foreign (non-crate) jail
  hit `continue` without moving the cursor and jailparam_get returned
  the same jail forever — 100% CPU, wedging every crateOnly caller
  incl. the crated control-socket jail listing. No attacker input
  needed. Cursor now advances before the filter.

- lib/stack.cpp: container name, static IP, and network gateway were
  interpolated unescaped into `printf '…' >> /etc/hosts` /
  `printf 'nameserver …' > /etc/resolv.conf` shell fragments run via
  `/bin/sh -c` as root; and network.name built the root-managed
  dns-<name> dir (create_directories/writeFile/unbound -c/remove_all)
  unvalidated → traversal write+delete. New StackPure::validateStackName
  ([A-Za-z0-9._-], <=64, no leading '-') and validateStackIp
  (charset-gated inet_pton, CIDR tolerated) applied at parse time AND
  as a sink-guard where each value enters the shell string.

- lib/gui.cpp screenshot: predictable /tmp/crate-screenshot-<n>.{ppm,xwd}
  opened without O_EXCL/O_NOFOLLOW while running as root → symlink
  attack (CWE-59). Scratch files now live in a private mkdtemp dir
  (0700, random) removed on every exit path.

- lib/util_pure.cpp safePath: the separator check demanded
  canonical[prefix.size()]=='/' even when the prefix already ends in
  '/', so prefix "/" rejected every real path. Separator now demanded
  only when the prefix doesn't supply it.

- lib/run_services.cpp socket_proxy: `share` used safePath(sock,"/",…)
  which (once fixed) confines nothing and discarded its return; `proxy`
  had no guard. Both now validate the concatenated jail-side path stays
  under jailPath, as run.cpp does for dirsShare. Closes the socket_proxy
  TODO item.

Tests: stack_test (name/IP injection + traversal), util_security_test
(root prefix accepts, trailing-slash prefix still rejects siblings).
Latent vm_spec/vm_run/vm_stack libvirt-XML injection (dead code, no
callers) recorded in TODO. Bumps to 1.1.25; CHANGELOG + trust-model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
…om the 14.2 pkg catalogue

PR #228's FreeBSD lite job died in 41s at the very first step, before a
single line compiled:

  pkg: No packages available to install matching 'kyua' have been found

The freshly-refreshed 14.2 catalogue (pkg 2.6.2 -> 2.7.5, 36986 pkgs)
no longer carries devel/kyua. FreeBSD 14.x ships kyua, libatf-c{,++},
atf-c++.hpp and atf-sh in BASE, and the Makefile's
`-L/usr/local/lib -latf-c++ -latf-c` still resolves against /usr/lib,
so base is sufficient.

Both workflows now: install the hard build deps unconditionally; try
`pkg install kyua atf` but fall through to base if the repo lacks them;
then preflight-check kyua / atf-sh / atf-c++.hpp with diagnostics
(`pkg search` output) so a future rename or removal fails with a
readable reason instead of a cryptic pkg line.

Not a code change — 1.1.25's sources were never reached by the failed
run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
…om base (port deleted)

Second CI iteration on PR #228. Run 34233361523 proved the 1.1.25 code
compiles cleanly on FreeBSD (smoke compile, crate, crated, crate-snmpd
and every unit-test .o all built) and then died at the very last step:

  ld: error: unable to find library -latf-c++
  ld: error: unable to find library -latf-c

Root cause, now verified: FreeBSD 14.x base ships /usr/bin/kyua and the
/usr/include/atf-c++.hpp HEADER, but its ATF libraries are PRIVATE
(/usr/lib/private/libprivateatf-*) and cannot be resolved as -latf-c++.
The linkable /usr/local/lib/libatf-c{,++}.so come from devel/atf — which
is alive (0.23, quarterly) — but the previous commit bundled it with
kyua in ONE `pkg install`, and since devel/kyua was DELETED from ports
on 2026-05-07 ("part of the base in all supported versions — Kyua's
evolution happens in the base", D47473), pkg aborted the whole
transaction and atf silently never installed.

Both workflows now: `pkg install atf` as a hard dep on its own line;
kyua tried from pkg but expected from base; preflight additionally
asserts a LINKABLE libatf-c++.so exists (the check that would have
caught this run up front) and fails with a readable reason otherwise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
@click0
click0 merged commit 32a20df into main Sep 8, 2026
1 check 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.

2 participants