Skip to content

Cipher: install registry + scan-failure diagnostics#25

Merged
MisterGatto merged 3 commits into
skyprotocol:osp-masterfrom
HugeFrog24:osp-master
May 20, 2026
Merged

Cipher: install registry + scan-failure diagnostics#25
MisterGatto merged 3 commits into
skyprotocol:osp-masterfrom
HugeFrog24:osp-master

Conversation

@HugeFrog24
Copy link
Copy Markdown
Contributor

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 CipherScan returns 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, all static methods. Additive only: no existing API removed, no existing signature changed.

  • IsAddressPatched(addr), DescribePatchesAt(addr), DescribePatchesIn(start, len) query the registry.
  • ExplainScanFailure(bytes, mask, [libName]) and ExplainScanFailure(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 successful Fire.
  • struct CipherInstallInfo carries owner basename (from dladdr), 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:

if (Cipher::IsAddressPatched(target)) {
    LOGW("feature X: address claimed by %s; installing anyway",
         Cipher::DescribePatchesAt(target).front().ownerModule.c_str());
}
hook.Fire();  // override at your own risk

set_Lock semantics are untouched. Mods that want framework-enforced exclusivity keep using it; mods that want YOLO ignore it and consult the registry themselves.

Implementation notes

  • Storage is a side-table inside libciphered.so, keyed by CipherBase*. Class layouts for CipherBase and CipherHook are unchanged.
  • One ABI bump: std::size_t m_patchBytes added to CipherPatch so 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.
  • Identity is captured via dladdr(__builtin_return_address(0)) inside Fire; mods don't register themselves.
  • Registry state uses function-local statics to avoid an init-order race with main.cpp's __attribute__((constructor)) int main(), which fires Canvas's own ImGui hook at .so-load time.
  • Chain installs and lock rejections now emit LOGI / LOGW lines naming both sides; previously silent.

Consumer impact

Mods using only CipherHook, CipherScan, and CipherUtils: zero impact. No header sync, no rebuild, no copy of the new libciphered.so required. Existing binaries are unaffected.

Mods using CipherPatch: need to rebuild against the new Cipher.h because the layout grew by one std::size_t. An old-build mod that stack-allocates a CipherPatch and calls set_Opcode on it would have its patch_t slot corrupted by the new code writing m_patchBytes at the new offset.

Mods that want to use the new diagnostic APIs: need both the updated Cipher.h and a libciphered.so with the new symbols at link time.

Zero-bump alternative: if you'd prefer no consumer rebuild at all (including CipherPatch users), m_patchBytes can move into a second side-table inside libciphered.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 EnqueueRequest in the same range.

Before this PR:

[net] EnqueueRequest pattern not found
[init] network feature unavailable (API calls disabled)

After this PR, with libtibik consuming Cipher::ExplainScanFailure:

[net] EnqueueRequest pattern not found
[net] EnqueueRequest scan overlaps with libSPT.so install at 0x76f42519ac (install seq #1)
[init] network feature unavailable (API calls disabled)

The middle line is the registry attributing the failure to a specific other mod by .so basename, with the install sequence number proving it was the first non-Canvas install in the process. Everything from dladdr identity capture through side-table query through overlap correlation had to work for that line to appear.

Testing

  • Smoke test, Canvas alone, no mods: works.
  • libtibik alone: works.
  • libtibik + libSPT, the known-conflict combo: diagnostic line above appears as designed.
  • Chain detection and lock rejection paths exist but did not fire in the tested combos (the tested mods patch disjoint addresses and nobody uses 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

  1. Zero ABI bump or not (see Consumer impact). Default in this PR keeps the one std::size_t bump; happy to refactor to the side-table version.
  2. OnUninstallEvent is not included. Symmetric to OnInstallEvent but no consumer asked yet; easy add later.

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.
@MisterGatto
Copy link
Copy Markdown
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.
@HugeFrog24
Copy link
Copy Markdown
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:

[net] EnqueueRequest scan overlaps with libSPT.so install at 0x7dd902f9ac (install seq #1)
[candle-patch] auto-collect-wax @ 0x7dd8f91d80

Mods that want modern overlap diagnostics ("why scan failed", "who else patched this address") opt in via Cipher::ExplainScanFailure / IsAddressPatched / etc.

@MisterGatto MisterGatto merged commit 7a2bcf1 into skyprotocol:osp-master May 20, 2026
1 check failed
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.

2 participants