Skip to content

fix: adversarial audit findings (#62–#83) - #86

Merged
Teakowa merged 24 commits into
mainfrom
fix/audit-findings
Oct 15, 2026
Merged

Teakowa merged 24 commits into
mainfrom
fix/audit-findings

Conversation

@e54-bot

@e54-bot e54-bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes all verified findings from the full-repo adversarial audit (tracking issue #84). Each fix was developed red-green: a failing regression test was added and observed failing, then the smallest targeted change was made and re-verified.

Mock provider crashes (H1–H5 → #62–#66)

  • Lexer now emits Unknown(char) tokens and syntax diagnostics for unrecognized input instead of panicking on non-ASCII bytes or treating them as identifiers.
  • SourceText no longer inverts the byte range of the phantom line after a trailing newline.
  • EOF-truncated documents ("puzzle", "puzzle p") now produce a diagnostic and artifact: null instead of a silent None that panicked in lpp/compile.
  • Failed arithmetic simulation now reports an error diagnostic instead of panicking.
  • stdin framing reads raw bytes: non-UTF-8 input answers -32700 (null id) and the session continues, instead of aborting the process.

Ordering & semantics (#68–#74)

  • Diagnostics sorted by range.start (§9.2); rename textEdits sorted by range (§15.2) while references keep the declaration-first carve-out.
  • position_of excludes CRLF terminators from character counts.
  • references/rename resolve by symbol kind, so a puzzle and an operation sharing a name no longer conflate.
  • rename now refuses with invalidDocument when any received document has errors (§15.2 atomic coverage).
  • Malformed operation declarations resync at the line boundary instead of cascading into the next line.
  • Out-of-range integer literals produce a diagnostic instead of silently becoming 0; i64::MIN still parses.

Project loading, reconstruct, transport (#75, #76, #79)

  • Source enumeration skips directories (without following symlinks), propagates read_dir entry errors, and reports details.uri on canonicalization failure.
  • reconstruct rejects non-identifier names instead of emitting unparseable source.
  • Request validation enforces string/integer ids (-32600 otherwise) and required object params (-32602).

Runner (#77, #78, #82)

  • Providers are killed and reaped on response mismatch, unparseable output, and disconnected stdout; the stdout reader thread is detached after child exit so a descendant holding the pipe can no longer hang the runner.
  • Session drains trailing provider output (previously undetected extra lines), applies ${PROJECT_URI} substitution to rawLine steps, and the CLI rejects flag-like values and .json directories.

Spec & docs (#67, #80, #81, #83)

  • Refusal details pinned to spec-owned echo keys (uri, format, newName, within) matching fixture assertions.
  • Version-gating, ordering, field-handling, and validation-order ambiguities pinned; DocumentSet key vs inner uri semantics, whitespace-only lines, post-shutdown behavior, and equal-start edit ordering clarified.
  • ADR README link text and contradictory issue reference corrected; CHANGELOG 1.7.0 date fixed to 2026-10-11.

Test plan

  • cargo fmt --all --check clean
  • cargo clippy --all-targets -- -D warnings clean
  • cargo test --workspace — 61 tests pass (new regression coverage for every code fix)
  • Release conformance: 67 passed, 0 failed against rebuilt mock provider
  • git diff --check clean

The lexer emitted an identifier-shaped token for every byte outside the grammar, so 'puzzle @ {}' checked clean, and multi-byte characters produced tokens ending mid-character which panicked position_of's str slicing. Lex unknown input as TokenKind::Unknown, consuming a whole char so spans stay on UTF-8 boundaries.

Fixes #62
line_byte_range stripped a trailing newline unconditionally, so the empty phantom line after a document-final newline got start > end and any positioned method (definition/references/rename/validateEdits) panicked on a position that spec section 6.4 declares valid. Only strip a terminator that lies inside the line's own span.

Fixes #63
…ning a silent no-puzzle

parse_top returned None without a diagnostic when input ended right after 'puzzle' or 'puzzle <name>', breaking compile's 'no errors implies a puzzle' invariant (panic) and making check report truncated files as clean. Emit an unexpected-end-of-input diagnostic at the end of the token stream.

Fixes #64
Op simulation failure (division by zero, i64 overflow) produced no diagnostic, so compile's 'no errors implies resolvable ops' invariant broke and the provider panicked. Emit x-demo/simulation-failed at the solution range so check surfaces it and compile returns diagnostics instead of crashing.

Fixes #65
…cking

read_line into a String fails on non-UTF-8 bytes and the panic killed the session with no response; read_until keeps the bytes, answers Parse error for an undecodable line, and continues. Stdout write/flush failures now exit quietly instead of panicking on EPIPE.

Fixes #66
Diagnostics were emitted in analysis-phase order (body errors, then missing-section at the puzzle-name range, then semantic checks), violating the spec section 9.2 sort mandate whenever a later-position diagnostic was pushed first. Stable-sort by range.start in parse_document.

Fixes #68
occurrences() lists the declaration first (a references carve-out in spec section 14.2), so a puzzle with 'solution' before 'ops' produced unsorted rename edits, violating section 15.2's sorted-edits mandate. Sort rename_ranges' op result; references keeps declaration-first.

Fixes #69
…counts

position_of counted the '\r' of a CRLF terminator as a character, so diagnostics on CRLF documents landed past the visible line length (invalid ranges per spec section 6.5). Clamp the counted span to the line's content end: bytes in the terminator map to end-of-line.

Fixes #70
references looked occurrences up by bare name, so a position on 'puzzle a' returned the occurrences of an unrelated op named 'a', and a puzzle name with no same-named op reported no declaration even with includeDeclaration. Resolve occurrences through the typed Symbol instead.

Fixes #71
The rename edits loop read non-position documents with .puzzle, so a document carrying error diagnostics still contributed edits while an unparseable one was silently skipped - a partial rename violating section 15.2's all-references-covered rule (e.g. only the first of two same-named ops was edited). Parse every received document with into_puzzle and answer invalidDocument/documentHasErrors instead.

Fixes #72
…dary

parse_op ignored expect_punct failures and expect_x/expect_number recovered twice, so one bad op line produced extra misattributed diagnostics and swallowed the next line's valid declaration, which then surfaced as a bogus unresolved-op. Return after the first reported error; remove the redundant recover_line calls.

Fixes #73
read_number mapped parse failure to 0, so 'target = 99999999999999999999' silently compiled as 0. Tokenize overflow as BadNumber and report 'integer literal out of range' in expect_number. Parsing the signed slice also keeps -9223372036854775808 representable.

Fixes #74
read_dir entry errors were dropped via filter_map, .xdl-named directories were read as source files, and a canonicalize failure reported no details.uri. Collect entries fallibly, skip directories (symlinks still resolve through canonicalization), and name the failing file in details.uri per spec 8.1.

Fixes #75
reconstruct_source wrote sheet names into generated source verbatim, so an artifact carrying names with spaces or newlines produced text the provider's own parser rejects. Validate every name against the identifier grammar and answer invalidArtifact instead.

Fixes #76
Response parse/mismatch failures returned without killing the provider, leaking the process. A stdout-closed provider now gets killed immediately too. And the final reader_thread.join() could block forever when a provider descendant inherits stdout; the reader is detached instead - it ends on EOF or a failed send and dies with the process.

Fixes #77
Fixes #78
A fractional id (1.5) was accepted and could even initialize the session; bool/array/object ids were misreported as notificationNotSupported; a missing params member silently became {}. Spec 4 requires integer/string ids and a REQUIRED params object: wrong-shaped ids now answer -32600 and missing/non-object params answer -32602.

Fixes #79
Four gaps: a provider could emit extra lines past the last step unobserved (bounded reader drain now reports them); ${PROJECT_URI} was not substituted inside rawLine steps; --provider --fixtures swallowed the next flag as a value; and a directory named *.json or a failed read_dir entry confused fixture discovery.

Fixes #82
Section 18.3 documented refusal details as { refusalCode, uri?, range? } while normative fixtures asserted format/newName/uri/within verbatim, so a provider implementing exactly the documented shape failed the suite. Refusal details are now a closed contract: refusalCode plus the echo keys each refusal's defining section requires - uri for noSymbolAtPosition, newName for rename name refusals, format for reconstruct.artifactFormatUnsupported, within for unknown lookup selectors. The mock already emits exactly these keys; no fixture changes needed.

Fixes #67
kind is defined since LPP 1.1 ('file' or omitted) with 'directory' added at 1.2; supportedProtocolVersions lists exactly the supported set; a version-gated entry field before 1.1 is -32602 (capabilityUnavailable when negotiated but unadvertised); invalidEntry covers an invalid version.

Fixes #80
Clarifications matching mock + fixture behavior: document-set results order by map key (check/compile/symbols/rename); references' declaration-first carve-out and invalidPosition on all position-taking methods; overlappingEdits reports the original-order index of the later-in-document edit with a stable sort; whitespace-only lines are -32700; the full request validation pipeline is pinned; shutdown receives no further responses; languages is a REQUIRED non-empty array of exact {id, extensions} entries; invalidLanguage covers top-level languageId params; and a spec-defined field sent in an inapplicable context is -32602, not ignorable.

Fixes #81
The ADR README linked a nonexistent spec/lpp-v1.md text and cited both 'Issue #10' and 'PR #10' for one object (GitHub shares the numbering; #10 is the PR). CHANGELOG dated 1.7.0 2026-10-10 while the release commit and ADR-0006 decision are both 2026-10-11.

Fixes #83
Two races only surfaced under Linux CI scheduling: both reaped-provider
tests shared one scratch dir (parallel cleanup deleted the pidfile), and
scripts that exit without reading stdin raced the request write to EPIPE.
This was referenced Oct 15, 2026
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