Skip to content

Commit 0cb7042

Browse files
security: sandbox Ghidra JVM via bubblewrap; require Ghidra >= 12.1 (CVE fixes)
RawView drives Ghidra's importers and native decompiler over adversarial, possibly-hostile binaries. Ghidra < 12.1 has high-severity CVEs that a crafted binary can trigger during import/decompilation: - CVE-2026-52757 (7.8) decompiler HighVariable::merge heap use-after-free - CVE-2026-52752 (7.8) SleighBuilder use-after-free - CVE-2026-52750 (7.8) Swift demangler arbitrary code execution - CVE-2026-52753 (5.5) Mach-O export-trie OOM / JVM crash Changes: - Bump default Ghidra bundle to 12.1.3 (GHIDRA_BUNDLE_URL); auto-resolve already prefers the newest GitHub PUBLIC release. - New RAWVIEW_SANDBOX=bwrap option (Linux). Runs the Ghidra JVM inside a bubblewrap mount-namespace sandbox: read-only root, /home+/root+/media /mnt+/srv+/var stripped to tmpfs, toolchain re-bound read-only, project dir the only writable host tree. A compromise of the Ghidra process can no longer read user wallets/.ssh/browsers. Defaults on Linux when bwrap is available; degrades to unsandboxed if bwrap is missing. - controller/smoke test thread the sandbox setting through. - README + CHANGELOG: document Ghidra 12.1 requirement + sandbox. Version bump to 1.3.3 (security release).
1 parent 5930812 commit 0cb7042

9 files changed

Lines changed: 203 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
# Changelog
22

