The research landed without its index, and the review's three code findings are still open - #218
Conversation
…ree fixes is finished Research only, from issue #142. No Attadipa code changed and none should have: this repository links no MeshCore. What changes is the evidence a future pin decision is made on, and the criteria are now written down instead of implied. Three upstream pull requests claim missing length checks in MeshCore's frame and advert parsers. The issue was cautious about comparing them to our pin because they are based on `dev`, and that caution turns out to be unnecessary: every file any of them touches is byte-identical between `d929643` 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. `d929643` is also upstream's current `main` tip and its newest release, so the pin is not lagging — nothing containing these guards has shipped. The findings hold. A harness compiling four upstream translation units unmodified, with each input exactly its declared length ending flush against a PROT_NONE guard page under ASan, over-reads on nine of ten cases at named lines. The first version used a tight malloc alone and ASan does not report a read at offset 0 of a malloc(0), which silently passed two real findings; the guard page is the mechanism of record and the note is in the harness so the next person does not lose the same hour. What "out-of-bounds" is worth here is smaller than it sounds for the three the pull requests describe. At every reachable call site the backing buffer is a fixed array big enough that the read stays inside the allocation -- 256 B in Dispatcher::checkRecv, 262 B and 250 B in the two bridges, the Packet object itself for adverts -- and all nine cases end in a rejected packet. The one path where Attadipa is the party supplying the bytes, CMD_IMPORT_CONTACT, cannot reach the worst of them at all: it gates on len > 98 and Packet::readFrom reaches at most byte 70. Two things the pull requests do not say, and one nobody has said. #3269's entire diff logs the condition and then performs the read -- no break, no return -- and #3270 leaves app_data[0] unguarded for app_data_len == 0, measured on its own head, so both are incomplete against their own findings and #3271 is the same commit as #3270 rather than merely equivalent. Separately, Utils::decrypt rounds its output up to whole blocks and documents the precondition its on-air caller does not honour: 192 bytes into a 184-byte stack buffer for payload_len 181..184, reproduced against the real translation unit. That one is a write, it is in no pull request, and both it and the PATH extra_len underflow sit behind a two-byte MAC, which is not the authentication gate it looks like. Reporting that upstream is the owner's call and deliberately not done here. attadipa_link's own decoder was checked rather than assumed and is not analogous -- it validates the declared length before reading, behind a length check and a CRC. Nothing in link/ changed. No ADR changed either: ADR-0008 already treats the node as a peer, and an ADR edited to say what it said is churn. What could not be established is a section of its own, not a footnote: neither arithmetic finding was driven end to end, the stale bytes they read may or may not be groomable, what the eight over-written bytes hit on an ESP32-S3 is a frame layout nobody here has compiled, and a ten-case corpus demonstrates rather than searches. M15-M18. The harness is kept because the alternative to a runnable one is a table nobody can re-derive when upstream moves; it is under docs/ so no glob can sweep it into a build. Running it is now the entry condition on T-013's pin choice, and path_arith and decrypt_bounds are not in its matrix, so a green run.sh is not a clean revision. Neither README needed a counterpart edit -- nothing in either describes MeshCore revisions or this research. Tests: cmake configure, build and ctest -- 24/24 pass, unchanged by this branch, which touches no code. Harness verified from the committed copy. Nothing ran on a radio, a node or a board: NOT EXECUTED -- HARDWARE REQUIRED. Fixes #142 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the link Correcting this branch's own first commit. The report said Dispatcher::checkRecv was the sole caller of tryParsePacket, which a grep across the tree contradicts: examples/companion_radio/MyMesh.cpp:2000 calls it too, from CMD_SEND_RAW_PACKET, on a client-supplied buffer gated only by len >= 4. That is not a detail. It is the single place in the whole review where a *client* hands bytes to a MeshCore parser, and a client is precisely what Attadipa is on the node path — so the one direction the report described as having no exposure at all turns out to have exactly one. Four bytes from any connected app make the node read four bytes of its previous command frame. The consequence is still nil, and for the same reason as everywhere else here: cmd_frame is a 177-byte member array, the parser's furthest reach is raw[5], the read stays inside the allocation and the call returns ERR_CODE_ILLEGAL_ARG. The neighbouring companion path was checked in the other direction and holds -- CMD_IMPORT_CONTACT gates on len > 98 while Packet::readFrom reaches at most byte 70, so that one cannot be triggered at all. What changes is the reachability claim, which was wrong, and what a future Attadipa client that emits raw packets has to know. Two smaller fixes found in the same pass: the three memcpys in onContactResponse are at MyMesh.cpp:722, :733 and :744, not :735, and `i` is 8 in the status and telemetry branches but 6 in the binary-response one. Found by verifying the report's own citations against the tree rather than against my notes, which is where the missing caller had been lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checked every citation in the report against the tree instead of against what I had written down while reading, which is how the missing tryParsePacket caller got in. Four were off by one or two lines: the "multiple of 16" comment is src/Utils.cpp:81, the decrypt loop is :76-79, the Attadipa decoder's short-frame wait is frame_codec.cpp:147, and simple_repeater tests isValid() at MyMesh.cpp:657 rather than at the constructor a line above. Nothing a reader would have been misled by, and all four are the kind of thing that makes the next reader stop trusting the ones that matter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report reused M2's count from 2026-08-21 to say the commits dev is ahead by are elsewhere in the tree. The conclusion holds -- every file the pull requests touch is byte-identical -- but the number does not: measured today, dev at 9d7cee66 leads d929643 by 31, and 29 is #3267's base rather than dev's tip. Measured with git rev-list --count against a full clone. Worth saying how, because the first attempt returned 1: the clone had been made blobless and then deepened with --depth=1 fetches for the specific SHAs, which makes it shallow, and a shallow clone will answer a rev-list count with a confident wrong number rather than an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n twice The branch has been open long enough for `main` to move 35 commits under it, and the conflict was the smaller half of the problem. Three files conflicted — `STATUS.md`'s date line, and one additive block each in `TASKS.md` and `VERIFIED_FACTS.md` — and all three keep both sides, because both sides are real. What the merge exposed is that this work was filed under identifiers that other work now owns: - **`T-127` is taken twice on `main`** — the `#anchor` check at `TASKS.md:2935` and the companion frame capacity at `:3406` — so this would have been the third. It is **T-173**, the first free number. - **`STATUS.md` called the same work `T-051`**, which is the not-started **P1** MIA-M10Q GNSS research. That is the one that costs something: `CLAUDE.md` sends the next agent to `STATUS.md` for where things are, and they would have read a live P1 as closed while the research that was actually done had no entry under any real ID. Found by the independent review of #160, and CI could not have: `check_docs.py` checks duplicate IDs *within* `TASKS.md`, so an ID that is consistent there and wrong in `STATUS.md` passes every check. - **`M15`–`M18` collided the same way**, in `OPEN_QUESTIONS.md`: the frame-capacity research took M15–M19 on `main` while this was open. The four parser-bounds questions are **M20**–**M23**, with a note in place saying so. Also merged the two `VERIFIED_FACTS.md` entries that say our MeshCore pin is upstream's tip. They were written hours apart by two research runs from different API calls; one entry now carries both sources, and the other is a signpost rather than a deletion, because the failure mode of a duplicated fact is a reader citing whichever copy nobody updated. Two citations moved off their lines because of the inserts and are re-pointed at the facts they name (`VERIFIED_FACTS.md:264` and `:697`). `check_docs.py` clean. No Attadipa code in this commit and none in this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…'s bounds fix Two separate things, both from #142 and neither of them production code. ## The independent review of #160, findings 2, 3 and 4 **`build-extras.sh` measured the wrong revision, silently.** `tree` was hardcoded to `tree-base`, so `./build.sh cand <sha>` followed by `./build-extras.sh` built `decrypt_bounds` from whatever `base` last held and printed only `built build/decrypt_bounds`. That is precisely the failure the T-013 entry condition exists to prevent, reintroduced inside the tool written to prevent it: a candidate where upstream fixed `Utils::decrypt` would be recorded as still broken. It now takes the tag, and `build.sh` writes the resolved SHA to `build/tree-<tag>/.revision` so both binaries can say what they speak for. **`path_arith` printed the same answer for every revision, including one where upstream had fixed the line.** It hand-copied `MAX_PACKET_PAYLOAD`, `MAX_PATH_SIZE` and `isValidPathLen`, and `build-extras.sh` compiled nothing from the tree — yet `TASKS.md`, the harness README and §4 all enrolled it as a revision check, which it could not be. The constants and the validator now come from the built tree (`Packet.cpp` is linked and `mesh::Packet::isValidPathLen` called). The eight lines of index arithmetic genuinely cannot be executed on a host — they sit behind `MACThenDecrypt`, so behind AES, SHA-256 and ed25519 — so instead of pretending, the `PAYLOAD_TYPE_PATH` branch is extracted between two anchors, whitespace-normalised, hashed and compared. A revision that has touched those lines is refused with exit 65 and told to re-read P3 by hand. `#3269`'s head is such a revision, so the check has a one-command demonstration rather than an argument: `./build-extras.sh pr3269` refuses, naming both digests. **The P4 publication question is the owner's, and it is narrower than it read.** The document said reporting upstream was their call, which is right, but left it sounding as if the disclosure decision were open. It is not: this repository is public, and opening #160 published the affected range, the faulting line and a one-command reproduction. What remains is an *ordering* decision — notify first, or accept publication-without-notice deliberately — and §5 now says so. Minors from the same review: the P3 results fence is the tool's real output instead of a paraphrase wearing its clothes; the `mmap`/`mprotect` in `decrypt_bounds.cpp` are checked, because an unchecked one faults in a way that looks like the finding and is not it; the References line said M15–M17 where four were filed. Re-run from the committed copy after the changes, all four trees: §4 reproduces exactly. `#3269`'s ten rows are byte-identical to `base`, which is the expected result and now a measured one — the report said its diff was a log line, and that was a reading of a diff rather than a run. Both say it now. Also re-checked upstream, 2026-08-24: all five pull request states, heads and bases unchanged; `main` has moved two commits ahead of the pin and both are `docs/faq.md`, so the pin is no longer the literal tip and is still upstream's newest code and newest release. The two halves are recorded separately because only one of them ages. ## The owner's upstream delta, verified rather than repeated Meshtastic `firmware#11573`, merged `ac330e6a` on 2026-08-23, replaces an `assert()` on a wire-supplied payload length with an executable rejection that releases the packet and unwinds the transmit state, and adds a unit test that asserts the rejection — return value, no half-started transmit, and the pool slot reusable. Every claim in the owner's summary was re-read from the merged diff and held; the metadata was taken from the API, not from the summary. It is **GPL-3.0** and it is **not in any release** — `master` does not contain the merge commit — so nothing is copied and nothing is called fixed downstream. What it changes for us is the weight of P4: an unchecked wire length reaching a fixed destination is now a two-instance pattern across unrelated firmwares rather than one project's defect. So the invariant and the test shape are written in our own words as entry conditions on T-013 and T-050, and the ledger carries the row with `ADAPT the invariant, IGNORE the code`. Deliberately not claimed: whether that `assert()` was compiled out in shipping builds. `NDEBUG` is in three of that repository's files and in none of its build flags, and the toolchain's own definition was not traced. The narrower claim stands without it — an `assert` is a statement about a program's own consistency, and a length that arrived from outside is not that. **NOT EXECUTED — HARDWARE REQUIRED** for everything physical, on both halves. Tests: `cmake && ctest` 33/33. `check_docs.py` clean. `shellcheck` clean on all three scripts. No Attadipa code changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`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>
|
Could not determine the active pull-request count; do not assume capacity. |
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. |
# Conflicts: # TASKS.md # docs/research/WAVESHARE_ARRIVAL.md
Fixes #142
Research only. No Attadipa code changed and none should have — this
repository links no MeshCore and has no radio stack. Documentation, plus the
harness that was already merged.
Problem
Three things, and only the first was the plan.
#160 landed in two pieces and
maincurrently holds the smaller one.a7624b0merged it as 14 of its 19 files — the report and the wholedocs/research/meshcore-parser-bounds/harness — and leftSTATUS.md,TASKS.md,VERIFIED_FACTS.md,OPEN_QUESTIONS.mdandREUSE_LEDGER.mdbehind. Nothing that landed is wrong. What is missing is the index around it:
CLAUDE.mdsends the next agent toSTATUS.mdfor "where things actuallyare", and today it says this research does not exist, while the document's own
References line sends a reader to
OPEN_QUESTIONSM15–M17 — which onmainbelong to the frame-capacity work, not to this. A green partial merge looks
exactly like a whole one from the commit list.
The independent review of #160 left one blocking finding and two real code
findings, and the blocking one got worse while the branch sat: it stamped this
work
T-127, whichmainnow uses twice (TASKS.md:2935the anchor check,:3406the frame capacity), andSTATUS.mdcalled itT-051, which is thenot-started P1 MIA-M10Q GNSS research. The
M15–M18open questionscollided the same way. Findings 3 and 4 are the harness quietly measuring the
wrong revision — described below, because they are the ones with teeth.
The owner posted an upstream delta on 2026-08-24 — Meshtastic
firmware#11573 — asking for
the invariant rather than the code.
Solution
The identifiers
T-173 and M20–M23, the first free numbers, with a note in each file
saying what they were filed as. CI could not have caught the original:
check_docs.pychecks duplicate IDs withinTASKS.md, so an ID consistentthere and wrong in
STATUS.mdpasses every check. Also merged the twoVERIFIED_FACTS.mdentries that both said our MeshCore pin is upstream's tip —written hours apart by two research runs from different API calls.
The harness could not check the revision it was pointed at
Both of these are enrolled as revision checks by the T-013 entry condition,
which is exactly when nobody will re-read the harness.
build-extras.shhardcodedtree-base../build.sh cand <sha>followedby
./build-extras.shbuiltdecrypt_boundsfrom whateverbaselast heldand printed only
built build/decrypt_bounds. A candidate where upstream hadfixed
Utils::decryptwould be recorded as still broken, silently. It nowtakes the tag, and
build.shwrites the resolved SHA tobuild/tree-<tag>/.revisionso both binaries can print what they speak for.path_arithprinted the same answer for every revision, including onewhere upstream had fixed
Mesh.cpp:172— it hand-copiedMAX_PACKET_PAYLOAD,MAX_PATH_SIZEandisValidPathLenand compiled nothing from the tree. Theconstants and the validator now come from the built tree (
Packet.cpplinked,mesh::Packet::isValidPathLencalled). The eight lines of index arithmeticgenuinely cannot run on a host — they sit behind
MACThenDecrypt, so behindAES, SHA-256 and ed25519 — so rather than pretend, the
PAYLOAD_TYPE_PATHbranch is extracted between two anchors, whitespace-normalised, hashed and
compared against the pin. A revision that has touched those lines is refused
with exit 65 and told to re-read P3 by hand.
#3269's head is such a revision, so this has a one-command demonstrationrather than an argument:
P4's publication question is narrower than it read, and it is the owner's.
The document said reporting upstream was their call — right — but left the whole
disclosure decision sounding open. It is not: this repository is public, and
opening #160 published the affected range, the faulting line and a one-command
reproduction. What remains is an ordering decision, and §5 now says so.
Minors from the same review: §3 P3's results fence is the tool's real output
instead of a paraphrase wearing its clothes;
mmap/mprotectare checked indecrypt_bounds.cpp, because an unchecked one faults in a way that looks likethe finding and is not it; References said M15–M17 where four were filed.
§8 — a second ecosystem, verified rather than repeated
Meshtastic
firmware#11573, mergedac330e6aon 2026-08-23. Every claim inthe owner's summary was re-read from the merged diff and held; the metadata
came from the API rather than the summary. It replaces
assert(p->encrypted.size <= sizeof(radioBuffer.payload))with an executablerejection that releases the packet and returns 0 before the
memcpy, makesstartSendunwind on that zero, restores the radio's home configuration evenwhen there was no packet, releases the beacon packet on
ERRNO_SHOULD_RELEASE,and adds a test asserting the rejection — return value, no half-started
transmit, and the pool slot reusable. That third assertion is the one worth
copying: "it rejected" and "it rejected without losing the buffer" are different
claims.
GPL-3.0, and not in any release —
compare/master...ac330e6aanswersdiverged, and the newest release predates it by two months, so the shippingfirmware still has the assertion. Nothing is copied and nothing downstream is
called fixed. What changes is the weight of P4: an unchecked wire length reaching
a fixed destination is now a two-instance pattern across unrelated firmwares
rather than one project's defect. The invariant and the test shape are written in
our own words as entry conditions on T-013 and T-050.
Deliberately not claimed: whether that
assert()was compiled out inshipping builds.
NDEBUGis in three of that repository's files and in none ofits build flags, and the toolchain's own definition was not traced. The narrower
claim stands without it.
Upstream re-checked, 2026-08-24
All five pull request states, heads and bases unchanged; the three open ones
last touched 2026-08-22.
mainhas moved two commits ahead of the pin andboth are
docs/faq.md, so the pin is no longer the literal tip and is stillupstream's newest code and newest release. Recorded as two separate
sentences, because only one of them ages.
Upstream and reuse
meshcore-dev/MeshCore, MIT, pinnedd929643…. Nothing vendored; allthree pull requests are still open, unreleased, and two do not close their own
findings.
meshtastic/firmware, GPL-3.0,ac330e6a— read-only evidence, never asource of code. The ledger row says so and explains why a GPL row sits in a
table of otherwise-takeable candidates.
Tests
cmake -S . -B build && cmake --build build && ctest --test-dir build --output-on-failure— 33/33 pass. This branch touches no code, so that is"nothing regressed", not evidence for any claim here. (
python3-pilmust beinstalled or the build registers a deliberately failing placeholder.)
python3 tools/docs/check_docs.py— clean. It caught two citations my insertspushed off their lines; both are re-pointed at the facts they name.
shellcheck— clean on all three scripts.changes: §4 reproduces exactly.
#3269's ten rows are byte-identical tobase, which is expected and now measured — the report said its diff was a logline, which was a reading of a diff rather than a run.
./build-extras.sh pr3269refuses with exit 65, as above.sha256sumper file against0ac1615, confirmingmain's copy of each is byte-identical to what the fixeswere applied to — a strict superset, not a discarded change.
Hardware
NOT EXECUTED — HARDWARE REQUIRED. Nothing ran on a radio, a node or any
board, on either half. No host sanitizer result may be presented as radio or HIL
validation, and the four boards Meshtastic's author lists were not touched here.
Risks
the row says why in its own text rather than relying on the licence column.
point — but it means a future agent meets a red exit 65 and has to read the
message rather than assume the tool is broken. Said in the message itself, in
the README and in the T-013 entry condition.
reproduction is now permanently in
main.Remaining blockers
None blocking. M20–M23 in
OPEN_QUESTIONS.mdare what this could notestablish, and none of them blocks anything while Attadipa compiles no MeshCore.
needs-ownerstands for one decision only: whether upstream is told about P4before or after the fact.
🤖 Generated with Claude Code