fix: undo byte-fidelity, undo path guard, and result-cap bypasses - #137
Merged
dynamics365ninja merged 1 commit intoAug 5, 2026
Merged
Conversation
Eight defects found during a full-repo audit. Each is covered by a test. undo lost the UTF-8 BOM. The journal promises "the exact pre-image bytes", but PreImage is stored as a decoded string and the decoder swallows the BOM, so AtomicWriteText rewrote AOT XML three bytes short. D365FO and Visual Studio both write these files WITH a BOM, so every undo produced a spurious git diff. JournalEntry gains an optional PreImageHadBom flag; entries written by older builds (null) fall back to the BOM state of the file on disk. undo bypassed PathGuard. ReplayDisk deleted and rewrote files with no boundary check, unlike every other write path. The target path comes out of the journal store rather than the command line, so a journal carried over from a different D365FO_PACKAGES_PATH -- or hand-edited -- could reach anywhere on disk. A negative --limit disabled the row cap. SQLite reads a negative LIMIT as "no limit at all", turning a bounded lookup into a full-index dump: the exact token blow-up the caps exist to prevent. The MCP layer already guarded this; the CLI and Core did not. Adds ClampLimit across all 27 query methods plus a hard MaxRowLimit ceiling. BridgeClient desynchronised after a timeout. The abandoned ReadLine stayed parked on the pipe and consumed the late reply, handing it to the NEXT request and shifting every subsequent response by one; replies were never correlated by JSON-RPC id either. The child process is now discarded on timeout, and a mismatched id is a hard error. undo --steps 0 reverted one entry. Both Core and the CLI silently rounded 0 up to 1. Undo is destructive, so a caller asking for no steps now gets INVALID_ARGS rather than an unrequested revert. Also: escape LIKE wildcards in FindFormsByPattern (--pattern "Simple_List" matched SimpleXList); tighten grounding-token object binding from a bidirectional substring match -- which let a token for "Cust" authorize writes to anything containing "Cust" -- to a prefix match that still allows derived names like CustTable_MyExt_Extension; drop 3x CS1573 and a doc comment claiming an OpenReadOnly fallback that does not exist. Build is warning-free; 657 tests pass (648 existing + 9 new). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dynamics365ninja
deleted the
fix/audit-2026-08-guardrails-and-undo-fidelity
branch
August 5, 2026 15:50
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.
Full-repo audit of the CLI, Core, MCP, and Bridge projects. Eight defects, each with a regression test. Build is warning-free; 657 tests pass (648 existing + 9 new).
Bugs fixed
undodid not restore the exact bytes it promisesThe journal's contract is "restoring the exact pre-image bytes", but
PreImageis stored as a decoded string and the decoder swallows the UTF-8 BOM.AtomicWriteTextthen rewrote the file without one. Reproduced end-to-end:D365FO and Visual Studio both write AOT XML with a BOM, so every
undoleft a spurious change ingit diff.JournalEntrygains an optionalPreImageHadBomflag — backward compatible, since entries from older builds deserialize tonulland fall back to the BOM state of the file on disk. After the fix the restored file is byte-identical to the original.undobypassedPathGuardReplayDiskcalledFile.Deleteand wrote files with no boundary check, unlike every other write path in the codebase. The target path comes out of the journal store rather than the current command line, so a journal carried over from a differentD365FO_PACKAGES_PATH— or hand-edited — could reach anywhere on disk.A negative
--limitdisabled the row capSQLite reads a negative
LIMITas "no limit at all", so--limit -1turned a bounded lookup into a full-index dump — the exact token blow-up the result caps exist to prevent. The MCP layer already guarded this (limit <= 0 ? 50); the CLI and Core did not. AddsClampLimitacross all 27 query methods, plus a hardMaxRowLimit = 1000ceiling.BridgeClientdesynchronised after a timeoutOn timeout the abandoned
ReadLinestayed parked on the pipe and consumed the bridge's late reply — which was then handed to the next request, shifting every subsequent response by one. Replies were never correlated by JSON-RPCideither. The child process is now discarded on timeout so the next call starts clean, and a mismatchedidis a hard error rather than silently misattributed data.undo --steps 0reverted an entryBoth
UndoEngine(Math.Max(1, steps)) and the CLI (settings.Steps <= 0 ? 1) silently rounded 0 up to 1. Undo is destructive, so a caller asking for no steps now getsINVALID_ARGSinstead of an unrequested revert.Three smaller ones
FindFormsByPattern—--pattern "Simple_List"also matchedSimpleXList.Custauthorized writes to anything containing "Cust", and a token forCustTableauthorized the unrelated shorterCust. Now a prefix match, which still allows derived names likeCustTable_MyExt_ExtensionandCustTableEventHandler.OpenReadOnlypromising anEnsureSchemafallback that does not exist in the code.Verification
dotnet build -c Releaseis clean, and the fixes were exercised against theMiniAotsample through the real CLI, not only through unit tests: BOM round-trip,--steps 0/-3,--limit -1, and--pattern "Simple_List".Deliberately not changed
Three findings alter operational behavior and are left as follow-ups:
HttpServerHostbinds0.0.0.0even with noAPI_KEY(log warning only). A loopback default would be safer but breaks Azure App Service deployments, which require0.0.0.0.CallDedupis a static cache shared across all HTTP clients — in a shared team deployment client B can be served client A's cached answer. Read-only metadata from the same index, so not a leak, but it is cross-session state.PathGuardalways allows the whole%TEMP%tree. Intentional for tests, but it widens the boundary for production writes too.🤖 Generated with Claude Code