3+
## 1.3.3
4+
5+
### CRITICAL security: Ghidra >= 12.1 + optional sandboxed engine
6+
7+
The bundled/default Ghidra was 12.0.4, which is vulnerable to multiple high-severity
8+
CVEs that a **crafted binary can trigger during import or decompilation** - exactly the
9+
input RawView is built to analyze:
10+
11+
- **CVE-2026-52757** (CVSS 7.8) - heap use-after-free in the decompiler's
12+
`HighVariable::merge()`, triggerable by a crafted binary.
13+
- **CVE-2026-52752** (CVSS 7.8) - SleighBuilder use-after-free, triggerable by
14+
decompiling a malicious binary.
15+
- **CVE-2026-52750** (CVSS 7.8) - Swift demangler arbitrary code execution via a
16+
malicious Ghidra project.
17+
- **CVE-2026-52753** (CVSS 5.5) - Mach-O export-trie out-of-memory / JVM crash.
18+
19+
All are fixed in Ghidra 12.1. This release:
20+
21+
- **Bumps the default/bundled Ghidra to 12.1.3** (`GHIDRA_BUNDLE_URL`), the latest
22+
patched public release at release time. Existing installs should re-point
23+
`GHIDRA_INSTALL_DIR` / `GHIDRA_BUNDLE_URL` at a 12.1+ build and recompile the Java
24+
bridge (`python -m rawview.scripts.compile_java`).
25+
- **Adds `RAWVIEW_SANDBOX=bwrap` (Linux).** The Ghidra JVM now runs inside a
26+
[bubblewrap](https://github.com/containers/bubblewrap) mount-namespace sandbox:
27+
read-only root, `/home` `/root` `/media` `/mnt` `/srv` `/var` stripped to tmpfs, the
28+
toolchain re-bound read-only, and the RawView project dir the only writable host tree.
29+
A compromise of the Ghidra process (e.g. a future importer/decompiler bug) **cannot
30+
read your `~/.ssh`, wallets, browsers, or other host files**. The Python/Qt app stays
31+
user-mode on the host. Enabled by default on Linux when `bwrap` is installed; use
32+
`RAWVIEW_SANDBOX=none` to disable.
33+
34+
**Recommended:** all users analyzing untrusted/hostile binaries should upgrade to this
35+
release and re-point at Ghidra 12.1+ as soon as possible.
36+
337
## 1.3.2
438

539
### Fixes a broken 1.3.1

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ Writes `RawView-source-<version>.zip` on the parent of this repo folder.
105105

106106
Do **not** commit API keys, tokens, or `rawview.env` from your machine. Settings normally live under `%LOCALAPPDATA%\RawView\`. `.gitignore` excludes common secret filenames and large local Ghidra/JDK trees if they are ever copied next to the clone.
107107

108+
### Ghidra version (CVE fixes)
109+
110+
RawView analyzes untrusted binaries **through Ghidra's importers and native decompiler**. Ghidra releases before **12.1** have known high-severity vulnerabilities that a crafted binary can trigger during import or decompilation:
111+
112+
- **CVE-2026-52757** (CVSS 7.8) — heap use-after-free in the decompiler's `HighVariable::merge()`, triggerable by a crafted binary.
113+
- **CVE-2026-52752** (CVSS 7.8) — SleighBuilder use-after-free, triggerable by decompiling a malicious binary.
114+
- **CVE-2026-52750** (CVSS 7.8) — Swift demangler arbitrary code execution via a malicious Ghidra project.
115+
- **CVE-2026-52753** (CVSS 5.5) — Mach-O export-trie out-of-memory (JVM crash).
116+
117+
RawView requires **Ghidra ≥ 12.1** to analyze hostile samples. The bundled default and `GHIDRA_BUNDLE_URL` point at the latest public release (auto-resolved at download time).
118+
119+
### Sandboxed Ghidra engine (optional, Linux)
120+
121+
On Linux with [bubblewrap](https://github.com/containers/bubblewrap), RawView can run the **entire Ghidra JVM (parsers + native decompiler) inside a mount-namespace sandbox** while the Python/Qt app stays user-mode on the host. `RAWVIEW_SANDBOX=bwrap` (default on Linux when `bwrap` is installed) wraps the JVM launch:
122+
123+
- Read-only root, sensitive trees stripped to tmpfs (`/home`, `/root`, `/media`, `/mnt`, `/srv`, `/var`).
124+
- The toolchain (Ghidra, JDK, bridge classes, py4j jar) is re-bound read-only at its real path.
125+
- The **project dir** is the only writable host tree.
126+
- Py4J stays loopback-only; network namespace is shared.
127+
128+
This means a compromise of the Ghidra process — e.g. an unknown importer/decompiler bug in the parser — **cannot read your `~/.ssh`, wallets, browsers, or other host files**. Set `RAWVIEW_SANDBOX=none` to disable, or `bwrap` to enable. Windows always uses `none` (no bwrap).
129+
130+
> Best practice: for highly hostile samples, still consider a dedicated analysis **VM**; the sandbox removes file access, but shared-kernel+network residual risk remains.
131+
108132
## License
109133

110134
[GNU General Public License v3.0](LICENSE).

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "rawview"
7-
version = "1.3.2"
7+
version = "1.3.3"
88
description = "Qt front-end and agent for Ghidra reverse engineering"
99
readme = "README.md"
1010
requires-python = ">=3.11"

‎rawview/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
See LICENSE at the repository root for the full GPLv3 text.
55
"""
66

7-
__version__ = "1.3.2"
7+
__version__ = "1.3.3"

‎rawview/config.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,18 @@ class Settings(BaseSettings):
9292
description="Override URL for the Ghidra public zip download.",
9393
)
9494

95+
rawview_sandbox: str = Field(default="bwrap", validation_alias="RAWVIEW_SANDBOX")
96+
9597
rawview_auto_start_bridge: bool = Field(default=True, validation_alias="RAWVIEW_AUTO_START_BRIDGE")
9698

99+
@field_validator("rawview_sandbox", mode="before")
100+
@classmethod
101+
def _normalize_sandbox(cls, v: object) -> object:
102+
if v is None:
103+
return "bwrap"
104+
val = str(v).strip().lower()
105+
return val if val in ("none", "bwrap") else "bwrap"
106+
97107
@field_validator("ghidra_install_dir", mode="before")
98108
@classmethod
99109
def _empty_ghidra_dir_none(cls, v: object) -> object:

‎rawview/ghidra/bridge.py‎

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,97 @@ def _windows_java_cmdline_limit() -> int:
121121
return 24_000
122122

123123

124+
def _classpath_items(java_args: list[str]) -> list[str]:
125+
"""Extract the classpath entries from a ``-cp`` java argument list."""
126+
try:
127+
i = java_args.index("-cp")
128+
except ValueError:
129+
return []
130+
cp = java_args[i + 1]
131+
sep = ";" if sys.platform == "win32" else ":"
132+
return [c for c in cp.split(sep) if c]
133+
134+
135+
def _build_bwrap_mounts_and_rewrite(
136+
*,
137+
java_exe: Path,
138+
ghidra_install: Path,
139+
classes_root: Path,
140+
py4j_jar: Path,
141+
project_dir: Path,
142+
java_args: list[str],
143+
) -> tuple[list[str], list[str]]:
144+
"""
145+
Build bwrap arguments that sandbox the Ghidra JVM using a read-only root + overlay layout.
146+
147+
Layout:
148+
--ro-bind / / read-only whole root (so the JVM's arbitrary absolute toolchain paths resolve)
149+
--tmpfs over user/sensitive trees to strip real data:
150+
/home, /root, /media, /mnt, /srv, /var
151+
--ro-bind back the exact toolchain dirs that must exist under /home (Ghidra install,
152+
JDK, bridge classes, py4j jar) at their real absolute guest paths.
153+
--bind <project_dir> <same> the ONLY writable host tree (analyzed programs / RE sessions).
154+
--tmpfs /tmp + HOME=/tmp sandbox-local temp space.
155+
156+
The project dir keeps its real host absolute path so Ghidra-returned paths round-trip to RawView.
157+
Py4J is loopback-only, so the network namespace is shared (:func:`_classpath` uses loopback).
158+
With /home + /root stripped to tmpfs and only the toolchain re-bound, a compromise of the Ghidra
159+
process cannot read the user's wallets, .ssh, browsers, or other data.
160+
"""
161+
jdk_root = java_exe.resolve().parent.parent
162+
163+
bwrap_args = [
164+
"bwrap",
165+
"--unshare-pid",
166+
"--unshare-uts",
167+
"--unshare-ipc",
168+
"--unshare-user",
169+
"--die-with-parent",
170+
"--share-net",
171+
"--ro-bind",
172+
"/",
173+
"/",
174+
"--tmpfs",
175+
"/home",
176+
"--tmpfs",
177+
"/root",
178+
"--tmpfs",
179+
"/media",
180+
"--tmpfs",
181+
"/mnt",
182+
"--tmpfs",
183+
"/srv",
184+
"--tmpfs",
185+
"/var",
186+
"--proc",
187+
"/proc",
188+
"--dev",
189+
"/dev",
190+
"--ro-bind",
191+
"/sys",
192+
"/sys",
193+
"--tmpfs",
194+
"/tmp",
195+
]
196+
197+
toolchain_roots: list[Path] = [
198+
ghidra_install.resolve(),
199+
jdk_root,
200+
classes_root.resolve(),
201+
py4j_jar.resolve().parent,
202+
]
203+
for root in sorted(toolchain_roots, key=lambda p: -len(p.parts)):
204+
bwrap_args += ["--ro-bind", str(root), str(root)]
205+
proj = project_dir.resolve()
206+
bwrap_args += ["--bind", str(proj), str(proj)]
207+
bwrap_args += ["--setenv", "HOME", "/tmp"]
208+
bwrap_args += ["--chdir", str(proj)]
209+
210+
# The toolchain stays at its real host paths, so java_args need no path rewriting.
211+
bwrap_args.append(str(java_exe.resolve()))
212+
return bwrap_args, java_args
213+
214+
124215
def _pick_free_loopback_tcp_port(preferred: int, *, span: int = 256) -> int:
125216
"""
126217
Return a port on 127.0.0.1 that is free at probe time, scanning upward from ``preferred``.
@@ -176,6 +267,7 @@ class GhidraBridgeController:
176267
project_dir: Path
177268
java_classes_dir: Path | None
178269
raw_classpath: str | None
270+
sandbox: str = "none"
179271
startup_timeout_s: float = 120.0
180272
# Wait for in-flight RPC before tearing down Py4J (auto-analysis can run a long time).
181273
# Still bounded so Quit does not hang forever on a stuck JVM.
@@ -301,7 +393,32 @@ def _java_command(self, java_args: list[str]) -> tuple[list[str], Path | None]:
301393
_write_java_argfile(argf, java_args)
302394
logger.info("Using Java @argfile (command line length ~%s): %s", approx, argf)
303395
return [exe, f"@{argf.resolve()}"], argf
304-
return [exe] + java_args, None
396+
plain = [exe] + java_args
397+
if self.sandbox == "bwrap" and not sys.platform.startswith("win"):
398+
try:
399+
classes_root = self.java_classes_dir
400+
if classes_root is None:
401+
classes_root = _packaged_bridge_classes_dir()
402+
if classes_root is None:
403+
raise FileNotFoundError("cannot resolve Java bridge classes dir for sandbox mount")
404+
bwrap_args, rewritten = _build_bwrap_mounts_and_rewrite(
405+
java_exe=Path(exe),
406+
ghidra_install=self.ghidra_install_dir,
407+
classes_root=classes_root,
408+
py4j_jar=_find_py4j_jar(),
409+
project_dir=self.project_dir,
410+
java_args=java_args,
411+
)
412+
cmd = bwrap_args + rewritten
413+
logger.info(
414+
"Running Ghidra JVM inside bubblewrap sandbox (%d ro-binds, project rw): %s",
415+
bwrap_args.count("--ro-bind"),
416+
" ".join(cmd) [:240],
417+
)
418+
return cmd, None
419+
except Exception:
420+
logger.exception("bwrap sandbox build failed; falling back to unsandboxed JVM")
421+
return plain, None
305422

306423
def _spawn_and_connect(self) -> None:
307424
"""Start JVM + Py4J, retrying if the listen port is still occupied (stale process / race)."""

‎rawview/ghidra_bootstrap.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ def _ssl_context() -> ssl.SSLContext:
3030

3131

3232
# Official NSA Ghidra release zip (must match a real GitHub release asset name).
33-
# Resolved from https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/latest
33+
# Security: 12.0.4 and earlier are vulnerable to multiple Ghidra CVEs fixed in 12.1+
34+
# (decompiler heap-UAF CVE-2026-52757, SleighBuilder UAF CVE-2026-52752, Swift demangler
35+
# arbitrary code execution CVE-2026-52750, Mach-O export-trie OOM CVE-2026-52753).
36+
# RawView requires >= 12.1 for parsed-malware analysis. Auto-resolution via
37+
# resolve_latest_ghidra_public_zip_url() picks the newest GitHub PUBLIC release.
3438
DEFAULT_GHIDRA_ZIP_URL = (
3539
"https://github.com/NationalSecurityAgency/ghidra/releases/download/"
36-
"Ghidra_12.0.4_build/ghidra_12.0.4_PUBLIC_20260303.zip"
40+
"Ghidra_12.1.3_build/ghidra_12.1.3_PUBLIC_20260817.zip"
3741
)
3842

3943

‎rawview/qt_ui/controller.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def _ensure_bridge(self) -> None:
199199
project_dir=self.settings.rawview_project_dir,
200200
java_classes_dir=self.settings.rawview_java_classes_dir,
201201
raw_classpath=self.settings.rawview_java_classpath,
202+
sandbox=self.settings.rawview_sandbox,
202203
)
203204
self.status_message.emit("Starting Ghidra JVM...")
204205
self.bridge_state_changed.emit(BridgeState.STARTING.value)

‎rawview/scripts/smoke_bridge_test.py‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@
99
def main() -> int:
1010
root = Path(__file__).resolve().parents[2]
1111
sys.path.insert(0, str(root))
12+
_default_ghidra_versions = ("ghidra_12.1.3_PUBLIC", "ghidra_12.0.4_PUBLIC", "ghidra_11.4.3_PUBLIC")
1213
if sys.platform == "win32":
13-
_default_ghidra = str(
14-
Path(os.environ.get("LOCALAPPDATA", os.path.expanduser("~")))
15-
/ "RawView" / "ghidra_bundle" / "ghidra_extract" / "ghidra_12.0.4_PUBLIC"
16-
)
14+
base = Path(os.environ.get("LOCALAPPDATA", os.path.expanduser("~")))
1715
else:
18-
_default_ghidra = str(
19-
Path(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")))
20-
/ "RawView" / "ghidra_bundle" / "ghidra_extract" / "ghidra_12.0.4_PUBLIC"
21-
)
22-
os.environ.setdefault("GHIDRA_INSTALL_DIR", _default_ghidra)
16+
base = Path(os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")))
17+
rpa = base / "RawView" / "ghidra_bundle" / "ghidra_extract"
18+
cand = next((rpa / v for v in _default_ghidra_versions if (rpa / v).is_dir()), None)
19+
if cand is not None:
20+
os.environ.setdefault("GHIDRA_INSTALL_DIR", str(cand))
2321

2422
from rawview.config import load_settings
2523
from rawview.ghidra.api import GhidraAPI
@@ -41,6 +39,7 @@ def main() -> int:
4139
project_dir=s.rawview_project_dir,
4240
java_classes_dir=jcd,
4341
raw_classpath=s.rawview_java_classpath,
42+
sandbox=s.rawview_sandbox,
4443
)
4544
api = GhidraAPI(bridge=bridge)
4645
print("starting JVM…", flush=True)

0 commit comments

Comments
 (0)