Repository navigation
feat(quest): token auth on the Quest shim over a shared auth core (#158 G) - #223
Merged
Merged
Conversation
…allback thunks (refs #158) WIP: tests and just recipes follow in the next commits. Co-Authored-By: Andrew Bates <a@sprock.io>
Token model, refresh request/response handling and the device-code loop move to src/core with injected HTTP, clock and log, so the Quest shim can compile the same source. The Windows module delegates to them; the new core has not been built or tested yet. Co-Authored-By: Andrew Bates <a@sprock.io>
…158) Real shared objects (JUMP_SLOT, GLOB_DAT, RELRO, writable GOT, lazy) and in-memory images (ambiguous, addend, malformed, vaddr-relative pointers, read-only slot page) through the production Install/Remove. Co-Authored-By: Andrew Bates <a@sprock.io>
…er OpenSSL on arm64-android (refs #158) Session runs the cached-login refresh, the device-code login and the background refresh on its own worker thread, so Start() never blocks the game's bootstrap thread. Adapters: libcurl with certificate and host verification on and the Android system CA directory, an atomic 0600 credential file under /sdcard/Android/data/com.readyatdawn.r15/files/, and a login-link file. Host tests run it under a fake HTTP server and fake clock (just test-quest-shared). Not yet wired into the sentinel (ADR 0003 gates the hook). Co-Authored-By: Andrew Bates <a@sprock.io>
…s into verify (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…#158) Co-Authored-By: Andrew Bates <a@sprock.io>
…refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…ption contract, JSON logs (refs #158) Review of #222: one lock across WriteSlot (two handles on one RELRO page crashed), Remove and Install are compare-and-swap, protection to restore is read under the lock, thunk lets the original's exceptions through and never re-calls the original, atomic publish, fixed-size registry and pthread mutex (no throw from the ELF constructor), GotHook::Forget, JSON log lines, no agent paths in the repo test. Co-Authored-By: Andrew Bates <a@sprock.io>
… a wrong shape, tidy comments (refs #158) - RefreshOutcome::Denied for HTTP 400/401/403 so callers can stop retrying a token the server refuses; other non-200 stays Rejected. - The wider json::exception catch now logs 'valid JSON of an unexpected shape' for [] or {"access_token":1} instead of 'not valid JSON'. gameserver.cpp's RefreshAuthToken call picks up the same catch. - HttpClient::Interrupt() so a shutdown can abort a request in flight. - The device-flow log says open_result= (it was shellexecute_result= on every platform). - RefreshRequestBody test now calls BuildRefreshBody; comments describe the current tree; the refresh lead / fallback relationship is stated once and matches the static_assert. Co-Authored-By: Andrew Bates <a@sprock.io>
… handle dying credentials (refs #158) - CA: read the Android CA directories into memory and pass them as CAINFO_BLOB; CAPATH cannot find Android's old-hash file names with an OpenSSL backend. Fails closed, loudly, with no certificate. Proxy variables ignored, response size capped, every setopt checked, requests interruptible. Loopback TLS tests with a generated CA and an Android-style directory. - The refresh token goes to /data/data/<package>/files (derived from /proc/self/cmdline); only the login link stays on /sdcard. O_NOFOLLOW, exclusive temp file, directory fsync. - A permanent poll error gives up after a bounded run of failures and logs the status. A denied or expired refresh token publishes Expired and restarts the device login. - Session: Start/Stop serialised, no lock held across the log sink, Start does not throw, Stop does not wait out a request. Tests run under a per-test deadline. - N94 sensor pins the core-to-EchoVR log level mapping. Co-Authored-By: Andrew Bates <a@sprock.io>
…haviour (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
thesprockee
added a commit
that referenced
this pull request
Oct 8, 2026
…hared loader, not CAPATH (refs #158) Token auth (#223) found OpenSSL cannot use Android's /system/etc/security/cacerts as a CApath (old MD5 subject-hash names). CurlWsConnector now loads the same directories with quest/auth/ca_bundle into memory and passes CURLOPT_CAINFO_BLOB; with no certificate loaded every Connect fails closed (TlsError, kNoTrustAnchors). The host TLS test trusts directories through the same loader; the verify sensor requires CAINFO_BLOB and forbids CAPATH. Not built or run: the host is under the disk floor. Co-Authored-By: Andrew Bates <a@sprock.io>
…xports, one hook library (refs #158) Second review of #222: handler that swallows the original's exception and then throws is a handler fault (exception identity, not a sticky flag); the entry uses one captured original per call and the rollback after a failed re-protect keeps the original published; per-thread state moves from emulated thread_local (allocates on first use) to pthread keys; streaming relocation scan (no 8-slot cap); protect seam + tests for failed re-protect, compare-and-swap Install and the 64-slot bound; -fvisibility=hidden with an export allowlist test; got_hook compiled once into nevr_quest_got_hook with a justfile sensor; first-call-only log in the clock hook. Not built or run on this commit: the disk was under the 8 GiB floor. Co-Authored-By: Andrew Bates <a@sprock.io>
- Denied needs the 401/403/400 body to name the refresh token; a bare 401 (nakama's wrong-http_key answer) is Unauthorized and does not drop the login. - CA store: every certificate is parsed with OpenSSL and re-encoded, unparsable files are counted and skipped, a directory with nothing parsable falls through to the next, total size bounded. - Per-user credential directory /data/user/<uid/100000>/<package>/files. - Transient login failures (cached refresh, device-code request) retry on a bounded backoff; 4xx never retried; a flaky network does not prompt the player. - CURLOPT_QUICK_EXIT so a shutdown does not wait for a resolver thread; real-curl test that interrupts a stalled request; configure-time curl >= 7.87 check; OpenSSL linked for the CA parse. - Session: Stop() from the worker thread (a log sink) no longer deadlocks or joins itself, Start catches std::exception, the login-link file is removed when the flow throws, the bundle is shared by pointer. - Docs: header and ADR state the current tree. Co-Authored-By: Andrew Bates <a@sprock.io>
… thread (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…aths; ELF sensors (refs #158) Third review of #222. The game uses libc++_shared's libgcc-style unwinder and the sentinel a static LLVM one, so no exception may unwind through a sentinel frame that has a personality. callback_thunk.h is now built -fno-exceptions (enforced by #error), handlers are noexcept, the proxy / pthread-key / exception_ptr machinery is gone. A kWriteVerifyFailed or rollback keeps the original published, a failed re-protect no longer hides an earlier failure, and a failed rollback compare-and-swap is logged. tests/quest pins the hook frames to the personality-free CIE, rejects any non-libc++abi emulated TLS, and counts the backend's compile rules in build.ninja; test-android runs with -count=1 (Go cached a result across a changed .so). Co-Authored-By: Andrew Bates <a@sprock.io>
…nk handler (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…ned slots, counter reporter (refs #158) Fourth review of #222. The frame sensor now starts from every thunk entry and every NEVR_HOOK_HANDLER function (own output section) and follows direct edges, so a handler that passes the original to an exception-enabled helper fails it. A slot where our entry may still be reachable through a foreign hook stays reserved (retry would publish that hook as the original and loop). Hooks only count; a reporter thread logs the counters (first change, then at most once a minute). Thunk contract text states what noexcept does and does not do under -fno-exceptions. The backend library is built -fno-exceptions like the host tests. Host tests also parse every log line with nlohmann::json. Co-Authored-By: Andrew Bates <a@sprock.io>
…isoned-slot status (refs #158) Fifth review of #222. Hooks are defined by NEVR_HOOK_RECORD ({entry, handler} in nevr_hook_records) and Arm takes only a record; the frame sensor requires one record per thunk Entry and walks from each record's entry and handler, with no allowlist. GotHook::Install is private; production installs through InstallThunk (hook_install.h) and only GotHookTestAccess (tests) reaches the raw install. The reporter no longer stays in fast mode because a counter never moved: grace window, one never_fired line per idle counter, then one pass per steady interval; reporter_started line, thread name, spurious-wakeup loop, logging outside the lock, pthread_atfork. A retry on a poisoned slot reports slot_poisoned and ReleasePoisonedSlotsIn only releases unmapped addresses. hook_log.h states its real reasons and that logcat is not a durable log. Co-Authored-By: Andrew Bates <a@sprock.io>
…fail-closed maps check (refs #158) Sixth check of #222. HookRecord has no public constructor and Thunk::Arm refuses a record outside the nevr_hook_records section; InstallThunk and ThunkInstaller static_assert a CallbackThunk instantiation; just test-quest-hooks compiles snippets that break each rule and requires the naming error. The raw-install scan covers every C++ extension and production includes of anything under src/quest/tests. IsMapped treats a failed open, a read error and an empty file as unknown and ReleasePoisonedSlotsIn then keeps the reservation (logged). StartReporter/StopReporter/Register share one control lock; never_fired text corrected and fault-only counters never report it. Doc fixes (example, narrative, logging rationale, advisory #error). Co-Authored-By: Andrew Bates <a@sprock.io>
…refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…e-fail and include gates (refs #158, #231) - TestBackendBuiltWithoutExceptions matches the hook backend's own symbols by exact mangled prefix; the substring 8LogEvent also matched an exception-enabled function that takes a std::vector<LogEvent> (#220's ApplyEmbedded). Shown on a scratch merge with #220. - The reporter holds 32 counters (static storage); the 33rd and any registration after StartReporter are refused and logged; the ADR states that registration precedes start. - Compile-fail snippets must produce ONLY the rule's own errors, each matched against a rule-specific diagnostic. - TestRawInstallOnlyInTests walks src/quest/tests recursively and resolves includes relative to the including file, src/ and src/quest/sentinel before the prefix test. - One relroReadOnly comment, in the right place; ADR wording matches the scan; the stale docs citation in the Go test header and src/quest/CMakeLists.txt points at the real design doc. Co-Authored-By: Andrew Bates <a@sprock.io>
… (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
- Other 4xx than a refused refresh token (a bare 401/403, 400 missing payload, 404) are one attempt: cache kept, player not prompted, Failed until a recovery attempt. Denied now needs the JSON message to be one of the refresh RPC's exact errors, so an echoed or proxy body is not read as a refused token. - A Failed login that is not final (outage longer than the backoff, held 4xx, refused device-code request) is attempted again every five minutes with one request, one Warning per failure class. A device login the player was involved in (expired code, timeout, refused poll) stays final. - While the player holds a link, transient poll failures are waited out until the code's deadline; a 4xx poll ends the login. - CA store: size and file type are checked on the open file before reading, each PEM block is parsed on its own, non-regular entries are counted as skipped. - QuestTokenAuth's public calls are noexcept and catch std::exception; the worker thread is named nevr-auth; the response sink cannot throw through libcurl. - Merge of origin/claude-quest-hooks was committed separately. justfile: sensor for OpenSSL/libcurl in the sentinel; ADR records the symbol-clash risk and the failure policy. Co-Authored-By: Andrew Bates <a@sprock.io>
… not a grep (refs #158) tools/verify_quest_sentinel_link.py walks the Quest CMake target graph from the sentinel (comments ignored, .cmake files included) and, if token auth, libcurl or OpenSSL is reachable, requires --exclude-libs,ALL on the sentinel and a live TestExportAllowlist. TestSentinelLinkLineGuard judges the real link statement in build.ninja. Both are falsified on the review's cases (intermediate target, parent file, comment-only flag, skipped/renamed/missing allowlist test, libcurl directly). Co-Authored-By: Andrew Bates <a@sprock.io>
…158) Co-Authored-By: Andrew Bates <a@sprock.io>
… (refs #158) A 200 whose body has no string "code" (a captive portal, {"status":"ok"}, {"code":""}) showed the player nothing, so it retries on the backoff and then recovers like any other unreadable response, instead of ending the login as final. Co-Authored-By: Andrew Bates <a@sprock.io>
… after it (refs #158) An unterminated BEGIN ends where the next BEGIN starts and is counted unparsable; PEM objects that are not certificates (TRUSTED CERTIFICATE, CRL) are counted instead of vanishing. Co-Authored-By: Andrew Bates <a@sprock.io>
…eshes follow the recovery cadence, recovery prompts log at Info (refs #158) - A 200 poll response the parser cannot read (captive portal) is treated like an outage until the code's deadline; only an object with an "error" member is the server's own refusal. The shared parser is unchanged, so the Windows module still aborts on its first poll error. - A refresh refused for a reason that is not about the token (Unauthorized/ClientError) is retried at the recovery period in the background loop too, with one Warning per class; repeats of the core's own lines drop to Debug. - A device login that starts from recovery logs its state transition at Info. Co-Authored-By: Andrew Bates <a@sprock.io>
…-> awaiting_user line (refs #158) Co-Authored-By: Andrew Bates <a@sprock.io>
…(refs #158) SystemClock::SleepFor waits on a pthread condition variable whose clock is CLOCK_MONOTONIC (libc++ on Android API 26 would use a CLOCK_REALTIME deadline for std::condition_variable::wait_for); SteadyNow reads CLOCK_BOOTTIME so the device-code deadline keeps counting while the headset sleeps and a dead code is not polled after wake-up (the login ends, the player is not re-prompted automatically). Co-Authored-By: Andrew Bates <a@sprock.io>
…rding (refs #158) Recovery with no cache -> device login, a refused re-login request -> recovery, recovery meeting a refused refresh token -> prompt. The 4xx device-request test now asserts what it means (no backoff, one attempt per recovery period); transient lists name 408 everywhere; FakeStore::Last no longer aborts the run on an empty list. Co-Authored-By: Andrew Bates <a@sprock.io>
…aining terminate paths are guarded (refs #158) QuestTokenAuth::Create returns nullptr with a logged reason instead of throwing from the caller's frames; the worker's failure report, ~Session and a failing join() can no longer throw out of a noexcept boundary or leave a joinable std::thread to terminate the process. Co-Authored-By: Andrew Bates <a@sprock.io>
…ons, expiry (refs #158) The link file now carries the verification URL, the code, one line of instructions with the direct link, and the expiry; it is replaced atomically for every new code, a stale file from an earlier session is cleared when the worker starts, and it is removed when the login ends. One Info line says it was written and where (no code). If the directory is not writable an Error says so and an Info line carries the direct link, and the login goes on. Co-Authored-By: Andrew Bates <a@sprock.io>
It has a shebang and was committed 100644; test_no_new_non_executable_shebang_script fails on that. Co-Authored-By: Andrew Bates <a@sprock.io>
This was referenced Oct 8, 2026
Member
Author
|
Landed via #326. |
1 of 11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #158 (package G: token auth on the Quest shim). Tip
caba8cb955ef66478fb658e99fdc22e918603d3f.What it is
src/core/: platform-neutral token model, refresh handling, device-code loop, injected HTTP/clock/log. The Windowstoken_authmodule,RefreshAuthTokenandauth_token.hcall them.src/quest/auth/: Android adapters andSession. Not linked into the sentinel yet; nothing hands the token to the login path (ADR 0003 gate)./data/user/<uid/100000>/<package>/files(package from /proc/self/cmdline); exclusive 0600 temp + fsync + rename + dir fsync; O_NOFOLLOW read. Only the login link is on /sdcard.QuestTokenAutharenoexcept; the worker thread is namednevr-auth.origin/claude-quest-hooks(d96bb82), conflict only in the ADR.Failure policy (what the code does, and the tests pin)
messageis exactly one of the refresh RPC's own errors)Refreshing -> AwaitingUser)missing payload, 404Faileduntil the next recovery attemptFailedand not finalFailed, no recovery (the player was involved)Expiredpublished, one log line, new device loginPC comparison: PC runs the device flow after one failed refresh and aborts a login on the first failed poll; Quest keeps the cache and does not prompt on transient or held failures.
Differences from the previous Windows behaviour
RefreshAuthToken(also called fromsrc/runtime/server/gameserver.cpp) catches everynlohmann::json::exception(wasparse_erroronly); valid JSON of the wrong shape logs "valid JSON of an unexpected shape".refresh_token_named=0|1; the device-flow log saysopen_result=(wasshellexecute_result=) on every platform.kRefreshLeadSec=kFallbackAccessTokenLifetimeSec= 300; comments now state the equality;static_assertis<=.fix/202-token-refresh(4bdc761) is not in origin/main, not merged.Gates on tip
caba8cb955ef66478fb658e99fdc22e918603d3f(hash is line 1 of each log; /var/tmp/work-nevr-runtime/claude-main/tokenauth/)Inside verify4.log:
57 tests, 0 failed checks(auth_core_test),17 tests, 0 failed checks(tls_ca_test). Standalone host runs under the 4G cap before the gates: host7.log, tls7.log.Mutation proof (host; each restored afterwards; logs mut3-*.run.log)
The sensor for linking token auth into the sentinel is only exercised on its not-linked path by
just verify; a replay of its condition against scratch files (sensor-demo.sh) rejects "linked, flag missing" and "linked, allowlist test missing".OpenSSL in the sentinel (ADR 0003)
Token auth is not linked into the sentinel. Linking it would bring OpenSSL/libcurl (about 35 MB unstripped vs 1.8 MB). The game's libraries export about 2411 OpenSSL/curl symbols (OpenSSL 3.0.0-dev, libcurl 7.68.0) and have the sentinel as DT_NEEDED, so the sentinel keeps
--exclude-libs,ALLandTestExportAllowlist;just verifyfails if the sentinel linksnevr_quest_token_authwithout them. The "keeps calls from binding to the game's copy" effect is an inference from a probe, not run on a device.Not verified
/data/user/<n>/<package>/files, Quest multi-user, Bionic behaviour.auth_core_testis compiled for arm64, not run there.std::threadconstructor failure inStart()(caught; not testable here).device_login.txtis a placeholder) and the hook that hands the token to the login path../launch-client.shwas not run.Process note: in this round a mutation script restored a file with
git checkoutand discarded two uncommitted source files; I re-applied them and committed before mutating again. The pushed history is unaffected.Update: delta review and the login file (tip
58a1acd8c31ee4048d1ed4ed1df7c71650c1abea)Host tests only on this tip (
auth_core_test69 tests,tls_ca_test20 tests, 0 failed checks, under a 4G cap; the Android sources also passclang++ --target=aarch64-linux-android26 -fsyntax-only -Werror).just verify/just build-android/just test-androidwere last run oncaba8cb, not on this tip./sdcard/Android/data/com.readyatdawn.r15/files/device_login.txt, four lines:URL: <verification page>,Code: <code>, one instruction line that also carries the direct link with the code filled in,Expires: <UTC ISO time> (unix <seconds>). Written atomically for every new code (a stale file is replaced and is also cleared when the worker starts), removed when the login ends. One Info line says it was written and where (no code). If the directory is not writable: an Error, then an Info line with the direct link; the login continues.codeis transient (retry on the backoff, then recovery), not final.{"error":..}is still final. The shared parser, and so the Windows module, is unchanged.QuestTokenAuth::Createis anoexceptfactory returning nullptr with a logged reason; the worker's failure report,~Sessionand a failingjoin()are guarded.SystemClockwaits on a CLOCK_MONOTONIC pthread condition variable;SteadyNowreads CLOCK_BOOTTIME so a headset sleep ends the old device-code poll instead of polling a dead code.TestSentinelLinkLineGuard).🤖 Generated with Claude Code