fix(security): getAllJails infinite loop, stack sh -c injection + traversal, gui /tmp symlink, safePath prefix bug, socket_proxy confinement (1.1.25) - #228
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
lastjidcursor advance sat after the crateOnly filter, so a non-crate jail hitcontinuewithout moving the cursor and the nextjailparam_getreturned the very same jail forever — 100% CPU the moment any foreign jail (bastille/pot/plainjail(8)) coexisted with crate. That wedged every crateOnly caller —crate top/clean/doctor/info/list/console/stack— and the crated control-socket jail listing. No attacker input needed. The cursor now advances before the filter.2. Command injection into a root
sh -cvia stack-file fields —lib/stack.cpp(HIGH)Container name, its static IP, and a network's
gatewaywere interpolated unescaped intoprintf '…' >> /etc/hosts/printf 'nameserver …' > /etc/resolv.conffragments stored inrun:before-start-servicesand executed by/bin/sh -cas root. A container keyedx';touch /tmp/pwned;'ran arbitrary commands as root oncrate stack up. NewStackPure::validateStackName([A-Za-z0-9._-], ≤64, no leading-) andvalidateStackIp(charset-gatedinet_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>wascreate_directories'd, written, passed tounbound -c, andremove_all'd as root with an unvalidated YAML key.../../../etc/cron.dlet root write under an attacker-chosen path and recursively delete an attacker-chosen tree. Closed by the samevalidateStackName(no/, no..).4. Predictable
/tmpscreenshot files → symlink attack —lib/gui.cpp(HIGH when root)/tmp/crate-screenshot-<displayNum>.{ppm,xwd}(displayNum guessable, starts at 10) opened viafopen/xwd -outwith noO_EXCL/O_NOFOLLOW;gui screenshotruns 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 privatemkdtemp(3)dir (0700, random name) removed on every exit path.5.
Util::safePathover-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.shareaborted on every real path. Separator now demanded only when the prefix doesn't supply it.6.
socketProxyjail-side confinement —lib/run_services.cpp(closes deferred TODO item)With
safePathfixed, theshareloop'ssafePath(sock, "/", …)would pass everything (prefix/can't confine; return discarded while the raw..path still reachedJ()), andproxyhad no guard at all. Both now validate the concatenated jail-side path stays underjailPath— the same guardrun.cppuses fordirsShare. (proxy.hostis the operator's host-side target, deliberately not jail-confined.)Tests
stack_test: name/IP injection + traversal rejected, clean values accepted (a..bsingle component OK,10.0.0.5/24CIDR tolerated).util_security_test: root prefix accepts absolute paths; trailing-slash prefix still rejects siblings.jail_query/gui/run_servicesare runtime-only → FreeBSD lite compile gate.Also noted (not changed)
lib/vm_spec.cpp/vm_run.cpp/vm_stack.cppcarry libvirt-XML injection + traversal sinks (vmName,vol.tag,disk,sharedBridge) that would be HIGH if reachable — butcreateVm/parseVmOptions/generateDomainXmlhave no callers. Recorded inTODOto harden before that code is wired up. Remaining deferred: thenetwork_leaseflock 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