Skip to content

fix(tx): reject unsupported TXDAT_TYPE / TXDSA_TYPE in tx__init()#153

Merged
adequatelimited merged 1 commit intoaudit-fixesfrom
audit/F-13-tx-init-uninitialized
Apr 13, 2026
Merged

fix(tx): reject unsupported TXDAT_TYPE / TXDSA_TYPE in tx__init()#153
adequatelimited merged 1 commit intoaudit-fixesfrom
audit/F-13-tx-init-uninitialized

Conversation

@adequatelimited
Copy link
Copy Markdown
Collaborator

Summary

  • tx__init() now returns int (VEOK/VERROR) instead of void
  • default: return VERROR added to both type switches so uninitialized offsets are never read
  • Bounds checks added on dsaoff and tlroff + sizeof(TXTLR) against sizeof(tx->buffer)
  • tx_read() propagates the new error via EMCM_TXINVAL
  • New unit test src/test/tx-read-unknown-type.c covers 5 cases

Vulnerability proof (pre-fix)

Built with CCARGS='-ftrivial-auto-var-init=pattern' so any uninitialized stack local shows as 0xFE poison bytes. Temporary trace inside tx__init() captured:

TXDAT TXDSA dsa_matched tlr_matched dsaoff tlroff
0x00 0x00 1 1 0xa0 0x940
0x01 0x00 0 1 0xfefefefefefefefe 0xfefefefefeff079e
0x00 0x01 1 0 0xa0 0xfefefefefefefefe
0xFF 0xFF 0 0 0xfefefefefefefefe 0xfefefefefefefefe

The garbage dsaoff/tlroff flowed into tx->dsa = tx->buffer + dsaoff and tx->tlr = tx->buffer + tlroff, storing pointers far outside the buffer in the TXENTRY. tx_read() happened to reject the test cases on its size check (because tx_sz was also garbage), but this is luck rather than defense — a larger crafted buffer could propagate the garbage pointers into tx_val() or process_tx() for memory corruption.

Mirror protection

Static walk of process_tx() confirms mirror_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:

F-13 TRACE tx__init entry: TXDAT_TYPE=0x01 TXDSA_TYPE=0x00
F-13 TRACE: unknown TXDAT_TYPE -- returning VERROR
F-13 TRACE tx__init entry: TXDAT_TYPE=0x00 TXDSA_TYPE=0x01
F-13 TRACE: unknown TXDSA_TYPE -- returning VERROR
F-13 TRACE tx__init entry: TXDAT_TYPE=0xff TXDSA_TYPE=0xff
F-13 TRACE: unknown TXDAT_TYPE -- returning VERROR

Each unsupported type byte returns VERROR immediately at the default: 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 in catchup(). Thousands of clean entries logged for real mainnet TXs (all TXDAT_TYPE=0x00 TXDSA_TYPE=0x00 with dsa_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

  • Verify make NO_CUDA=1 mochimo compiles cleanly (no -Wno-error workaround needed)
  • Build and run src/test/tx-read-unknown-type — must print [PASS]
  • Confirm the new unit test follows the existing _assert.h convention

Closes #90

Comment thread src/tx.c Fixed
Comment thread src/tx.c Fixed
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
@adequatelimited adequatelimited force-pushed the audit/F-13-tx-init-uninitialized branch from eb17abe to 28e0fe6 Compare April 13, 2026 14:12
@adequatelimited
Copy link
Copy Markdown
Collaborator Author

Update: removed redundant runtime bounds checks (CodeQL follow-up)

GitHub CodeQL flagged tlroff + sizeof(TXTLR) > sizeof(tx->buffer) (and by the same reasoning, dsaoff >= sizeof(tx->buffer)) as always false. Verified: both checks are unreachable dead code because the TXENTRY buffer is declared as:

word8 buffer[sizeof(TXHDR) + sizeof(TXDAT) + sizeof(TXDSA) + sizeof(TXTLR)];

and existing STATIC_ASSERT()s at types.h:501 and types.h:518 enforce:

  • sizeof(TXDAT) == 256 * sizeof(MDST)
  • sizeof(TXDSA) == sizeof(WOTSVAL)

Combined with MDST_COUNT(opts) = opts[2] + 1 having a word8-bound maximum of 256, the largest possible dsaoff + sizeof(WOTSVAL) + sizeof(TXTLR) equals sizeof(tx->buffer) exactly — it can never exceed it. The bounds are structural, not runtime.

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:

  • Clean compile with default -Werror flags
  • Unit test src/test/tx-read-unknown-type still passes all 5 cases (case 5 — oversize MDST_COUNT — now rejects at tx_read's existing size check instead of my removed redundant one, same outcome)

Force-pushed the amended commit. PR content otherwise unchanged.

@adequatelimited adequatelimited merged commit 06e5f85 into audit-fixes Apr 13, 2026
4 of 5 checks passed
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