Skip to content

fix(launcher): never replace a profile state file we could not read - #77

Merged
ralyodio merged 2 commits into
mainfrom
fix/profile-clobber
Aug 4, 2026
Merged

fix(launcher): never replace a profile state file we could not read#77
ralyodio merged 2 commits into
mainfrom
fix/profile-clobber

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Evidence

A profile that had worked for months came up unusable — blank side panel, New Tab stuck loading. On the same machine, same build, same engine:

Command Result
tron (existing profile) frozen
tron --user-data-dir=/tmp/tb-clean works, with our extension loaded
tron --user-data-dir=/tmp/tb-clean2 --disable-extensions works

Moshpit mode read clearnet (the default — no registry lookups on real endings). So: not the extension, not moshpit, not the engine. The profile was the fault, not the code it ran.

The defect

Three blocks in the launcher edit Chromium's JSON state files. Every one of them did:

try:
    d = json.load(open(p)) if os.path.exists(p) else {}
except Exception:
    d = {}
... json.dump(d, open(p, "w"))

Both halves are wrong, and they feed each other:

  1. The write is in-place and truncating. An interrupted launch leaves a half-written file.
  2. The read then treats that file as absent and writes a stub holding only the key that block cared about.

Default/Preferences is the profile — search engine, startup behaviour, every extension's state. So the second launch after an interrupted one silently factory-resets it, and nothing tells you. Local State has the same shape.

The fix

  • A state file that exists and does not parse is left exactly as found, with a line on stderr naming it.
  • Refusing to write is not refusing to start — the browser still launches, just without that block's setting applied.
  • All three writes now land as a rename (fsync then os.replace) rather than in place, so there is no longer a truncated file for the next launch to misread.

#75 fixed the write half for the search block alone, left its read, and left the other two blocks untouched. This finishes it.

Tests

5 cases: corrupt Preferences and corrupt Local State each survive a launch byte-for-byte; the refusal is announced; the browser still starts; and setting restore_on_startup preserves every unrelated key.

Verified 3 of the 5 fail against the pre-fix launcher — the two clobber cases and the announcement.

Full desktop suite: 169 passed.

Not covered

This stops profiles from being damaged. It does not repair one already damaged — that needs the bad file moved aside so Chromium regenerates it.

🤖 Generated with Claude Code

A profile that had worked for months came up unusable: a blank side panel, a
New Tab that never finished loading. A fresh profile on the same machine, same
build, same engine, with the same extension loaded, was fine — so the profile
was the fault, not the code it ran.

Three blocks here edit Chromium's JSON state. Every one of them did:

    try:
        d = json.load(open(p)) if os.path.exists(p) else {}
    except Exception:
        d = {}
    ... json.dump(d, open(p, "w"))

Both halves are wrong, and they feed each other. The write is in-place and
truncating, so an interrupted launch leaves a half-written file. The read then
treats that file as absent and writes a stub holding only the key that block
cared about. Default/Preferences IS the profile — search engine, startup, every
extension's state — so the second launch after an interrupted one silently
factory-resets it, and nothing says so.

So: a state file that exists and does not parse is now left exactly as found,
with a line on stderr naming it. Refusing to write is not refusing to start —
the browser still launches, just without that block's setting applied. And all
three writes now land as a rename (fsync, then os.replace) instead of in place,
so there is no longer a truncated file for the next launch to misread. #75
fixed the write half for the search block alone and left its read, and left the
other two blocks untouched; this finishes the job.

Tests: 5 cases — a corrupt Preferences and a corrupt Local State survive a
launch byte-for-byte, the refusal is announced, the browser still starts, and
setting restore_on_startup keeps every unrelated key. Verified 3 of the 5 fail
against the pre-fix launcher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

93 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 87

Severity Rule Location
HIGH js-unescaped-html-sink apps/extensions/public/store.js:545
HIGH manifest-typosquat apps/mobile/package.json:35
HIGH js-unescaped-html-sink apps/web/public/dns.js:101
HIGH secret-generic-api-key packages/storage/src/config.ts:51
HIGH secret-generic-credential packages/storage/src/config.ts:51
HIGH secret-generic-credential services/api/src/store/payments.test.ts:11
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:96
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:29
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:34
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:57
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:237
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:266
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:336
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:305
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/settings-sections.js:24
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:77
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:165
MEDIUM insecure-temp-file apps/desktop/src/launcher.test.ts:21
MEDIUM insecure-temp-file apps/desktop/src/tor.test.ts:40
MEDIUM insecure-temp-file apps/desktop/src/tor.test.ts:44
MEDIUM insecure-temp-file apps/desktop/test/launcher.test.ts:92
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:92
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:93
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:104
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:107
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:110
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:155
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:163
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:175
MEDIUM sql-template-interpolation apps/extensions/public/store.js:176
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:222
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:225
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:230
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:282
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:289
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:292
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:302
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:307
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:319
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:419
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:425
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:429
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:449
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:468
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:477
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:481
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:497
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:506

…and 43 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

`pnpm install --frozen-lockfile` failed on this branch with ENOENT trying to
mkdir apps/desktop/node_modules — because the path was checked in as a symlink
pointing at an absolute path that exists on no runner.

It got committed because .gitignore said `node_modules/`, and a trailing slash
matches directories only. A symlink named node_modules — which is what running
the suite against a hoisted store leaves behind — is not a directory, so it was
never ignored. Dropping the slash covers both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 5861494 into main Aug 4, 2026
8 checks passed
@ralyodio
ralyodio deleted the fix/profile-clobber branch August 4, 2026 10:08
ralyodio added a commit that referenced this pull request Aug 4, 2026
main moved twice underneath this branch: #77 rewrote the profile-state blocks
and #78 reworded the Manifest V2 comment after uBlock stopped being bundled.
The conflict was that comment — this branch still carried the old wording, with
the GPU block inserted above it. Kept main's wording and the GPU block, ordered
so the block runs before MV2_KEEP is used.

The branch was parked as WIP with no tests, and launcher flag handling is
exactly what this suite covers, so it should not merge without them. Six cases:
each mode's effect on the command line, that `safe` appends Vulkan to the
disable-features list without displacing the MV2 kill switch it shares that
list with, that an unknown mode falls back to `on` and says so, and that
TRONBROWSER_GPU overrides the stored mode. Verified 5 of the 6 fail against
main's launcher; the sixth asserts the default, which holds either way.

Still worth saying plainly: this is an escape hatch, not a diagnosis. Launching
with --disable-gpu did NOT fix the freeze it was written for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant