# LEAD: MERIT `cmd 5` compares with a host-controlled length (gated, not reached) **Component:** MERIT TA 24.0.0.70 — loadable *and* invokable on this Navi 31. **Class:** potential byte-at-a-time memory-disclosure oracle (unbounded compare length). **Status:** **lead.** The unbounded length is real and confirmed statically; on hardware the handler rejects before the compare, so the oracle is not reachable as driven here. ## The unbounded length `merit_cmd05` validates its slot properly — index `(v3-1) <= 0x3F` (matching the allocator's 1..64 range) and a presence byte `!= 0` — and then: ``` 0x401b28 LDR R7, [R5,#0x4C] ; length <- *(params + 0x4c), HOST-controlled ... loop v10 = 0, 16, 32 ... while v10 != 0x90 ; 9 x 16-byte table entries merit_lib_52b900d8_88(table + v10, params + 0x50, R7) ``` `merit_lib_52b900d8_88` is **memcmp** — verified from its body (word-wise compare, then a byte-wise tail, returning 1/-1/0). There is **no `cmp` on R7 anywhere in the function**; the only compares are the index bound (`0x3F`), the loop bound (`0x90`) and the memcmp result. ## Why an unbounded compare length would matter The table is at `0x40204c`, nine 16-byte GUID-like entries, 144 bytes, and its contents are **in the image** — so an attacker holding the firmware knows them. A length beyond 144 walks forward out of the table. The image is `0x15000` bytes at `0x400100`, and the slot array (`off_401CE0 = 0x415004`) sits just past it holding live per-slot data. So with a length of roughly `0x13000` the compare would run from known image bytes into runtime slot memory, and because only equality is consumed (`memcmp(...) == 0`), each invoke answers "does my guessed prefix match" — the standard byte-at-a-time extension, ~256 invokes per byte, starting from a known prefix. That is the shape of a genuine disclosure primitive. Two things have to hold, and one does not. ## Hardware [HW]: the compare is NOT reached `/root/psp/hw/merit_probe.py` + `merit_probe2.py`: ``` cmd01 (allocate) -> status 0, slot index 1 at params+4132 cmd05, len 0x10/0x20/0x100/0x1000/0x13000, data = real table entry 0 -> identical response every time cmd05, len 0x10, data = 0xAA * 16 -> identical response diff of the whole response page, valid vs garbage: ONLY my own input bytes at 0x50 differ MERIT status (params+8) -> 0x105 (261) in every case ``` Identical responses for a *correct* table entry and for garbage prove the memcmp never ran: had it run, entry 0 would have matched and taken the success branch. `0x105` is a rejection raised by one of the earlier gates: * `merit_cmd05__h1(idx, params+0x14, 0x1010, params+0x14)` must return non-NULL; or * `*(params+0x44) == ` a 4-byte value read from **slot+33..36**, i.e. the caller must already know a slot-resident value. The second gate is the interesting one: it requires knowledge of slot content, which is presumably established by a successful `cmd04`-style provisioning step. Until that is driven, the compare is unreachable and the oracle is theoretical. ## Honest placement Recorded as a lead because the dangerous half is verified (unbounded host length into memcmp, with the target region known to abut live slot data) while the reachable half is measured to fail. Cross-checking the gate values — particularly whether `slot+33..36` is host-settable via `cmd01`/`cmd04` — is the next step, and it is a small hardware experiment. Note also that MERIT was previously "cleared" by the liveness sweep on the strength of `merit_cmd04` alone; `merit_cmd05` was flagged by the same detector and had **not** been read until now. The detector was right to flag it twice. ================================================================================ # RESOLVED — gated by an ASD-derived 32-bit value the host cannot obtain The open question was whether the gate value at `slot+33..36` is host-settable. It is not. **Who writes it:** `merit_cmd04` at `0x4019ec`: ``` 0x4019e8 LDRD R0, R1, [SP,#0x28] ; two dwords of a DERIVED blob 0x4019ec STRB R0, [R5,#0x21]! ; written byte-wise into slot+33.. 0x4019f2 STRB R1, [R5,#4]! ... ; (big-endian byte order) ``` **Where the blob comes from:** `sub_40143C(params+0x14, len, out, &outlen)`, whose only non-trivial callee is **`merit_asd_rpc_200`** — service 2, ASD opcode `0x200`. So those four bytes are the output of an ASD operation, not a transform of host input. **Can the host read them back?** No. Scanning every MERIT function for reads of offsets `0x1f..0x28` relative to a slot finds exactly one: `merit_cmd05` itself, the consumer. (`merit_cmd01`'s apparent hit is a literal-pool load, not a slot access.) No command returns those bytes to the caller. So reaching the unbounded `memcmp` requires the caller to supply, at `params+0x44`, a **32-bit value derived inside ASD** and never disclosed — not brute-forceable at one invoke per attempt, and not obtainable by reading it back. That matches the hardware result exactly: status `0x105` before the compare, identical responses for a correct table entry and for garbage. ## Final verdict **Robustness defect, not exploitable.** The missing upper bound on the compare length is real and worth reporting as hardening (any length below the gate would read past a 144-byte table toward the slot array), but the path is protected by a secret-value gate, so there is no disclosure primitive here. Recorded in full because the reasoning is the useful part: an unbounded length is only as dangerous as the weakest gate in front of it, and that gate turned out to be ASD-derived — the same component that gates the GCP findings.