Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ backup_*/

# Artefatos de terceiros (gadget Frida, consumidor OpenKore) - nao versionados
relay/resources/

# Marcas temporais de sessão para diagnóstico de uso de item (dados locais)
itemuse_input_marks.json
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ sem cliente, com futura integração no projeto OpenKore.
- [Estado atual das pesquisas](docs/RESEARCH_STATUS.md)
- [Arquitetura](docs/ARCHITECTURE.md)
- [Operação, instalação e diagnóstico](docs/OPERATIONS.md)
- [Recuperação dinâmica de atores e evidências](docs/DYNAMIC_ACTOR_RING.md)
- [Resumo da auditoria e alterações locais de 2026-10-20](docs/RESEARCH_STATUS.md#revisão-do-checkout--2026-10-20)
- [Regras dos agentes](AGENTS.md)

## Setup rápido
Expand All @@ -21,9 +23,9 @@ O OpenKore é instalado separadamente.

Configure seu xKore 1 conforme [operação](docs/OPERATIONS.md#openkore-e-enquadramento-xkore).
Com cliente e consumidor prontos, `./run.sh` usa `relay_capture.conf`;
`./run.sh --no-inject` desativa escrita nessa execução. Os valores intencionais
`./run.sh --raw --no-inject` desativa escrita nessa execução (o wrapper normal não reconhece `--no-inject`). Os valores intencionais
`inject = true` e `inject-call = true` do arquivo permanecem preservados.
O movimento via merge continua reativo; a cifra independente permanece não resolvida.
Na configuração atual, `035F` segue o dispatcher nativo e `0437`/`0438`/`0AF4` usam carriers com merge; a cifra independente permanece não resolvida.

## Testes

Expand Down
97 changes: 92 additions & 5 deletions agent/relay_hook_linux.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ function ringBytes(peer, chunkLen) { // whole chunk + context, or
return Math.max(ringSpan[peer] || 0, 0x40 + chunkLen + 0x800);
}

// Fail closed: never let the outgoing scramble merge modify a registered RX ring.
// Compare the entire write interval, not just its first byte (including _fix0).
function overlapsRecvRing(at, length) {
if (!(at instanceof NativePointer) || !Number.isSafeInteger(length) || length < 0) return true;
if (length === 0) return false;
try {
var end = at.add(length);
if (end.compare(at) <= 0) return true;
for (var peer in ringBase) {
if (!Object.prototype.hasOwnProperty.call(ringBase, peer)) continue;
var base = ringBase[peer];
if (!(base instanceof NativePointer) || base.isNull()) return true;
var span = ringBytes(peer, lastLen[peer] || 0);
var ringEnd = base.add(span);
if (ringEnd.compare(base) <= 0) return true;
if (at.compare(ringEnd) < 0 && end.compare(base) > 0) return true;
}
return false;
} catch (e) { return true; }
}

hookAll(['__recvmsg64', 'recvmsg'], {
onEnter: function (args) {
this.fd = args[0].toInt32(); this.msg = args[1];
Expand Down Expand Up @@ -237,6 +258,23 @@ var scrCall = null; // sendcall: {pc} do ponto de c
// Session-scoped allocator: never overwrite a slot owned by another hook.
// Hardware API reports whether a candidate slot is actually available.
var hwOwners = {};
// Bounded breakpoint lifecycle telemetry; no payload, stack, or user data.
var bpStats = {};
function bpEvent(owner, event, extra) {
var o = {tag:'BP-TELEMETRY', owner:owner, hit:event, ts:Date.now(), generation:sessionGeneration};
if (extra) Object.keys(extra).forEach(function(k) { o[k] = extra[k]; });
send(o);
}
function bpHit(owner, durationMs, result) {
var now = Date.now(), s = bpStats[owner] || (bpStats[owner] = {count:0, last:0, maxMs:0});
s.count++;
s.maxMs = Math.max(s.maxMs, durationMs);
// A rare slow handler is reported immediately; normal hits at most every 5 seconds.
if (durationMs >= 20 || now - s.last >= 5000) {
bpEvent(owner, 'hit-summary', {count:s.count, max_ms:s.maxMs, duration_ms:durationMs, result:result});
s.count = 0; s.maxMs = 0; s.last = now;
}
}
function hwSet(owner, th, pc) {
var old = hwOwners[owner];
if (old) { hwClear(owner); }
Expand All @@ -245,21 +283,39 @@ function hwSet(owner, th, pc) {
var errors = [];
for (var i = 0; i < 4; i++) {
if (used[i]) continue;
try { th.setHardwareBreakpoint(i, pc); hwOwners[owner] = {th: th, slot: i}; return i; }
try { th.setHardwareBreakpoint(i, pc); hwOwners[owner] = {th: th, slot: i}; bpEvent(owner, 'armed', {slot:i, tid:th.id, pc:pc.toString()}); return i; }
catch (e) { errors.push(i + ':' + e.message); }
}
bpEvent(owner, 'arm-error', {tid:th.id, pc:pc.toString(), reason:errors.join(',')});
throw new Error('no free hardware breakpoint: ' + errors.join(','));
}
function hwClear(owner) {
var a = hwOwners[owner]; if (!a) return;
delete hwOwners[owner];
try { a.th.unsetHardwareBreakpoint(a.slot); } catch (_) {}
try { a.th.unsetHardwareBreakpoint(a.slot); bpEvent(owner, 'disarmed', {slot:a.slot, tid:a.th.id}); }
catch (e) { bpEvent(owner, 'disarm-error', {slot:a.slot, tid:a.th.id, reason:String(e)}); }
}
var sessionBlockedUntil = 0, sessionGeneration = 0;
// Low-volume lifecycle diagnostics, no speculative memory dereferences.
var thisDiagSeen = {};
function thisDiag(kind, owner, reason, address) {
var key = sessionGeneration + ':' + owner + ':' + kind + ':' + reason;
if (thisDiagSeen[key]) return;
thisDiagSeen[key] = true;
send({tag:'THIS-CONTEXT',hit:kind,owner:owner,reason:reason,
address:address ? address.toString() : null,
generation:sessionGeneration,ts:Date.now()});
}

function sessionReady() { return Date.now() >= sessionBlockedUntil; }
function resetNativeSession(reason, delayMs) {
if (delayMs === undefined) delayMs = 1500;
sessionGeneration++;
thisDiagSeen = {};
if (scSend !== null && scSend['this'] !== null)
thisDiag('stale','SendPacket','session transition: '+reason,scSend['this']);
if (md !== null && md.thisp !== null)
thisDiag('stale','MoveDispatch','session transition: '+reason,md.thisp);
scDemandScheduled = false;
sessionBlockedUntil = Date.now() + delayMs;
if (scSend !== null) {
Expand Down Expand Up @@ -475,6 +531,24 @@ function scrPrologueBefore(addr) { // nearest
}

function scrTrap(d) {
if (d.type !== 'breakpoint') return false;
var started = Date.now(), owner = 'unknown', result = false;
try {
var pc = d.context.pc;
if (md !== null && ((md.capArmed && pc.equals(md.disp)) || (md.fireArmed && pc.equals(md.site))))
owner = md.capArmed && pc.equals(md.disp) ? 'move-cap' : 'move-fire';
else if (scrCall !== null && pc.equals(scrCall.pc)) owner = 'sendcall';
else if (scr !== null && pc.equals(scr.pc)) owner = 'scramble';
result = scrTrapCore(d);
return result;
} catch (e) {
bpEvent(owner, 'handler-error', {reason:String(e)});
throw e;
} finally {
if (owner !== 'unknown') bpHit(owner, Date.now() - started, result ? 'handled' : 'unhandled');
}
}
function scrTrapCore(d) {
if (d.type !== 'breakpoint') return false;
var ctx = d.context;
if (md !== null && mdTrap(ctx)) return true; // native-dispatch (DR2/DR3)
Expand Down Expand Up @@ -533,7 +607,9 @@ function scrTrap(d) {
// nem assumir onde o XOR é aplicado: final = (valor_atual) ^ original0 ^ nosso0.
if (scr._fix0) {
var f0 = scr._fix0; scr._fix0 = null;
try { var c0 = f0.at.readU8(); f0.at.writeU8((c0 ^ f0.orig0 ^ f0.our0) & 0xff); } catch (e) {}
if (!overlapsRecvRing(f0.at, 1)) {
try { var c0 = f0.at.readU8(); f0.at.writeU8((c0 ^ f0.orig0 ^ f0.our0) & 0xff); } catch (e) {}
} else send({tag: 'SCRAMBLE', hit: 'blocked-rx-ring', phase: 'fix0'});
}
// Find the slot pointer + which body to write. Two modes:
// fixed (scr.reg is a register / sN): pointer = that reg, id at pointer+idoff (configured).
Expand Down Expand Up @@ -570,6 +646,10 @@ function scrTrap(d) {
else if (scr.body !== null && (scr.id === undefined || pid2 === scr.id)) { body = scr.body; } // legacy walk
if (body === null) return true; // not a packet we're injecting
}
if (overlapsRecvRing(bodyp, body.length)) {
send({tag: 'SCRAMBLE', hit: 'blocked-rx-ring', id: pid2});
return true; // preserve one-shot body for a later genuine TX slot
}
var was = null;
try { was = toHex(bodyp, body.length); } catch (e) {}
try {
Expand Down Expand Up @@ -703,6 +783,7 @@ recv('sendcall', onSendCall);
function onSendEmit(m) {
if (scSend === null) send({tag: 'SENDCALL', hit: 'error', e: 'emit with no sendcall armed'});
else if (!sessionReady() || scSend.awaitRecapture) {
thisDiag('stale','SendPacket','emit before context recapture',scSend['this']);
send({tag: 'SENDCALL', hit: 'stale-drop', e: 'waiting for a fresh SendPacket object', ts: Date.now()});
} else if (m.data) {
var queued = Array.from(hexToBytes(m.data));
Expand All @@ -720,7 +801,10 @@ recv('sendcall-emit', onSendEmit);
function drainSendCall() { // only runs on the game thread (SendPacket must)
if (!sessionReady() || scDraining || scSend === null || scSend.fn === null || scSend['this'] === null) return;
if (scSend.queue.length === 0) return;
if (scSend.tid && Process.getCurrentThreadId() !== scSend.tid) return; // so na thread que roda a SendPacket
if (scSend.tid && Process.getCurrentThreadId() !== scSend.tid) {
thisDiag('suspect','SendPacket','wrong execution thread',scSend['this']);
return;
} // so na thread que roda a SendPacket
scDraining = true;
try {
while (scSend.queue.length) {
Expand Down Expand Up @@ -817,7 +901,10 @@ function mdTrusted() {
var obj = mdContext();
if (md === null || md.thisp === null || md.receiver === null ||
obj === null || !obj.equals(md.receiver)) {
if (md !== null && md.thisp !== null) mdInvalidate('receiver unavailable/changed');
if (md !== null && md.thisp !== null) {
thisDiag('invalid','MoveDispatch','receiver unavailable or identity changed',md.thisp);
mdInvalidate('receiver unavailable/changed');
}
return false;
}
return true;
Expand Down
Loading
Loading