fix: audit conformance pass — LDF frame validation, E2E masquerade detection - #76
Merged
Conversation
…tection Gap-audit findings, most-severe first: - ldf.parseFrameHeader now rejects a frame whose ID falls outside 0x00-0x3F or whose length falls outside 0-8 bytes, matching LIN Specification Package 2.2A §2.3.1 (6-bit frame ID) and this package's own LINMaxDataLen constant. Previously a negative length (e.g. from `f: 0x10, MASTER, -4;`) reached DB.Encode's `make([]byte, f.Length)` and panicked with `runtime error: makeslice: len out of range` (confirmed by reproduction against the pre-fix code — see PR body), and an out-of-range ID (e.g. 300) was silently truncated via a bare uint8() cast, corrupting the frame table entry at the truncated ID (CWE-20/CWE-789). Fixing this exposed a second latent bug: a rejected frame's single `continue` didn't skip its body, so its own closing brace was mistaken for the Frames section's closing brace, silently dropping every frame after it — fixed by skipping to the matching closing brace before continuing. Signal-ref bit offsets are now rejected when negative (previously the parse error was discarded). - safety.Receiver.Unwrap now compares the wire-transmitted DataID/ SourceID against the receiver's configured Config and returns a new ErrIDMismatch on mismatch. Per the AUTOSAR E2E Protocol Specification, the DataID is included in the CRC computation specifically to provide masquerade protection, but Unwrap only ever recomputed the CRC over the transmitted bytes themselves — proving self-consistency, not that the frame belonged to the receiver's expected stream. The misleading `_ = dataID // validated implicitly via CRC` comment (which claimed a check that didn't happen) is removed. - virtual.Bus.Publish/PublishClassic now reject a non-nil, zero-length payload the same way they already reject an over-length one, so the in-process virtual bus can never broadcast a 0-data-byte frame that lin.ValidateFrame itself would consider malformed (LIN Specification Package 2.2A: the data field carries 1-8 bytes). PublishClassic also gained the LINMaxDataLen over-length guard Publish already had. - All GitHub Actions in .github/workflows/ are now pinned to immutable commit SHAs (with a `# vX` comment) instead of mutable tags, matching the repo's own SLSA/supply-chain evidence posture. Regression tests added for every fix (verified to fail against the pre-fix code, see PR body); go build/vet/test/test -race and the FuzzParse/FuzzProtectUnwrap/FuzzSendHeader short fuzz runs all pass. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
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.
Summary
Implements the go-LIN gap-audit worklist (audit_20260730-1552), most-severe first. Base commit audited:
7b45f6457cba4e6f1774a9b5047f029627b2e908(origin/main, unchanged at time of this PR).go-LIN-01 — LDF frame ID/length validation (CWE-20/CWE-789)
Spec citation: LIN Specification Package 2.2A §2.3.1 — protected identifier = 6-bit frame ID (ID0–ID5) + 2 parity bits, so raw frame ID MUST be 0x00–0x3F; LIN data field is 1–8 bytes (matches this package's own
LINMaxDataLen).parseFrameHeaderdiscarded the length-parse error and never range-checked ID or length. Reproduced the exact panic against pre-fix code:(
Frames { BadFrame: 0x10, MasterNode, -4 { EngineSpeed, 0; } }→db.Encode(0x10, nil)→make([]byte, f.Length)withf.Length == -4.) An out-of-range ID (e.g.300) was silently truncated viauint8(id)(300 → 44/0x2C), corrupting whichever legitimate frame lived at the truncated ID.Fix:
parseFrameHeadernow rejects (returns an error, causing the frame to be skipped) any ID outside0..lin.LINMaxIDor length outside0..lin.LINMaxDataLen, and no longer discards the length-parse error.Additional fix surfaced by this change: once invalid frames are rejected instead of silently corrupted, a pre-existing bug in
parseFramesbecame load-bearing: on a header-parse error it didcontinuewithout skipping the frame's body, so the next peeked line (the frame's own signal-ref lines) fell through to the "skip one line" branch — and the frame's own closing}was mistaken for theFramessection's closing}, silently truncating the whole section and dropping every subsequent frame. Fixed by skipping forward to the matching closing brace before continuing. Verified:Also fixed the sibling gap (go-LIN-A3, not spec-mandated but same CWE-20 class): signal-ref bit offsets are now rejected when negative instead of relying on incidental Go shift/comparison semantics.
Regression tests (
ldf/parser_test.go), each independently verified to fail against pre-fixparser.go:TestParse_rejectsNegativeFrameLength— asserts the panic-triggering frame is never stored, andEncodeon the rejected ID returns nil instead of panickingTestParse_rejectsOutOfRangeFrameID— asserts ID 300 is not stored under its truncated 0x2C keyTestParse_badFrameDoesNotSwallowLaterFrames— asserts a good frame after a bad one still parsesTestParse_rejectsNegativeSignalBitOffsetFuzzParsestrengthened to calldb.Encode(id, nil)for every frame ID after every parse (the existing fuzz target never calledEncode, so it could not have caught the original panic) — 200,000 iterations cleango-LIN-02 — E2E masquerade detection (AUTOSAR E2E)
Spec citation: AUTOSAR E2E Protocol Specification — DataID is included in the CRC specifically for masquerade protection; a frame protected under a different DataID/SourceID must fail validation, not just produce a self-consistent CRC. Matches this repo's own
HARA.mdH-06/SG-04 andREQ-SAFETY-001/002.Receiver.UnwrapstoredConfig{DataID, SourceID}but never compared it against the wire values — the comment_ = dataID // validated implicitly via CRCwas false. Fix: after CRC success, compare wireDataID/SourceIDagainstr.cfgand return a newErrIDMismatchon mismatch; removed the misleading comment.Regression tests (
safety/e2e_test.go):TestUnwrap_rejectsDataIDMismatch,TestUnwrap_rejectsSourceIDMismatch— both fail to compile against pre-fixe2e.go(ErrIDMismatchdidn't exist), confirming the symbol and behavior are new.go-LIN-A2 — virtual bus empty-payload rejection
Spec citation: LIN Specification Package 2.2A — data field is 1–8 bytes; matches this repo's own
lin.ValidateFrame("data must not be empty").Publish/PublishClassicaccepted[]byte{}(non-nil, zero-length) and later broadcast it viaSendHeaderas a 0-byte framelin.ValidateFramewould itself reject. Fixed both to reject non-nil empty data the same way they reject over-length data;PublishClassicalso gained theLINMaxDataLenguardPublishalready had (it had none before).Regression tests (
virtual/bus_test.go):TestPublish_rejectsEmptyPayload,TestPublishClassic_rejectsEmptyPayload— both fail against pre-fixbus.go.go-LIN-A3 — negative bit offset
Folded into the go-LIN-01 fix above (
ldf/parser.go,parseFrames), since both stem from the same discarded-error pattern.go-LIN-A1 — Action SHA pinning
All
uses:lines in.github/workflows/{ci,dco,docker,release}.ymlnow pin to full commit SHAs (resolved viagh api repos/<owner>/<repo>/commits/<tag>) with a# vXcomment, matching the repo's own SLSA/supply-chain evidence posture. YAML re-validated withyaml.safe_loadafter the rewrite.Verification
go build ./...,go vet ./...— cleango test -race -count=1 ./...— all packages passldf90.4%,safety100%,virtual94.5% (all above floor)FuzzParse(ldf),FuzzProtectUnwrap(safety),FuzzSendHeader(virtual) — all cleangofusaCLI is not available in this environment; CI'sgofusajob (v0.48.0, ERROR-gating + 100% req/sec-tested coverage) will be the first real check. No newfusa:reqIDs were introduced — new tests are tagged against existing requirement IDs (REQ-LDF-005,REQ-LDF-006,REQ-SEC-001,REQ-SAFETY-001/002,REQ-SEC-002) that already describe the fixed behavior at the appropriate level of abstraction, consistent with how PR fix: audit pass 2 — diagnostic-frame checksum + 8 more findings #72's fix-pass didn't touch.fusa-reqs.jsoneither.Not merging — leaving for review/CI per repo convention.