GHSA-PXHW-H44J-8PFX: Verified Reproduction
GHSA-PXHW-H44J-8PFX: bubblewrap: sandbox escape via /oldroot symlink traversal during setup — files created on host
GHSA-PXHW-H44J-8PFX is verified against the affected target. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00339.
What Is GHSA-PXHW-H44J-8PFX?
GHSA-PXHW-H44J-8PFX is a high-severity vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00339).
GHSA-PXHW-H44J-8PFX Severity
GHSA-PXHW-H44J-8PFX is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce GHSA-PXHW-H44J-8PFX
pruva-verify REPRO-2026-00339 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00339/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for GHSA-PXHW-H44J-8PFX
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
filesystem content bound into the sandbox (untrusted tree containing symlink subdir -> /oldroot<host_target>), plus marker file content via --file fd
- bwrap --bind <untrusted> / --dir /subdir/newdir --file 3 /subdir/newdir/ESCAPE_MARKER.txt /usr/bin/true; setup_newroot()
- mkdir_with_parents('/newroot/subdir/newdir') follows the absolute symlink into /oldroot (host)
How the agent worked
Root Cause and Exploit Chain for GHSA-PXHW-H44J-8PFX
During sandbox setup, bubblewrap (bwrap) mounts the host filesystem at /oldroot
and builds the sandbox root at /newroot. When a setup operation that creates a
file or directory (e.g. --dir, --file, --bind-data, --ro-bind-data) has a
destination whose parent components contain a symbolic link, vulnerable versions
resolve that symlink with plain mkdir()/open() path semantics. If the symlink
lives in attacker-controlled filesystem content (such as a malicious Flatpak app
image bound at /) and points at an absolute path under /oldroot/..., the
creation is redirected out of the sandbox and onto the host filesystem. This
happens during setup, before any sandboxed code runs. We reproduced the escape
end-to-end with the real bwrap CLI: a directory and an attacker-content marker
file were created on the host in 2/2 vulnerable attempts, while the fixed version
failed closed in 2/2 attempts.
- Package/component:
bubblewrap(bwrap), used by Flatpak and similar app frameworks. - Affected versions:
< 0.12.0(confirmed on v0.11.0, commit9ca3b05ec787acfb4b17bed37db5719fa777834f). No backport exists; the fix is only in v0.12.0, which also drops setuid build support. - Risk level and consequences: High (advisory scores 8.8). An attacker who controls filesystem content that a launcher binds into the sandbox can cause bwrap to create directories and attacker-controlled files at arbitrary host paths writable by the launching uid/gid (generally unprivileged). This can overwrite/seed config files, startup scripts, SSH keys, etc., enabling further compromise. If bwrap is invoked by a privileged (setuid) launcher, the write is privileged.
Impact Parity
- Disclosed/claimed maximum impact: sandbox escape — files created on the host outside the sandbox during setup (CWE-22 path traversal).
- Reproduced impact from this run: sandbox escape —
--dircreated/subdir/newdiron the host and--filewrote a fully attacker-controlled marker file (ESCAPE_MARKER.txt) into it, outside the sandbox, through the realbwrapCLI entrypoint, in both vulnerable attempts. - Parity:
full - Not demonstrated: privileged (root) file creation — the run used an unprivileged launcher, matching the advisory's typical (non-setuid) scenario; this is a deployment precondition, not a gap in the vulnerability proof.
Root Cause
In v0.11.0, setup_newroot() (bubblewrap.c:1189) processes each setup op and,
for ops with a destination, computes dest = get_newroot_path(op->dest) which
returns the absolute path /newroot/<dest> (utils.c:868). It then calls
mkdir_with_parents(dest, parent_mode, false) (bubblewrap.c:1233) and, for
file-creating ops, ensure_dir()/ensure_file()/open() on the resulting path.
mkdir_with_parents() (utils.c:705) walks the path component by component calling
mkdir(), which follows symlinks in parent components with full host-kernel
resolution.
At that point in setup, bwrap has already pivot_root()ed into a scratch tmpfs
(bubblewrap.c:3345) so that the namespace root contains oldroot/ (a bind of the
host filesystem) and newroot/ (the sandbox root under construction). An
absolute symlink inside attacker-controlled content that was bound at /
(= /newroot) therefore resolves against the setup namespace root: a symlink
<untrusted>/subdir -> /oldroot/tmp/.../host_target makes
mkdir /newroot/subdir/newdir create /oldroot/tmp/.../host_target/newdir,
i.e. a directory on the host. The same applies to --file/--bind-data/
--ro-bind-data, which additionally write attacker-controlled content.
resolve_symlinks_in_ops() (bubblewrap.c:1615) only realpaths the source
paths of bind mounts, not the destination paths of file/dir-creating ops,
so it does not mitigate this.
Fix: v0.12.0 (tag 2a76602a8c71f36c1527cf9fc3417d9149822e0c) resolves all
destination paths with openat2(RESOLVE_IN_ROOT) via safe_openat() imported
from crun (commit 67d4be103b18706b5b4e3f495daa35e89e47b163, with a
chroot_realpath.c fallback for pre-5.6 kernels) together with commit
ea185f6fb135782cabab342e33432e8482a2f5c9 ("Inline the privileged ops"), so
symlink resolution is confined to /newroot and escaping symlinks fail with
ENOENT instead of being followed onto the host.
Advisory: https://github.com/containers/bubblewrap/security/advisories/GHSA-pxhw-h44j-8pfx
Reproduction Steps
bundle/repro/reproduction_steps.sh(self-contained; run from any directory, exit 0 = confirmed).- What the script does:
- Reads
bundle/project_cache_context.jsonand reuses the prepared project cache (<project_cache_dir>/repo), falling back tobundle/artifacts/. - Clones
containers/bubblewrap, verifies the vulnerable commit (v0.11.0 =9ca3b05e...) lacks fix commits67d4be10.../ea185f6f...and the fixed commit (v0.12.0 =2a76602a..., the ticket's fixed tag) contains them; also checks the source trees (safe_openat.cabsent/present). - Builds both versions with the system
gcc(no sudo in this environment: libcap headers are obtained by downloading thelibcap-dev.deb and extracting it; the only requiredconfig.hmacro,PACKAGE_STRING, is generated; no sanitizer is used — this is a plain product build). - Smoke-tests that unprivileged user namespaces work.
- Runs the advisory recipe hermetically, twice per build:
creates
untrusted/subdir -> /oldroot<abs path of host_target>and invokesbwrap --bind <untrusted> / --ro-bind /usr /usr --ro-bind /lib /lib --ro-bind /lib64 /lib64 --dir /subdir/newdir --file 3 /subdir/newdir/ESCAPE_MARKER.txt /usr/bin/truewith a known marker content on fd 3. - Verifies per attempt: vulnerable builds must exit 0 and leave
<host_target>/newdir/ESCAPE_MARKER.txton the host with the exact attacker content; fixed builds must exit non-zero and leave nothing. - Writes
bundle/repro/runtime_manifest.jsonwith SHA-256 of every proof artifact.
- Reads
- Expected evidence: vulnerable attempts show the marker file on the host;
fixed attempts log
bwrap: Can't mkdir parents for /subdir/newdir: No such file or directoryand create nothing on the host.
Evidence
bundle/logs/attempt-vuln-1.log,bundle/logs/attempt-vuln-2.log— bwrap v0.11.0 exits 0, sandboxed/usr/bin/trueruns, and the post-run listing showshost_target/newdir/ESCAPE_MARKER.txtwith the attacker-controlled contentBWRAP_OLDROOT_ESCAPE_<timestamp>.bundle/logs/attempt-fixed-1.log,bundle/logs/attempt-fixed-2.log— bwrap v0.12.0 exits 1 withCan't mkdir parents for /subdir/newdir: No such file or directory; host target remains empty.bundle/repro/work/vuln-{1,2}/host_target/newdir/ESCAPE_MARKER.txt— the actual marker files created on the host by the vulnerable binary.bundle/repro/proof_summary.txt— per-attempt verdicts and binary SHA-256s.bundle/repro/runtime_manifest.json— entrypointcli_command,target_path_reached=true, proof artifacts with SHA-256 hashes.- Environment: Linux 6.8.0-138-generic x86_64, uid 1000 (unprivileged),
kernel.unprivileged_userns_clone=1, user namespaces functional (seebundle/logs/smoke_test.log). Openat2-capable kernel (>= 5.6), so the fixed build exercises the realRESOLVE_IN_ROOTpath.
Key excerpt (vulnerable attempt):
command: timeout 30 .../bwrap-v0.11.0 --bind .../untrusted / --ro-bind /usr /usr ... --dir /subdir/newdir --file 3 /subdir/newdir/ESCAPE_MARKER.txt /usr/bin/true
--- exit_code: 0
host_target (post-run):
.../host_target/newdir:
-rw-rw-rw- 1 pruva pruva ... ESCAPE_MARKER.txt
host marker content: BWRAP_OLDROOT_ESCAPE_20260826T200804Z
Key excerpt (fixed attempt):
--- exit_code: 1
bwrap: Can't mkdir parents for /subdir/newdir: No such file or directory
host marker content: <absent>
Recommendations / Next Steps
- Fix approach (upstream, already shipped): confine all destination path
resolution to the new root using
openat2(RESOLVE_IN_ROOT)(safe_openat()), with thechroot_realpathfallback on pre-5.6 kernels — commits67d4be103b18706b5b4e3f495daa35e89e47b163andea185f6fb135782cabab342e33432e8482a2f5c9in v0.12.0. - Upgrade guidance: upgrade to bubblewrap >= 0.12.0. There is no backport
for versions that support setuid builds; users relying on setuid bwrap should
migrate to unprivileged user namespaces. Until upgraded, avoid binding
attacker-controlled filesystem content into sandboxes (audit Flatpak-style
launchers for
--bind <untrusted> /combined with file/dir-creating options). - Testing recommendations: regression-test every file/dir-creating option
(
--dir,--file,--bind-data,--ro-bind-data,--chmod, bind dest auto-creation) with parent symlinks targeting both/oldroot/...(absolute) and relative..chains; the variant-analysis stage covers these.
Additional Notes
- Idempotency: the script was run four consecutive times (two before the
manifest-ordering fix, two after), all exiting 0; per-attempt work directories
are wiped and recreated (
rm -rf) so runs are hermetic and repeatable. Cache reuse (repo/build/tools) makes reruns take ~2 s. - No sudo/pip in this environment: the build avoids meson (not installable
without root) by compiling the four (vuln) / six (fixed) upstream C files
directly with gcc; this is byte-identical upstream source at the anchored
commits, and binary SHA-256s are recorded in
proof_summary.txt. - Non-sanitized product proof: the primary oracle is real product behavior (host filesystem state + CLI exit codes), not ASAN/UBSAN.
- "Host" scope: bwrap was executed directly on this machine (not nested in
Docker), so
/oldrootis the real machine root and the marker files land in the real filesystem outside any bwrap namespace. - Edge cases not covered here (delegated to variant analysis):
--file,--bind-data,--ro-bind-dataas the primary creating op without--dir; relative (../../oldroot) symlink targets;--symlink-created parents; pre-5.6 kernel fallback path of the fix.
GHSA-PXHW-H44J-8PFX Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
id; uname -a; cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null; cat /proc/sys/user/max_user_namespaces 2>/dev/null; ls bundle/logs bundle/repro bundle/support bundle/learning 2>/dev/nulluid=1000(pruva) gid=1001(pruva) groups=1001(pruva),27(sudo),993(kvm),1000(docker) Linux pruva-beef-01 6.8.0-138-generic #138-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 31 22:41:49 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux 1 127776 bundle/learning: exploit_knowledge_preflight.json policy_inputs.json policy_result.json
Artifacts and Evidence for GHSA-PXHW-H44J-8PFX
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix GHSA-PXHW-H44J-8PFX
FAQ: GHSA-PXHW-H44J-8PFX
Is GHSA-PXHW-H44J-8PFX exploitable?
How severe is GHSA-PXHW-H44J-8PFX?
What type of vulnerability is GHSA-PXHW-H44J-8PFX?
How can I reproduce GHSA-PXHW-H44J-8PFX?
Is the GHSA-PXHW-H44J-8PFX reproduction verified?
References for GHSA-PXHW-H44J-8PFX
Authoritative sources for GHSA-PXHW-H44J-8PFX — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.