Repository navigation
feat(quest): exact GOT backend, shared hook lifecycle, typed thunks (#158 package A) - #222
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>
…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>
…s into verify (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>
…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>
…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>
This was referenced Oct 8, 2026
…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>
thesprockee
added a commit
that referenced
this pull request
Oct 8, 2026
…rocess-wide facade is never destroyed (refs #158) The thunk contract (callback_thunk.h after #222 round 3): no exception crosses between game frames and sentinel frames, the Social handler is noexcept, and every TU that includes callback_thunk.h is built -fno-exceptions. - The facade's slots that call the game (Update delivering the party callbacks, JoinInternal and AcceptInvite asking the accept gate) move to social_game_calls.cpp, built -fno-exceptions. They call social_internal.h functions in social_facade.cpp that contain their own failures and return before the game is called; events are copied into a fixed batch so no heap object is alive while the game runs (more than 32 in one frame are dropped and counted). - social_install.cpp is built -fno-exceptions; the handler is noexcept and reads the facade object published at install instead of constructing anything. - tools/check_quest_social_frames.sh (100755) pins that those objects reference no personality and carry no LSDA, with a negative control on the object that does. - Facade::Instance() is a leaked, never-destroyed object (a function-local static was destroyed at exit while network threads still ran); a test registers an atexit check before first use. - Tests: a game exception thrown by a callback and by the accept gate reaches the game's caller through Update, JoinInternal and AcceptInvite and is neither caught nor counted by the facade; event batch overflow. 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>
thesprockee
added a commit
that referenced
this pull request
Oct 8, 2026
…(refs #158) - The hook is a NEVR_HOOK_RECORD (kSocialHook) armed with SocialThunk::Arm / Disarm and installed with InstallThunk<SocialThunk>; GotHook::Install is no longer called directly. - The handler no longer logs: SelectSocialObject counts (selected, null result, pnsovr unavailable, foreign object) and RegisterSocialReportCounters hands the counters, with the thunk's calls and faults, to the sentinel's reporter (6 of its 8 slots). - Tests define their own record for the production handler; the handler path is checked to log nothing. - The ADR records the registration order and the counters; the recipes build and link hook_report.cpp and the backend without exceptions. 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>
This was referenced Oct 8, 2026
feat(quest): embedded-default config, nevr-quest.json override and activation switches (#158 C)
#220
Merged
Member
Author
|
Landed via #326. |
This was referenced Oct 10, 2026
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.
Package A of #158 (tranches 1e, 1d, 1c). Design: docs/adr/0003, 0004. Tip d96bb82. Earlier rounds are in the commit history; this body states the tree as it is now and what the last two rounds added.
Follow-up round (one commit, d96bb82, on top of the pushed fd02052)
TestBackendBuiltWithoutExceptionsmatched backend functions by the substring8LogEvent, which also matchednevr_quest::{anon}::ApplyEmbedded(PR feat(quest): embedded-default config, nevr-quest.json override and activation switches (#158 C) #220, built with exceptions, takes astd::vector<LogEvent>). It now anchors on_ZN8sentinel<len><name>for the backend's own functions (GotHook,ResolveSlot,LogFields,LogEvent,HexString,SetLogSink,StartReporter,RegisterReportCounter,StopReporter,ReporterRunning,ReporterMain). Fixture: a scratch worktree withorigin/claude-quest-hooks+origin/claude-quest-config(81e446e) merged, conflicts resolved in the working tree only (the CMake source list takes feat(quest): embedded-default config, nevr-quest.json override and activation switches (#158 C) #220'ssentinel_log.cpp/activation.cppand drops its owngot_hook.cppso the backend is compiled once), built for Android; the old test fails on it and the new one passes (merge-fixture.log, excerpt below). The scratch worktree is removed. Mutation R7 (backend library built with exceptions) still fails the new matcher.RegisterReportCounterholds 32 counters (static storage, no heap); the 33rd and any registration afterStartReporterare refused and logged (register_refused). ADR 0003 states the contract: allRegisterReportCountercalls come beforeStartReporter. Test registers 32, refuses the 33rd, refuses one after start, and accepts again after Stop; mutation K1 (back to 8) fails it.error:lines and requires every one to match the rule-specific pattern (the static_assert text;HookRecord.*is private within this context;invalid conversion from .*::Handler) and at least one. An unrelated extra error, or a failure for another reason, fails the gate (mutations H2, H3).TestRawInstallOnlyInTestswalkssrc/quest/testsrecursively and resolves each include of a file outside atests/directory relative to the including file, tosrc/and tosrc/quest/sentinel(eachpath.Cleaned) before testing membership of the test tree. It reads files with the extensions.cpp .cc .cxx .h .hpp .incundersrc/. ADR 0003 now says exactly that. Mutations T1r (#include "../tests/compile_fail/common.h") and T1s (quest/tests/compile_fail/common.h) fail it.relroReadOnlycomment is one, sitting aboveWriteSlot's block; the Go test header andsrc/quest/CMakeLists.txtcitedocs/design/2026-07-13-quest-crash-reporter-injection.md(the file that exists) and the ADR.What the tree provides
src/quest/sentinel/got_hook.{h,cpp}:GotHookresolves a slot by module + symbol + relocation type with optional build ID, pinned link-time address and expected original; refuses with one JSON log line and no write on any mismatch; writes are under one process-wide lock and compare-and-swap; the protection to restore is read from/proc/self/mapsunder the lock; a failed install that may leave our entry reachable poisons the slot.src/core/hook_lifecycle.h: platform-free create / publish / enable ordering and rollback, shared bycore/hooking.h(MinHook) and the GOT backend.callback_thunk.h(thunk,NEVR_HOOK_RECORD),hook_install.h(InstallThunk),pinned_targets.h,hook_report.{h,cpp}(counter reporter),hook_log.{h,cpp}(JSON lines). Onlyclock_gettimeis installed (entry.cpp).Sixth round (commits 8291d46, fd02052)
HookRecordhas a private constructor (friendHookRecordAccess, used byNEVR_HOOK_RECORD);Thunk::Armrefuses and logs (arm_refused) a record whose address is not inside thenevr_hook_recordssection (checked against the linker's__start_/__stop_symbols). Tests: a run-time record is refused and the thunk stays unarmed; the legitimate record arms.compile_fail/direct_record.cppmust not compile ("is private within this context").InstallThunk<T>andThunkInstaller::Install<T>static_assertthatTis aCallbackThunkinstantiation.just test-quest-hooksnow compilessrc/quest/tests/compile_fail/:control.cppmust compile;fake_thunk.cpp("InstallThunk requires a CallbackThunk"),direct_record.cppandplain_handler.cpp("invalid conversion", a non-noexcepthandler) must fail with the message that names the rule, not for some other reason.TestRawInstallOnlyInTestsscans.cpp .cc .cxx .h .hpp .incand also fails if any file outside atests/directory includes a header fromsrc/quest/tests. Remaining limit (macro token pasting, a macro forwarding to the access class) is written incallback_thunk.hand ADR 0003, with the advisory nature of the#error(#undef __cpp_exceptionsdefeats it; the frame sensor on the built library is the real check).ReleasePoisonedSlotsInlogsreleased,still_mappedormapping_unknown. A test seam (SetMapsOpener) feeds it a directory descriptor (read error),/dev/null(empty) and a failed open; mutations Q6/Q7/Q8 each make the retry succeed and fail the test.StartReporter,StopReporterandRegisterReportCountershare one control lock held across thread creation, naming and join (the thread loop uses a separate mutex; order is control then state; the atfork handlers take both).ReporterRunning()added. Test: two threads loop Start/Stop 100 times each; without the control lock it hangs (mutation S1, 3 of 3 runs). The recipe now runs the host test undertimeout 300, so a hang fails the gate.RegisterReportCounter(..., ReportKind::kFaults)marks a counter whose zero is healthy; it never reportsnever_fired(the clock hook's fault counter is registered that way).callback_thunk.hnow matches the API; the dated PR feat(quest): embedded-default config, nevr-quest.json override and activation switches (#158 C) #220 narrative is gone fromhook_log.h; the orphaned comment aboveIsMappedis fixed; the logging rationale is "needs no heap" (the earlier claim that nlohmann throws was contradicted by the host test, which parses withallow_exceptions=false). The #error guard is documented as advisory.For dependants (#221, #226, #227, #229 and the router/social/redirect branches)
Everything from the previous round still applies (records,
InstallThunk,-fno-exceptions, three-argumentStartReporter). New in this round:Thunk::Arm(sentinel::HookRecord<Thunk>{...})and any other run-time record no longer compile or are refused at run time. Only a namespace-scopeNEVR_HOOK_RECORDis accepted.claude-quest-redirecthook_adapter.cpp140-141 conditional re-arm;claude-quest-socialsocial_install_test.cpp86Arm(SocialHandler())): define one record per candidate handler at namespace scope, and arm the chosen one:Thunk::Arm(useA ? kHookA : kHookB);(both areconst HookRecord<Thunk>&). A handler that is a pointer computed at run time cannot be recorded; enumerate the candidates as handlers, or make the handler a single recorded function that branches on state it reads. Each candidate handler is a root for the frame sensor.social_install_test.cpp): define its ownNEVR_HOOK_RECORDin the test translation unit; records in test binaries are fine, the section check is per program.RegisterReportCountertakes an optional thirdReportKind; a counter whose zero is healthy should bekFaults.Thunk::Arm(&H),hook.Install(target, Thunk::EntryAddress(), Thunk::OriginalOut()),Arm(nullptr)): listed in the review forclaude-quest-redirect,claude-quest-social,claude-quest-router; each becomes a record +Arm(record),InstallThunk<Thunk>(hook, target),Disarm().GotStatus::kSlotPoisoned(previous round) and the maps-unknown behaviour ofReleasePoisonedSlotsInneed no change in callers.Gates on tip d96bb82 (flock build lock, 6 jobs; first line of every log is
tip d96bb8253dbda583ec1d663a5d81bdc1e84142c4; one verify run)The mutation runs for this commit's content ran before it was committed (against the working tree that became d96bb82) and rebuilt the library only for R7; the artifact above was rebuilt from the committed tree after them.
Mutation proofs for the follow-up (each applied alone, source restored, restored run passes; mutations18/19.log)
Scratch-merge evidence for #231 (
merge-fixture.log)Not verified
pthread_atforkandpthread_setname_npwere never exercised on a device.HookRecordAccess, or a test-directory wrapper reached through a path the scan does not match, gets around the type-level gates.CNSUser::SendLogInRequestand the engine importsCJson::TStringcalls were not shown exception-free./proc/self/mapsreadability under SELinux on the headset; a concurrentdlclosebetweenRemove's revalidation and its store.ReporterRunning(), TSan was not re-run).refs #158
🤖 Generated with Claude Code