Repository navigation
Session: the runtime directory is a last-resort save candidate on device too - #185
Conversation
…ice too
A 2026-09-20 report from an unrooted webOS 4.4.3 set (Dev Mode, no ssh) signed
in successfully and then logged "session: could not persist to ANY candidate
path": none of /media/developer, /media/internal or the app dir accepted the
write on that jail. The one path known writable on that exact television was
the runtime root under /tmp — the event log was reaching it — but
in_runtime_dir("auth.json") was only ever offered as a session candidate
under ENV_STEERABLE (the simulator), so a real device install had nothing
left to try.
paths::session_candidates() now offers the runtime-dir path on a device
install too, strictly LAST — after every durable candidate, so an install
whose durable jail path is writable (the ordinary case) is unaffected. A
save that lands only there is not durable (/tmp is swept on reboot); it is
already reported through the same non-durable Uncertain class an unconfirmed
canonical commit uses, never as Durable, whichever legacy candidate the
write actually lands on (pinned by a new async_persistence test alongside
the two existing siblings for ProtectionFailed/Failed).
Regression coverage: paths::tests proves session_candidates() actually
offers the fallback last (RED before the fix — confirmed by reverting just
the production line); session_persistence_tests exercises the real
save_legacy_fallback_locked/read_legacy_locked loops against a candidate
list shaped like the field jail (two unwritable "durable" dirs, one
writable "runtime" dir) via a new TEST_CANDIDATES fixture, proving the
session persists at mode 0600 and reloads from disk across a simulated
process restart; a further test proves write_atomic refuses a symlink
planted at the runtime-dir candidate's own name, since /tmp is a
world-writable bind mount shared across jails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb4d008862
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !ENV_STEERABLE { | ||
| v.push(in_runtime_dir("auth.json")); |
There was a problem hiding this comment.
Prevent stale durable candidates from shadowing the fallback
When an earlier candidate still contains a readable old session but its directory has become unwritable, this addition lets the plaintext save fall through successfully to the runtime file, but save_legacy_fallback_locked returns without removing the stale file while read_legacy_locked always accepts the first parsable candidate. After relaunch, the old higher-priority account/profile is therefore loaded instead of the newly written runtime session. This can occur with the explicitly read-only legacy app-dir migration source or after jail/mount permissions change, so a fallback success must retire or otherwise supersede readable earlier candidates.
Useful? React with 👍 / 👎.
| std::fs::set_permissions(&durable_a, std::fs::Permissions::from_mode(0o500)).unwrap(); | ||
| std::fs::set_permissions(&durable_b, std::fs::Permissions::from_mode(0o500)).unwrap(); |
There was a problem hiding this comment.
Make unwritable-candidate tests work when run as root
In root-run containers or CI jobs with CAP_DAC_OVERRIDE, mode 0500 does not prevent creating files in these directories, so the negative test writes its first candidate and fails, while the positive test never reaches the runtime candidate and also fails. Model refusal through a fault seam or a structurally unavailable/read-only location rather than Unix permission bits so the new regression tests remain valid under the repository's root development environments.
Useful? React with 👍 / 👎.
A reporter on an unrooted LG webOS 4.4.3 set signs in successfully and the session is never saved:
session: could not persist to ANY candidate path. On a device installsession_candidates()offered exactly three paths —
/media/developer/<id>-auth.json,/media/internal/.<id>-auth.jsonand
in_app_dir("auth.json")./media/internalismount roin LG's stock jail profile, so it cannever succeed; the other two are refused on his television.
There is a fourth path we already know is writable there, and we know it from his own report: his
event log sits at
/tmp/com.beb.plxnative.nightly/plxnative-events.log. The jail mounts/tmprw at1777 and the per-install runtime root is app-owned.
in_runtime_dir("auth.json")was already acandidate — but only under
ENV_STEERABLE, so a shipped install never got it.It is offered on a device install now, strictly last, after every durable path. An install whose
jail path is writable — today's ordinary case — sees no change at all. A steerable build keeps that
same path FIRST for its own unrelated reason (concurrent simulators must not share one session file)
and is guarded against receiving it twice, which would otherwise put two entries in the list
resolving to one file.
This is deliberately a partial fix, and the code says so rather than pretending otherwise: a save
that lands only in the runtime directory is reported as the non-durable outcome it is. The sign-in
survives leaving and reopening the app; it does not survive restarting the television. Telling the
user "saved" would be a lie, and the existing
CanonicalCommit/CompletionOutcomevocabularyalready had the right word for it.
The file holds an account token, every per-(user, server) PMS token and the Plex Home roster, and
/tmpis a bind mount of the HOST /tmp shared across jails, so the directory is listable by otherusers on the set. The generic write path's protections therefore have to hold on this candidate
specifically, and are now tested there: mode 0600 asserted on the written file, and
write_atomicproven to refuse a symlink planted at the candidate.
Tests: the fallback is last and not duplicated on a device install; every durable candidate
unwritable plus a writable runtime directory persists and reloads across a simulated process
restart; the same shape with no fallback offered still fails (negative control); an uncertain
canonical commit never reports durable even when the fallback succeeded.
Does not fix the ~48 s main-thread freeze after Continue, and does not explain why the durable
paths are refused — that diagnostic is a separate change.
🤖 Generated with Claude Code