Cipher: install registry + scan-failure diagnostics#25
Merged
Conversation
Four latent defects in the hook/patch path. Mostly invisible because typical mods install once at startup and never restore — they surface when mods unload at runtime or two mods touch the same address. - Failed hook installs got pushed to the registry anyway. The next mod hooking that address would chain onto a function that was never wired up. - Hook lock-rejection was missing the address-equality check the patch path had. Any `set_Lock(true)` silently blocked hook installs at unrelated addresses. - `m_Restore`'s cleanup loop re-pointed unrelated hooks belonging to other mods and mutated the list while iterating. Likely the "random crashes" TODO from d6a7b81. Deleting the broken half drops ~20 lines. - Patch `set_Lock(true)` silently refused to apply — author thinks the patch is live, it isn't. No API change; no recompile needed for existing mods.
I've been sitting on this one for too long. Cipher is finally getting some introspection. Today the framework is blind to itself:
mods can't ask "is this address already claimed, and by whom?" so the only defence against cross-mod breakage is hand-curated compatibility tables. libtibik's currently lists libTSM (general-purpose, patches lots of things) and libSPT / Sky Packet Tool (network patches). Two entries, both already drifting, and the ecosystem surface keeps growing.
This PR adds the diagnostics layer that makes those tables shrinkable. Canvas observes and reports; it does not decide.
Additive only. No API removed or changed, no recompile at the consumer side needed:
- Cipher::IsAddressPatched / DescribePatchesAt / DescribePatchesIn = query the registry.
- Cipher::ExplainScanFailure = when a pattern scan returns 0, list the installs that overlap the segments the scan would have touched.
Turns silent "pattern not found" into "libTSM patched these bytes first."
- Cipher::OnInstallEvent = subscribe to every successful Fire().
YOLO contract: Canvas stays advisory only, enforcement is at the mod author's discretion. Each mod picks back-off / warn-and-proceed / override.
Example:
if (Cipher::IsAddressPatched(target)) {
LOGW("feature X: address claimed by %s; installing anyway",
Cipher::DescribePatchesAt(target).front().ownerModule.c_str());
}
hook.Fire(); // override at our own risk
Identity is captured automatically via dladdr on the caller's PC - mods don't register themselves. Chain installs and lock rejections now emit always-on LOGI / LOGW lines naming both sides, so conflicts surface in logcat without anyone opting in.
Side-table storage inside libciphered.so; only ABI change is one std::size_t added to CipherPatch. set_Lock semantics untouched.
Collaborator
|
I’d prefer the no-rebuild version if that’s not too much hassle |
Per review: zero ABI bump preferred. m_patchBytes moves into a second side-table inside libciphered.so, keyed by CipherPatch*, populated by set_Opcode, read by Fire. CipherPatch's class layout reverts to the pre-registry one, so any existing mod built against the shipped libciphered.so keeps working unchanged: no header sync, no rebuild, no copy. Verified with two third-party registry-unaware mods running unchanged against this branch: libSPT v1.3.2 (7 hooks installed cleanly) and libTSM v0.29.0. Public API surface is unchanged from the previous revision; only the byte-count storage moved.
Contributor
Author
|
Done, ready to test. Existing mods that don't need or care about the registry (libSPT v1.3.2 and libTSM v0.29.0) keep working exactly as before, without requiring a rebuild. From the run: Mods that want modern overlap diagnostics ("why scan failed", "who else patched this address") opt in via |
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.
Summary
Adds a diagnostics layer to Cipher so mods can introspect each other's hooks and patches at runtime. Currently a mod has no way to ask "is this address already claimed, and by whom?", so mods that need to coexist ship hand-curated compatibility tables that drift the moment a peer updates. This PR makes those tables shrinkable.
Motivation
libtibik's current compatibility table lists libTSM (general-purpose mod, patches tons of things) and libSPT / Sky Packet Tool (network patches). Two entries, both already drifting, and the ecosystem keeps growing. Cipher records nothing about who installed what; chains and rejections happen silently. When
CipherScanreturns 0 because another mod already rewrote the target bytes, the calling mod sees "pattern not found" with no clue why.What's added
All on
class Cipher, allstaticmethods. Additive only: no existing API removed, no existing signature changed.IsAddressPatched(addr),DescribePatchesAt(addr),DescribePatchesIn(start, len)query the registry.ExplainScanFailure(bytes, mask, [libName])andExplainScanFailure(idaPattern, [libName])list installs that overlap the segments a failed scan would have touched. Turns "pattern not found" into "libSPT.so patched these bytes first".OnInstallEvent(callback)subscribes to every successfulFire.struct CipherInstallInfocarries owner basename (fromdladdr), target library, kind (hook or patch), address, byte count, and a monotonic install sequence number.YOLO contract
Canvas observes and reports. It does not decide. Each mod author picks back-off, warn-and-proceed, or override. Example:
set_Locksemantics are untouched. Mods that want framework-enforced exclusivity keep using it; mods that want YOLO ignore it and consult the registry themselves.Implementation notes
libciphered.so, keyed byCipherBase*. Class layouts forCipherBaseandCipherHookare unchanged.std::size_t m_patchBytesadded toCipherPatchso the registry knows the exact byte count for accurate overlap reporting. See "Consumer impact" below; there's a zero-bump alternative if you'd prefer it.dladdr(__builtin_return_address(0))insideFire; mods don't register themselves.__attribute__((constructor)) int main(), which fires Canvas's own ImGui hook at.so-load time.LOGI/LOGWlines naming both sides; previously silent.Consumer impact
Mods using only
CipherHook,CipherScan, andCipherUtils: zero impact. No header sync, no rebuild, no copy of the newlibciphered.sorequired. Existing binaries are unaffected.Mods using
CipherPatch: need to rebuild against the newCipher.hbecause the layout grew by onestd::size_t. An old-build mod that stack-allocates aCipherPatchand callsset_Opcodeon it would have itspatch_tslot corrupted by the new code writingm_patchBytesat the new offset.Mods that want to use the new diagnostic APIs: need both the updated
Cipher.hand alibciphered.sowith the new symbols at link time.Zero-bump alternative: if you'd prefer no consumer rebuild at all (including
CipherPatchusers),m_patchBytescan move into a second side-table insidelibciphered.so. About twenty extra lines, fully ABI-clean. Happy to amend.Proof of concept
End-to-end test on a real device with libtibik (a CipherUtils consumer) and libSPT (Sky Packet Tool) installed simultaneously. libSPT patches network functions in the game library; libtibik's network init then pattern-scans for
EnqueueRequestin the same range.Before this PR:
After this PR, with libtibik consuming
Cipher::ExplainScanFailure:The middle line is the registry attributing the failure to a specific other mod by
.sobasename, with the install sequence number proving it was the first non-Canvas install in the process. Everything fromdladdridentity capture through side-table query through overlap correlation had to work for that line to appear.Testing
set_Lock(true)today). Both paths have always-on logs if anyone trips them.Bundled fixes
A companion commit on the branch fixes four pre-existing latent defects in
CipherHook/CipherPatch(the "random crashes in CipherHook.cpp" TODO from d6a7b81 plus three related). Orthogonal to the registry feature but touches the same files.Open questions
std::size_tbump; happy to refactor to the side-table version.OnUninstallEventis not included. Symmetric toOnInstallEventbut no consumer asked yet; easy add later.