Repository navigation
Make the .ashpkg read ceiling a ceiling, on every path that reads one - #491
Merged
Merged
Conversation
…--format-json) Checkpoint before addressing the adversarial review's blockers: the byte ceiling is bypassable by non-regular files, and the new e2e fixture uses `truncate`, which macOS does not ship. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 16 MiB guard was applied to two of four read paths, and on those two it was not a guard. FileInfo.Length is metadata: it reports 0 for a FIFO, a socket and a character device, and for a SYMLINK it reports the length of the target PATH STRING — 7, 9, 22 bytes. So `ln -s /dev/zero x.ashpkg` dropped into a synced mesh store walked past the check and OOMed the unbounded read, aborting a whole pull pass so one planted file denied every legitimate package behind it; `mkfifo x.ashpkg` hung the pull forever. Both are one command, and neither needs a large file to be synced at all. SafePackageRead replaces the check with five gates that never trust metadata: refuse a directory or a symlink without reading Length; open non-blocking (O_NONBLOCK on Unix, \?\ plus a device-namespace refusal on Windows) so a FIFO cannot block the guard itself; refuse anything not seekable; treat the declared length as advisory only; and cap the read at maxBytes+1 whatever the metadata claimed. The cap is not belt-and-braces: /proc/self/mem is a regular file with st_size 0 that yields unbounded bytes. All six read paths route through it — import, show, publish, pull, the auto-pull daemon, and the mesh SERVE endpoint. The serve path was the fifth round's find: it still gated on FileInfo.Length, so the index advertised symlinks at 23 bytes and then served 41,943,040; `ln -s /etc/passwd x.ashpkg` returned /etc/passwd to any peer; and 105 concurrent GETs of one planted FIFO stopped the node answering /hello at all (issue #488). Refusal text is escaped and bounded. ExtensionPackaging quotes the sender's formatVersion — read before anything is verified — into a message printed on all four verbs, so a CR could overwrite the refusal row with a counterfeit ADMITTED line carrying a sealer fingerprint, the one part of a row the code says a sender cannot choose. Also here: `pull --from file://` is no longer mangled into a nonsense path; progress moved off stdout so --format-json parses; a command that cannot honour --format-json now says so instead of ignoring it; a failing row is refused and the pass continues; and two CI filters that matched zero tests (green by vacuity) now run the suite — behind step conditions that a pull_request actually selects, which was the second half of that bug. cert-gate 402/402. e2e-loop 143/143. Ashlar.Tests.CLI 207/0. Five adversarial rounds shaped this. Each found a defect the implementer and the verifier had both called clean: the ceiling did not hold; the "Unix-only" e2e block ran on Windows, where MSYS turns mkfifo into a .lnk; SubmitAsync sat outside the per-row try; the escaping covered two of four sinks while the CHANGELOG said four; and the serve path was a sixth reader nobody had counted. Independent skeptics then killed 8 of 23 findings in one round and every HIGH in the next, which is the only reason the rest are worth acting on. Filed rather than fixed: #483 (pre-existing unescaped sites on the success paths), #484, #485, #486, #487, #488, #489, #490. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cursor
Bot
force-pushed
the
claude/fix-round3-cli
branch
from
September 5, 2026 01:02
b65a8e1 to
050ac52
Compare
Contributor
|
Rebased onto current Local check after rebase: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 16 MiB
.ashpkgceiling was applied to two of four read paths — and on those two it was not a ceiling.FileInfo.Lengthis metadata. It reports 0 for a FIFO, a socket and a character device, and for a symlink it reports the length of the target path string — 7, 9, 22 bytes. So a symlink to any honest 400 MB package walked straight past the guard, no device files required. Dropped into a synced mesh store,ln -s /dev/zero x.ashpkgOOMed the unbounded read and aborted a wholepullpass, so one planted file denied every legitimate package behind it;mkfifo x.ashpkghung the pull forever. Each is one command, and neither needs a large file to be synced at all.SafePackageRead— five gates, none of which trust metadataLinkTarget.Lengthis never readO_RDONLY|O_NONBLOCK|O_CLOEXECon Unix;\?\plus a device-namespace refusal on WindowsFILE_TYPE_DISKon WindowsmaxBytes + 1, refuse the moment the total crosses itG4 is not belt-and-braces.
/proc/self/memis a regular file withst_size0 that yields unbounded bytes, so a length check can never be the bound.An empirical probe settled the design rather than guesswork: .NET 10 exposes no
st_modetype bits anywhere —File.GetAttributes,UnixFileModeand evenFileSystemEntry(thereaddirpath, whered_typeis available to the runtime) all collapse FIFO, socket and chardev toNormal+ length 0. And the obvious remediation, "open it and then check", blocks forever on a FIFO — turning the hang insideFile.ReadAllTextAsyncinto a hang inside the guard.All six read paths
import,show,publish,pull, the auto-pull daemon, and the mesh serve endpoint. That last one was the fifth round's find — it still gated onFileInfo.Length, so:/mesh/v1/indexadvertised a symlink at 23 bytes and/mesh/v1/pkg/then served 41,943,040ln -s /etc/passwd x.ashpkgreturned/etc/passwdto any peer that reached the port/helloat allTracked as #488, fixed here.
Refusal text is escaped and bounded
ExtensionPackaginginterpolates the sender'sformatVersion— read before anything is verified — into a message printed by all four verbs. A CR in it overwrites the refusal row with a counterfeit✓ ADMITTED … sealed by <fingerprint>, i.e. the one part of a row the surrounding comment says a sender cannot choose.This one is worth dwelling on: an earlier round's skeptics killed "
result.Messageis an injection sink" 3/3, correctly, by testing the malformed-JSON route — whereSystem.Text.Jsonhex-escapes control bytes. The real route sets a validformatVersion, parses cleanly, bypasses that entirely, and reaches the same sink. Every fact they checked was true and the conclusion still didn't follow.Also here
pull --from file://…no longer mangled into a nonsense path--format-jsonoutput parses--format-jsonsays so rather than ignoring itSubmitAsyncthrow, which sat outside the per-rowtrypull_requestactually selects, which was the second half of that bugVerification
cert-gate404/404 ·e2e-loop.sh143/143 ·Ashlar.Tests.CLI207/0 · tier-C filter 61/0Every attack above was reproduced live against the built binary and then re-run after the fix. New tests were checked to fail against the unfixed code, not merely pass against the fixed one.
How this was reviewed
Five adversarial rounds. Each found a defect the implementer and the verifier had already called clean:
mkfifointo a.lnkandln -sinto a 32-byte!<symlink>fileSubmitAsyncsat outside the per-rowtryIndependent skeptics then adjudicated the findings — killing 8 of 23 in one round and every HIGH in the next. That filter is why the survivors were worth acting on; without it this PR would have chased several confident, reproduced, wrong claims.
Filed rather than fixed
#483 (pre-existing unescaped sites on the success paths), #484, #485, #486, #487, #488, #489, #490.
🤖 Generated with Claude Code