October 29, 2026

I Rewrote My Samsung TV's App Database: Here's Where the Trust Stops

A field note from a 2020 Samsung TU8000 on Tizen: one vulnerability advisory, a test-signed .NET app built on a Mac mini, a security checker that rejected two builds, a debug bridge that kept dying mid-session, and a byte-level rewrite of the launcher's own database that the launcher read but never trusted.

Samsung publishes security advisories for their TVs the way most vendors do: a CVE or SVE number, a severity score, a one-line description, and not much else. One of the recent ones, SVE-2025-50109, covers the TV's application database, the thing the launcher uses to decide what shows up in your Apps grid. The description was thin enough that the real question was answerable only one way: get onto one of these TVs, find the database, and find out how much of that attack surface actually exists on a living-room set.

So that's what we did. This is my own TV, a TU8000, on my own network. Everything below required developer mode turned on in the TV's settings menu, which is an owner action, and every tool ran over the local network. If your TV has developer mode off, none of this lands on it. But the question of what a resident app can and can't touch once it's on the box turned out to have a very clean answer, and it's not the one I expected.

The setup

The TV runs Tizen, which is Samsung's Linux. Developer mode opens a debug bridge called sdb, Samsung's version of adb, reachable over WiFi on port 26101. Once that's connected from a laptop or a Mac mini on the same network, you can install packages with sdb install, and the packages just need to be signed with Samsung's free developer test certificates, which they hand out through their developer site along with a device registration process.

The test app itself is a .NET application. Tizen supports C# apps through a .NET SDK, so the plan was simple: build a small app called org.hermes.tvagent, install it, and have it poke at the system from inside a real third-party process. That's the position a real attacker would be in after compromising one app, and it's the position worth mapping.

A note on division of labor, since this whole thing ran faster than it had any right to: I did this with my AI assistant doing the heavy lifting. It wrote the C#, built and signed the packages, parsed the database dumps, and kept a running log of every verdict. I drove the TV, made the calls, and provided the hands. Even with that, the session ran most of a night. There were three separate dead ends that each ate a rebuild cycle, and none of them were wasted. That's the story.

The bouncer: Samsung's package policy checker

The first app built clean, installed clean, and then refused to launch. The TV popped up a dialog:

TVAgent.dll references forbidden assembly LibC.so.6

That one took a while to understand, because the app didn't import libc. The answer was hiding in the package itself. The .NET build had packed a native launcher binary into the package, an x86-64 Linux executable linked against desktop glibc. Two facts about that: the TV is ARM, so the binary could never run there, and the policy checker reads the package anyway and rejects the whole thing on sight.

The better discovery came right after: the TV ignores that binary entirely. Tizen launches .NET apps through its own host, reading the manifest entry that names the managed DLL. The packaged launcher is dead weight that exists only to trip the checker. One project setting (UseAppHost=false) and the package dropped from 66,128 bytes to 13,414.

Second rejection:

TV Native.dll refers a forbidden assembly directly: Libaul.so.0

This is the one that taught me the checker's real shape. It doesn't scan your entry assembly. It scans every assembly in the package for direct P/Invoke declarations into a blocklist of system libraries: the app launcher's internals, the SQLite engine, and friends. My test app had a helper assembly that touched one of those to read the database the "normal" way. Cut it out, ship pure managed code, and the checker passes.

The policy checker scans the whole package, and it runs at launch, not at install. Every install succeeds. The rejection happens when a human presses the tile, which means you find out one launch-cycle at a time, from your couch, reading a popup on a 65-inch screen.

The ladder: what an installed app can actually touch

With a launchable app in place, the real map started drawing itself. Tizen secures its system databases with SMACK, the Linux simplified mandatory access control framework, and the map came out as a ladder:

/opt/dbspace/.webappservice.db (the app database, "wasdb"): read-write from the app. Opened with a plain FileStream, both read and write.

/opt/dbspace/.pkgmgr_parser.db (the package manager database): read-only. Attempting a write:

UnauthorizedAccessException: Access to the path '/opt/dbspace/.pkgmgr_parser.db' is denied.

Same for the app service database. So: the database that stores what every installed app is called, where its icon lives, whether it's hidden, and how many times it's been used, is writable by any installed app. The database that decides where the app's executable lives is not.

Remember that split. It's the ending of this story.

The ladder had other rungs. The app could spawn system binaries: /bin/sh launches fine from app context. It could not run copies of anything staged in its own data directory, so the exec gate is by path, not by capability. And some familiar commands just don't exist on this TV: no /bin/echo (the shell builtin covers it, but the standalone binary is genuinely absent), no curl, no id, no cat, no env. The pivot that made exec usable was file-based capture: run the commands with output redirected into a file, spawn without waiting, sleep a few seconds, read the file back. Attempting to capture through pipes after a hung child took the whole app runtime down mid-test. Files are the only reliable channel.

The debug bridge kept dying

Midway through, the sdb shell lane wedged. Not the connection: sdb install and sdb uninstall kept working perfectly through all of it. Just the shell: every command returned a closed result in about 0.15 seconds, three nights in a row. It survived reconnection, a standby power cycle, and a remote power-off/on. The historic fix for this class of wedge is a cold boot from the wall plug, which I haven't done yet, so that lane stayed dead for the rest of the session.

The reason this didn't stop anything is worth stating plainly: the app's own network channel was independent. Every test verdict came out of the TV as an HTTP POST from the app to a listener on the Mac mini, on a port I picked for no reason other than it was free. Package installs didn't care about the wedge either. So the night continued on two healthy lanes while the third sat there mocking me.

