Nine of ten malformed frames over-read at the pin, and none of the three fixes is finished - #160
Conversation
Independent review — PR #160Read the diff, Caveat bounding the rest: no network to 1 · BLOCKING —
|
This pull request has a merge conflict, so nothing is building itHead That is not CI being slow. GitHub builds
Every check this repository produces on a pull request — CI, CodeQL and the review — is Resolve the conflict and push; the checks start on the next commit. git fetch origin && git rebase origin/main # or merge, either starts CIIssue #142 carried Said once per head commit, not once per hour: push again while still conflicted and this appears again, because that push will not have been built either. |
0ac1615 to
a796874
Compare
`a7624b0` merged #160 as **14 of its 19 files**: the report and the whole harness landed, and `STATUS.md`, `TASKS.md`, `VERIFIED_FACTS.md`, `OPEN_QUESTIONS.md` and `REUSE_LEDGER.md` did not. Nothing is wrong with what landed — the evidence is all there — but the index around it is missing, and `CLAUDE.md` sends the next agent to `STATUS.md` for where things are. Right now it would tell them this research does not exist, while the document's own References line sends them to `OPEN_QUESTIONS` M15–M17, which on `main` belong to the frame-capacity work. Six files conflicted as add/add, because a squash merge shares no history with the branch it squashed. `main`'s copy of each is **byte-identical** to the version this branch's fixes were applied to — checked with `sha256sum` per file against `0ac1615`, not assumed from the diffstat — so taking this branch's side is a strict superset rather than a discarded change. `STATUS.md` and `TASKS.md` auto-merged with #163 and #216. `check_docs.py` clean, `shellcheck` clean, `ctest` 33/33. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #142
Research only. No Attadipa code changed, and none should have — this
repository links no MeshCore. Documentation and a runnable harness under
docs/research/.Problem
Three open upstream pull requests (#3267, #3269, #3270, plus two closed
predecessors) claim missing length checks in MeshCore's frame and advertisement
parsers, found by fuzzing. The issue asked three things: do they hold at the
revision we pinned rather than at the
devcommits they are based on; what isthe real blast radius traced from the triggering byte to the buffer it lands in;
and what does any of it cost the two Attadipa paths — an external stock companion
node today, a local MeshCore provider later.
The expensive failure mode here is not missing a finding, it is writing down
"out-of-bounds read" and letting it stand in for a consequence nobody measured.
Solution
docs/research/MESHCORE_PARSER_BOUNDS.md, with the executable half indocs/research/meshcore-parser-bounds/.The pin is not lagging, and the
devbases are not an obstacle.d929643issimultaneously our pin, upstream's
maintip and its newest release. Every fileany of the three pull requests touches is byte-identical between the pin and both
of their bases, so their diffs apply to us unchanged and a measurement on a pull
request head is a measurement of our pin plus that pull request's guards.
The findings hold — nine of ten corpus cases over-read at the pin. But at
every reachable call site the backing buffer is a fixed array large enough that
the read stays inside the allocation (256 B in
Dispatcher::checkRecv, 262 Band 250 B in the two bridges, the
Packetobject for adverts), and all nine endin a rejected packet. The one direction where Attadipa supplies the bytes —
CMD_IMPORT_CONTACT— cannot reach the worst of them: it gates onlen > 98andPacket::readFromreaches at most byte 70.Three things the pull requests do not say:
MESH_DEBUG_PRINTLNand then the read executes anyway — no
break, noreturn.f80d805e,app_data[0]is stillread when
app_data_len == 0(AdvertDataHelpers.cpp:34). #3271 is the samecommit, so both advert pull requests are incomplete in the same place. Only
#3267 is complete, and it covers none of what #3270 covers.
Utils::decryptrounds output up to whole 16-byte blocks and documents the precondition its
on-air caller does not honour: 192 bytes into a 184-byte stack buffer for
payload_len181…184. Both it and thePAYLOAD_TYPE_PATHextra_lenunderflow sit behind a 2-byte MAC, which is not the authentication gate it
looks like.
Reporting that upstream is the owner's call and deliberately not done here —
filing on a third-party repository is outward-facing and this run had no
authority for it. The evidence reproduces in one command.
Also updated:
REUSE_LEDGER.md(pin re-check, and a monitored deltas tablethat records these as watched and explicitly not vendored),
VERIFIED_FACTS.md(five entries, each separating "reads past its length" from "leaves the buffer"),
OPEN_QUESTIONS.md(M15–M18),STATUS.md,TASKS.md(T-127 done; entryconditions added to T-013 and T-050).
No ADR changed. ADR-0008 already treats the node as a peer whose output is
validated on arrival; nothing here moved that boundary, and an ADR edited to
restate itself is churn.
attadipa_link's decoder was checked, not assumed —it validates the declared length before reading (
frame_codec.cpp:139,:148)behind a length-check byte and a CRC. Nothing in
link/changed.Two corrections, made on this branch
Both found by checking the report's own citations against the tree rather than
against my notes, and both are in the history rather than silently amended:
tryParsePackethas a second caller —CMD_SEND_RAW_PACKETatexamples/companion_radio/MyMesh.cpp:2000, gated only bylen >= 4. The firstcommit said
Dispatcher::checkRecvwas the sole caller, which was wrong in theone direction that matters most here: it is the single place where a client
hands bytes to a MeshCore parser. Consequence is still nil — 177-byte
cmd_frame, reach ofraw[5], returnsERR_CODE_ILLEGAL_ARG— but thereachability claim was not.
Upstream and reuse
meshcore-dev/MeshCore, MIT (license.txt), pinnedd92964352441e53b93e8667b802e04f6e072b39e.Nothing was vendored. The guards in
05da523eandf80d805eare MIT andusable, but all three pull requests are unmerged and unreleased and two do not
close their own findings, so taking any of them would buy part of a problem into
a project that compiles none of the files. What was taken is the shape of the
inputs: the ten-case corpus is ours, written from reading the parsers. There was
no upstream corpus to port — MeshCore's
test/covers none of these paths andits
AES/SHA256test mocks are no-ops (M13).The harness compiles four upstream translation units unmodified at whichever
revision it is pointed at, against four shim headers. The
SHA256andAES128shims are not ciphers and say so in their own files; nothing measured depends
on what they compute, only on how many bytes the surrounding loops move.
Tests
cmake -S . -B build && cmake --build build && ctest --test-dir build --output-on-failure— 24/24 pass. This branch touches no code, so that is a statement that
nothing regressed, not evidence for any claim in the report.
(
python3-pilmust be installed or the build registers a deliberately failingplaceholder, as CI's own step notes.)
shellcheckclean on the three new scripts. Relative links in every editeddocument resolve.
directory it was written in, and reproduces the matrix in §4 exactly:
base10/10 over-read →pr3267closes A and B, leaves C →pr3270closesC1/C2/C4, leaves A, B and C3.
path_arith: 187 of 1309 reachable(len, path_len)pairs underflowextra_len, and all 187 produce a window pastdata[184].decrypt_bounds: clean atsrc_len=176, faults at 177/180/182 atUtils.cpp:77.One method note, because it changed a result. The first harness used a tight
malloc(len)alone and ASan does not report a read at offset 0 of amalloc(0)— which silently turned two genuine findings green, including theone that shows #3270 is incomplete. The guard page is the mechanism of record and
the trap is documented in the harness.
Hardware
NOT EXECUTED — HARDWARE REQUIRED. Nothing here ran on a radio, a node or any
board, and no host sanitizer result may be presented as radio or HIL validation.
To go further would need: a Heltec V4/V4.3 or SX1262 node to confirm the pull
request authors' own (unverified, trace-free) claim of hardware verification, and
a second radio to inject malformed frames on air. Neither is available and
neither is needed for anything asserted here.
Risks
opposite: the node stays a peer, its output stays validated on arrival, and the
reason is now stronger than it was.
(a compile error naming the file) rather than quietly, and
build.shrefuses aref the clone cannot resolve.
run.shis not a clean revision — P3 and P4 are outside its matrix.Said in the README, in the report, and in the T-013 entry condition, because
this is the one way the artifact could mislead a future pin.
memory-safety defect in shipping third-party firmware; the delay is a real cost
and it is the owner's to weigh, not an agent's to pre-empt.
Remaining blockers
None blocking. Four things this work could not establish, recorded as M15–M18
in
OPEN_QUESTIONS.md: whether the two arithmetic defects run end to end(needs a host build with the genuine AES/SHA-256/ed25519, which would also give
the project its first real MeshCore reference vectors); whether the stale stack
bytes they read can be groomed; what P4's eight over-written bytes hit on an
ESP32-S3 (a compiler question, not a hardware one); and whether a real fuzzing
pass finds more — the corpus demonstrates, it does not search, and
Utils::decryptwas found by following a caller rather than by the corpus.
None of the four blocks anything while Attadipa compiles no MeshCore. All four
become entry conditions the moment a local provider is real.
🤖 Generated with Claude Code