fix(tx): reject unsupported TXDAT_TYPE / TXDSA_TYPE in tx__init()#153
Conversation
tx__init() previously left dsaoff and tlroff uninitialized when the TXDAT_TYPE or TXDSA_TYPE switch case did not match a known value, then proceeded to use those uninitialized values in pointer arithmetic and size-check computations. A peer-controlled OP_TX packet with a type byte other than 0x00 produced undefined behavior at every step from the offset computation through the TXENTRY pointer initialization. Build with -ftrivial-auto-var-init=pattern and a temporary trace showed the offsets reading as 0xfefefefefefefefe (the GCC pattern fill for uninitialized stack locals) on every bad-type case, and the resulting tx->dsa / tx->tlr / tx_sz values were garbage. tx_read's post-tx__init size check happened to reject the test cases with a small buffer, but a larger crafted buffer could pass that check and propagate garbage pointers into tx_val() or process_tx(). Fix: - tx__init() return type changed from void to int (VEOK/VERROR) - Added default: return VERROR to both type switches so uninitialized offsets are never read - Added bounds checks: dsaoff < sizeof(tx->buffer) and tlroff + sizeof(TXTLR) <= sizeof(tx->buffer) so declared counts that would place pointers outside the buffer are rejected - tx_read() now checks the return value and propagates VERROR with set_errno(EMCM_TXINVAL) Also added src/test/tx-read-unknown-type.c that exercises tx_read() with five crafted buffers (valid, unknown TXDAT_TYPE, unknown TXDSA_TYPE, both unknown, oversized MDST_COUNT) and asserts all five yield VERROR. Follows the existing src/test/_assert.h pattern. Mirror protection (verified by static walk of process_tx and confirmed at runtime): bad TXs cannot reach mirror_tx(); the vulnerability is strictly a local-crash concern, not a propagation concern. Closes #90
eb17abe to
28e0fe6
Compare
Update: removed redundant runtime bounds checks (CodeQL follow-up)GitHub CodeQL flagged word8 buffer[sizeof(TXHDR) + sizeof(TXDAT) + sizeof(TXDSA) + sizeof(TXTLR)];and existing
Combined with Removed both checks and added an explanatory comment block pointing to the existing compile-time guarantees. The real vulnerability fix (default cases that reject unknown type bytes) is unchanged. Post-removal verification:
Force-pushed the amended commit. PR content otherwise unchanged. |
Summary
tx__init()now returnsint(VEOK/VERROR) instead ofvoiddefault: return VERRORadded to both type switches so uninitialized offsets are never readdsaoffandtlroff + sizeof(TXTLR)againstsizeof(tx->buffer)tx_read()propagates the new error viaEMCM_TXINVALsrc/test/tx-read-unknown-type.ccovers 5 casesVulnerability proof (pre-fix)
Built with
CCARGS='-ftrivial-auto-var-init=pattern'so any uninitialized stack local shows as0xFEpoison bytes. Temporary trace insidetx__init()captured:The garbage
dsaoff/tlroffflowed intotx->dsa = tx->buffer + dsaoffandtx->tlr = tx->buffer + tlroff, storing pointers far outside the buffer in theTXENTRY.tx_read()happened to reject the test cases on its size check (becausetx_szwas also garbage), but this is luck rather than defense — a larger crafted buffer could propagate the garbage pointers intotx_val()orprocess_tx()for memory corruption.Mirror protection
Static walk of
process_tx()confirmsmirror_tx()is unreachable on any error path.tx_val()itself rejects unknown types. Bad TXs cannot enter the mirror queue. The vulnerability is strictly a local-crash concern, not a propagation concern.Post-fix verification
Same unit test, post-fix, with trace still active:
Each unsupported type byte returns
VERRORimmediately at thedefault:arm — no uninitialized read, no garbage pointer, no UB.Regression
Re-ran the sync against mainnet with the trace still active.
tx__init()is exercised once per transaction during block-validation incatchup(). Thousands of clean entries logged for real mainnet TXs (allTXDAT_TYPE=0x00 TXDSA_TYPE=0x00withdsa_matched=1 tlr_matched=1). Fix does not regress legitimate transaction parsing.Notes
The temporary
plog()instrumentation that produced the diagnostic output above is NOT part of this commit — it was a local debugging aid. The committed code contains only the structural fix. The trace lines shown in this PR body and in the issue comment came from the local test build only.Test plan
make NO_CUDA=1 mochimocompiles cleanly (no-Wno-errorworkaround needed)src/test/tx-read-unknown-type— must print[PASS]_assert.hconventionCloses #90