If a remote-management channel dies, check what else never depended on it. The install service and the app's own outbound HTTP were completely orthogonal to the wedged shell. Half the discipline in a session like this is noticing which two of your three lanes still work and refusing to burn time on the third.

Byte surgery

The app database turned out to be a SQLite file, 49,152 bytes, one main table called widgetinfo, 101 columns. Thirty-six installed apps. The schema was readable straight out of the file header region, and every interesting field was there: title, icon path, install path, hidden flag, use count, launch date.

Here's where it gets stupid, in the fun way: I couldn't run SQL against it. The SQLite engine is on the policy blocklist (second rejection above), and the TV doesn't carry a sqlite3 command-line tool at all. So the app does what any stubborn piece of firmware would do: it reads the raw bytes and finds things by string anchor.

The technique is the same one that edits save files in video games. Find a unique string near the target (org.hermes.tvagent, the app's own package id), measure the fixed offset from that anchor to the field you want (the title sits 20 bytes past it, the install path 27), and flip bytes in place, same width, same length, no structure change. The first edit: the app's own display title, one byte, TVAgent to TVAg3nt. Write it, flush it to disk for real, read it back, post the verdict.

pre cur=TVAgent ... WROTE TVAgent->TVAg3nt readback=TVAg3nt ROUNDTRIP-OK

Then the part that mattered: launch the app again. A fresh run read the database before touching anything and posted:

pre cur=TVAg3nt

The edit had survived the app exiting, the launcher service doing whatever it does between launches, six minutes of wall time. And I saw it with my own eyes: the TV's loading screen, the one that shows while an app spins up, displayed the launcher reading the edited title live. TVG3NT, right there on the screen, because a byte in a database I own said so.

The launcher reads this database live, every single launch. That's what made the next test worth running.

The main event: pointing YouTube somewhere else

With proven write access and proven live reads, the natural escalation is the one the advisory was really about: if you can rewrite the database, can you make the TV launch the wrong thing?

The mechanics were ready-made. In the database, most streaming apps don't have tidy package-name paths; they have random ten-character directory names as their install paths. YouTube's was 9Ur5IzDKqV. TikTok's was 9FhM0cODbY. Ten characters each. Same width. The byte-surgery technique doesn't care what the bytes mean.

The app got a new mode. On launch, it would find both records by their individual anchors, and if YouTube's record pointed at YouTube's directory and TikTok's at TikTok's, it would swap them: both the install path and the resources path, six byte-writes total, read back and verified. Next launch, it would notice the swapped state and swap them back. Fully self-reversing, guarded with abort-and-dump-diagnostics in case the record layout ever shifted under it.

First launch:

CROSS-PLANTED writes=6 yt 9Ur5IzDKqV->9FhM0cODbY tk 9FhM0cODbY->9Ur5IzDKqV readback-yt=9FhM0cODbY readback-tk=9Ur5IzDKqV

The swap was live in the database, confirmed by reading it back. Now the test: press YouTube in the Apps grid.

YouTube opened YouTube.

That's it. That's the whole result, and it's the most informative two seconds of the night. The launcher does not resolve the executable path from the database it reads display metadata out of. Either it reads the launch target from the package manager database (the write-protected one), or it checks the wasdb value against the package manager before honoring it. Which of the two it is, I can't tell from outside, and it doesn't matter: the write-protected side of the ladder is where the authority lives.

One more launch of the test app posted RESTORED writes=6 with readbacks back at the original values, and the database was pristine again. The self-reversing design meant the whole experiment cleaned up after itself.

The database that stores what apps are called is not the database that decides what runs. Samsung splits display metadata (writable cache) from execution authority (protected package manager records), and that split is why a full rewrite of the app database, with verified readbacks, buys you a renamed tile and nothing else. The advisory's headline capability has a floor under it that the advisory doesn't mention.

What I'd tell someone else

The checker teaches you the platform's threat model in two popups. Native binaries: no. Direct P/Invoke into launcher or database internals: no. Everything the platform authors considered dangerous is enumerated in what they reject.

Same-width byte edits are the universal fallback when you have file write access but no query language, no library, and no schema tooling. String anchors plus fixed offsets plus readback verification. It's save-game editing, and it works on appliances just as well as it works on games.

Build the self-reversing behavior in from the start. Every experiment on that TV ended by putting the system back the way it found it, and the mechanism was always the app itself noticing state and undoing it. If your test payload can't clean up after itself, it isn't finished.

A wedged management lane is not a stopped project. Figure out which lanes are independent, keep those healthy, and stop poking the dead one.

The honest close: the test app is still installed on the TV, visible in the Apps grid, and it persists as a normal installed package. The title edit persists too, re-applied and self-reversed on every launch, which is cosmetic but real: user-visible metadata that an installed app can change and keep changed. What doesn't exist, at any point in the session, is code execution outside the app sandbox, launch hijacking, or auto-start persistence. The next experiment is already queued: whether an app can be launched while the TV is sitting on an HDMI input, because that decides whether the alert-overlay idea I actually want lives or dies.

Test environment: Samsung TU8000 (2020, 65-inch UN65TU8000, Tizen 5.5, current firmware as of October 2026, system updates declined and installed manually), sdb debug bridge at version 4.2.36, .NET Tizen SDK building for netcoreapp3.0, packages signed with Samsung developer test certificates, app built and served from a Mac mini on the same network, all testing between roughly midnight and 2 AM. Nothing left modified: the app database was returned to its original state after every experiment, verified by readback. The only standing changes are the installed test app itself and one toggleable byte in its own display title.

I Rewrote My Samsung TV's App Database: Here's Where the Trust Stops | Tech Connect Arizona