fix(#150): add EvalSymlinks to validateDocmapPath — close dir-symlink bypass #152
Reference in New Issue
Block a user
Delete Branch "issue-150"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes #150.
Note:
TestMainSubprocess_InvalidDocMapPathandTestMainSubprocess_InvalidDocMapFilemoved to PR #151, which takes a better approach (tests via main review flow with early validation) rather than via the validate-docmap subcommand.Problem
validateDocmapPath(used by thevalidate-docmapsubcommand and Step 6c of the main flow) calledos.Lstat(absPath)afterfilepath.Abs.os.Lstatonly avoids following the final path component — intermediate directory symlinks are still resolved by the OS. This created a bypass:.review-bot/as a directory symlink pointing to a directory outside the repofilepath.Abs(".review-bot/doc-map.yml")→ textually inside repo ✅os.Lstat(absPath)follows the dir symlink, finds a regular file — no symlink bit on final component ✅filepath.Rel(root, absPath)passes because the textual path is inside root ✅Fix
Call
filepath.EvalSymlinksafterfilepath.Absto resolve all symlink components before the confinement check.filepath.Relthen operates on the fully-resolved path.Semantic change: file-level in-repo symlinks (target also within root) are now allowed. The security invariant is about where the resolved content lives, not whether the entry is a symlink.
Changes
cmd/review-bot/validatedocmap.govalidateDocmapPath: callfilepath.EvalSymlinks(absPath)beforeos.Lstatandfilepath.RelresolvedPath(symlink-free)cmd/review-bot/validatedocmap_test.goTestValidateDocmapPath_Symlink: updated to test out-of-repo symlink target (correctly rejected)TestValidateDocmapPath_DirSymlinkBypass: new test reproducing the dir-symlink attack vectorCHANGELOG.mdTest Results
Checklist
TestValidateDocmapPath_DirSymlinkBypassreproduces the attackgo vet ./...cleanscripts/check-deps.shcleanOriginal reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
aee0927c)Sonnet Review
Summary
This is a well-targeted security fix that correctly closes a directory-symlink bypass in path confinement. The implementation is correct, the tests are comprehensive and well-designed, and the code follows established Go patterns throughout.
Findings
cmd/review-bot/validatedocmap.gofilepath.EvalSymlinksalready guarantees the returned path is fully resolved with no symlinks. The comment correctly explains this, but the dead check (ModeSymlink can never be set on a fully-resolved path) adds noise. It's not incorrect, just permanently unreachable code that could mislead future readers into thinking the check provides real protection here.cmd/review-bot/main_test.gorepoDir := os.TempDir()in the subprocess branch uses the system temp dir directly rather than creating a unique temp dir. Since nonexistent-doc-map.yml is unlikely to exist there in practice this works, but it's slightly fragile — if some other test or process creates that file in /tmp, the test would unexpectedly passEvalSymlinksand fail the confinement check instead of the stat check. Usingt.TempDir()would be safer, buttis not accessible in subprocess mode. A comment explaining the constraint would help.cmd/review-bot/validatedocmap_test.goTestValidateDocmapPath_Symlinktest no longer covers the case of an in-repo symlink (one whose target is within the repo root), which is now the allowed case per the semantic change. A brief comment or a second sub-case showing that an in-repo symlink is accepted would make the changed behavior explicit and prevent a future developer from re-hardening against it unnecessarily.Recommendation
APPROVE — Approve. The security fix is correct: calling
filepath.EvalSymlinksbeforefilepath.Relcloses the directory-symlink bypass as described. The implementation is idiomatic Go, errors are properly wrapped with context, the test coverage is thorough (unit test for the exact attack vector, subprocess tests for the subcommand path, updated existing tests), and CI passes. The minor findings are cosmetic and do not affect correctness or security.Review by sonnet
Evaluated against
aee0927c@@ -1509,0 +1540,4 @@// same validateDocmapPath that is invoked when --doc-map is provided to the// main review-bot command.func TestMainSubprocess_InvalidDocMapFile(t *testing.T) {if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {[NIT]
repoDir := os.TempDir()in the subprocess branch uses the system temp dir directly rather than creating a unique temp dir. Since nonexistent-doc-map.yml is unlikely to exist there in practice this works, but it's slightly fragile — if some other test or process creates that file in /tmp, the test would unexpectedly passEvalSymlinksand fail the confinement check instead of the stat check. Usingt.TempDir()would be safer, buttis not accessible in subprocess mode. A comment explaining the constraint would help.@@ -42,2 +53,4 @@// ModeSymlink will never be set. We keep the check as defense-in-depth.fi, err := os.Lstat(resolvedPath)if err != nil {return fmt.Errorf("cannot stat file: %w", err)[MINOR] The Lstat + ModeSymlink check after EvalSymlinks is acknowledged as 'defense-in-depth' in the comment, but
filepath.EvalSymlinksalready guarantees the returned path is fully resolved with no symlinks. The comment correctly explains this, but the dead check (ModeSymlink can never be set on a fully-resolved path) adds noise. It's not incorrect, just permanently unreachable code that could mislead future readers into thinking the check provides real protection here.@@ -465,23 +465,34 @@ mappings:}[NIT] The updated
TestValidateDocmapPath_Symlinktest no longer covers the case of an in-repo symlink (one whose target is within the repo root), which is now the allowed case per the semantic change. A brief comment or a second sub-case showing that an in-repo symlink is accepted would make the changed behavior explicit and prevent a future developer from re-hardening against it unnecessarily.Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
aee0927c)Security Review
Summary
The changes correctly harden path validation by resolving all symlink components before confinement checks, closing the directory-symlink bypass. CI passed and tests comprehensively cover the attack scenarios and new behavior.
Findings
cmd/review-bot/validatedocmap.goRecommendation
APPROVE — The fix to call filepath.EvalSymlinks prior to path confinement is sound and closes the reported directory symlink bypass. Tests validate both traversal and symlink scenarios and CI has passed, so this PR should be approved. As a defense-in-depth improvement, consider having validateDocmapPath return the resolved (symlink-free) path and use that for reading the doc-map to eliminate any check-then-use race if symlink targets could change between validation and file access. Otherwise, the change improves security and maintains clear error handling.
Review by security
Evaluated against
aee0927c@@ -42,0 +46,4 @@// path is inside the root while the actual destination is not.resolvedPath, err := filepath.EvalSymlinks(absPath)if err != nil {return fmt.Errorf("cannot resolve path (symlink): %w", err)[MINOR] Allowing in-repo symlinks (after EvalSymlinks) introduces a small TOCTOU window: validation is performed on the resolved path but the subsequent read uses the original flag value elsewhere (ParseDocMapConfig). Consider passing the resolved path through to the read to avoid potential race conditions where a symlink target could be changed between validation and use.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
aee0927c)Gpt Review
Summary
The change to resolve symlinks with filepath.EvalSymlinks before confinement checks is correct and closes the directory-symlink bypass. Tests are updated and expanded appropriately, and CI has passed.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap_test.goRecommendation
APPROVE — The security fix is sound: resolving all symlink components before applying the repo-root confinement check prevents directory-symlink bypasses and aligns the behavior with the stated security intent. The expanded test suite covers the new semantics (including the directory-symlink case) and subprocess validation paths, and CI is passing. Please update the validateDocmapPath doc comment to match the new behavior (allowing in-repo symlinks by confining the resolved destination) and consider returning/using the resolved path for reading the docmap as defense-in-depth against TOCTOU. Optionally, add a platform guard or skip to the symlink test for environments without symlink support. With these minor improvements, the PR is ready to merge.
Review by gpt
Evaluated against
aee0927c@@ -37,22 +37,33 @@ func validateDocmapPath(localPath, resolvedRoot string) error {return fmt.Errorf("cannot resolve path: %w", err)[MINOR] validateDocmapPath’s doc comment still states that "The path is not a symlink" while the implementation now permits in-repo symlinks (by resolving them first and confining the resolved path). Update the comment to reflect the new invariant (the resolved destination must be within repo-root) and, if intended, explicitly state whether non-regular files are permitted since the code doesn’t enforce IsRegular.
@@ -46,3 +59,2 @@// Reject symlinks outright — a symlink can point to /dev/zero or arbitrary// host paths, bypassing both the confinement check and the size check.// Defense-in-depth: reject any remaining symlink indicator.if fi.Mode()&os.ModeSymlink != 0 {[MINOR] Defense-in-depth: validateDocmapPath resolves symlinks but the caller later reads the docmap using the original path string. Consider returning and using the resolvedPath for reading to minimize a theoretical TOCTOU risk if the symlink were altered between validation and open.
@@ -490,10 +501,10 @@ mappings:[]string{"--docmap", symlinkPath, "--repo-root", dir},[NIT] TestValidateDocmapPath_Symlink creates a symlink without a platform guard. On platforms like Windows this may require privileges and fail. Consider mirroring the skip behavior used in TestValidateDocmapPath_DirSymlinkBypass when os.Symlink returns an error.
aee0927cfbto4dce8e4454Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
4dce8e44)Sonnet Review
Summary
The fix correctly closes the directory-symlink bypass by inserting
filepath.EvalSymlinksbefore thefilepath.Relconfinement check. The implementation, test updates, and documentation are all well-reasoned and consistent with the repo's conventions.Findings
cmd/review-bot/validatedocmap.goos.Lstat+ModeSymlinkcheck afterfilepath.EvalSymlinksis documented as 'defense-in-depth', butfilepath.EvalSymlinksguarantees the returned path contains no symlinks — theModeSymlinkbit can never be set on the result. The check is harmless but the accompanying comment calling it 'defense-in-depth' is misleading because it can never fire. Consider either removing the dead check or replacing the comment with a more accurate note like '// EvalSymlinks guarantees this is unreachable; kept for belt-and-suspenders'.cmd/review-bot/validatedocmap_test.goTestValidateDocmapPath_Symlinktest correctly tests that an out-of-repo symlink target is rejected. However, the behavioral change introduced by this PR (in-repo symlinks are now allowed) is not covered by a positive test case. A test verifying that a file-level symlink whose target resides within the repo root is accepted would document and protect the new semantic. This is a test-coverage gap rather than a bug in production code.Recommendation
APPROVE — Approve. The security fix is correct:
filepath.EvalSymlinksresolves all intermediate symlink components before thefilepath.Relconfinement check, closing the directory-symlink bypass described in #150. The existing test is updated to reflect the changed semantics and a new regression test reproduces the exact attack vector. CI passes. The two nits (deadModeSymlinkcheck and missing positive in-repo-symlink test) are minor and do not affect correctness or security.Review by sonnet
Evaluated against
4dce8e44@@ -42,2 +53,4 @@// ModeSymlink will never be set. We keep the check as defense-in-depth.fi, err := os.Lstat(resolvedPath)if err != nil {return fmt.Errorf("cannot stat file: %w", err)[NIT] The
os.Lstat+ModeSymlinkcheck afterfilepath.EvalSymlinksis documented as 'defense-in-depth', butfilepath.EvalSymlinksguarantees the returned path contains no symlinks — theModeSymlinkbit can never be set on the result. The check is harmless but the accompanying comment calling it 'defense-in-depth' is misleading because it can never fire. Consider either removing the dead check or replacing the comment with a more accurate note like '// EvalSymlinks guarantees this is unreachable; kept for belt-and-suspenders'.@@ -465,23 +465,34 @@ mappings:}[NIT] The updated
TestValidateDocmapPath_Symlinktest correctly tests that an out-of-repo symlink target is rejected. However, the behavioral change introduced by this PR (in-repo symlinks are now allowed) is not covered by a positive test case. A test verifying that a file-level symlink whose target resides within the repo root is accepted would document and protect the new semantic. This is a test-coverage gap rather than a bug in production code.Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
4dce8e44)Security Review
Summary
The change correctly hardens path validation by resolving all symlink components with filepath.EvalSymlinks before the confinement check, closing the directory-symlink bypass. Tests appropriately cover the attack scenario, and CI has passed.
Findings
cmd/review-bot/validatedocmap.goRecommendation
APPROVE — The security fix is sound and closes the directory-symlink bypass by resolving all symlink components before applying the confinement check. Approve this PR. As a defense-in-depth improvement, consider passing the fully-resolved path from validateDocmapPath through to the YAML loader (or re-resolving immediately before opening) to avoid any check-then-use race between validation and file read. Otherwise, the changes adhere to secure patterns and the added tests provide good coverage.
Review by security
Evaluated against
4dce8e44@@ -37,22 +37,33 @@ func validateDocmapPath(localPath, resolvedRoot string) error {return fmt.Errorf("cannot resolve path: %w", err)[MINOR] Potential TOCTOU: validateDocmapPath validates the fully-resolved path (resolvedPath), but downstream code later opens the file using the original flag value (*docmapFlag). While low-risk in CI, using the validated resolved path for reading would eliminate any check-then-use race.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
4dce8e44)Gpt Review
Summary
The fix correctly resolves all symlink components with filepath.EvalSymlinks before enforcing the repo-root confinement, closing the directory-symlink bypass. Tests are updated appropriately, CI passed, and overall implementation follows idiomatic Go error handling and security hardening.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goRecommendation
APPROVE — The security fix is sound: resolving all symlink components with filepath.EvalSymlinks and then applying the confinement check on the resolved path correctly closes the directory-symlink bypass, while still allowing in-repo symlinks that resolve within the repo. Error handling and tests look good, and CI passed. Please update the function-level and inline comments in validatedocmap.go to reflect the new semantics (symlinks are allowed if the resolved target is within the repo root) to keep the documentation aligned with behavior. With those minor documentation tweaks, this is ready to merge.
Review by gpt
Evaluated against
4dce8e44Self-Review: PR #152
Self-review against
4dce8e4454Phase 1: Independent Findings
cmd/review-bot/validatedocmap.goModeSymlinkcheck: afterfilepath.EvalSymlinks,resolvedPathis guaranteed symlink-free —fi.Mode()&os.ModeSymlinkcan never be set. The comment calling it 'defense-in-depth' is misleading because the branch is unreachable. Either remove it or replace comment with// EvalSymlinks guarantees this is unreachable; kept for belt-and-suspenders.cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.gorunValidateDocmap(~line 113) still lists "Is not a symlink" as a validation point. Same inaccuracy as Finding 2 — should say "resolves within resolvedRoot (in-repo symlinks allowed if resolved target is within root)".cmd/review-bot/validatedocmap.govalidateDocmapPathvalidates*docmapFlag(resolving the path internally), butreview.ParseDocMapConfigis called with the original unresolved*docmapFlagstring. The validated resolved path is not propagated to the reader. Low-risk in ephemeral CI, but using the resolved path would eliminate any check-then-use race entirely.cmd/review-bot/validatedocmap_test.goPhase 2: Prior Review Verification
ModeSymlinkcheck afterEvalSymlinks; comment says 'defense-in-depth' but unreachableLstat+ModeSymlinkcheck retained at line ~56. Comment still says 'defense-in-depth'.TestValidateDocmapPath_InRepoSymlinkAllowedtest added.*docmapFlagbutParseDocMapConfigcalled with original stringreview.ParseDocMapConfig(*docmapFlag)still uses the original flag value, not the resolved path returned/implied byvalidateDocmapPath.validateDocmapPathstill lists the invariant as 'not a symlink' rather than reflecting the new 'resolved target within root' semantic.runValidateDocmapstill lists 'is not a symlink' as a stepAssessment: ⚠️ Needs attention
The security fix itself is correct and the core behavior is sound. However, multiple documentation/comment issues flagged by all three bots remain unaddressed on the current HEAD:
ModeSymlinkcheck + missing positive in-repo-symlink testThe two MINOR findings should be addressed before merging. The NITs can be addressed or waived.
Fix Plan against
0723b48ca4c304353789c1ff27012e88009f475fAddressing self-review findings from
4dce8e4:Finding 1 [NIT] — Dead
ModeSymlinkcheck: removed the unreachablefi.Mode()&os.ModeSymlinkbranch and updated theLstatcomment to say so — fixed in3f8eb58Finding 2 [MINOR] — Stale godoc invariant #2: updated from "The path is not a symlink" to "The resolved path is within resolvedRoot" to reflect the new in-repo-symlink semantic — fixed in
3f8eb58Finding 3 [MINOR] — Stale inline comment: updated bulleted item in
runValidateDocmapfrom "Is not a symlink" to "Resolved target stays within the root (in-repo symlinks allowed...)" — fixed in3f8eb58Finding 4 [MINOR] — TOCTOU gap: changed
validateDocmapPathto return(string, error); on success returns the EvalSymlinks-resolved path. Caller now passesresolvedDocmaptoParseDocMapConfiginstead of the original*docmapFlagstring — fixed ina3dbf91Finding 5 [NIT] — Missing positive test for in-repo symlink: added
TestValidateDocmapPath_InRepoSymlinkAllowedverifying that a file-level symlink whose resolved target stays within the root is accepted, and that the returned path is the resolved real file — fixed in0723b48Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
0723b48c)Sonnet Review
Summary
This PR correctly closes a directory-symlink bypass in
validateDocmapPathby callingfilepath.EvalSymlinksbefore the confinement check, and upgrades the function signature to return the resolved path (eliminating a TOCTOU race). The fix is minimal, well-targeted, and the new testTestValidateDocmapPath_InRepoSymlinkAllowedproperly validates the behavioral change. CI passed.Findings
cmd/review-bot/validatedocmap.goEvalSymlinksguarantees the path is symlink-free, thefi.Mode()&os.ModeSymlinkcheck (which was removed) was the only code that used this fact. The remaining Lstat is still needed for the size check and existence check, so the code is correct — but the comment 'ModeSymlink can never be set here; this is unreachable' refers to the now-deleted guard. This is fine as written, just slightly confusing phrasing since nothing is actually 'unreachable' in the remaining code — the Lstat itself is very much reached.cmd/review-bot/validatedocmap.goresolvedDocmapinstead of*docmapFlag. For paths that were symlinks, the error will report the resolved target path rather than the original user-supplied flag value. This could be mildly confusing to users who passed a symlink path. Minor UX concern — consider logging both, e.g.--docmap %q (resolved: %q) is invalid.Recommendation
APPROVE — Approve. The security fix is correct and closes the documented attack vector. The function signature change (
(string, error)instead oferror) eliminates the TOCTOU window between validation and use. The test coverage is thorough: the attack vector is reproduced inTestValidateDocmapPath_DirSymlinkBypass, the behavioral change is validated inTestValidateDocmapPath_InRepoSymlinkAllowed, and the existing symlink-outside test was updated to reflect the new semantics. The two findings are minor nits that do not affect correctness or security.Review by sonnet
Evaluated against
0723b48c@@ -59,3 +57,1 @@// Defense-in-depth: reject any remaining symlink indicator.if fi.Mode()&os.ModeSymlink != 0 {return fmt.Errorf("symlinks are not allowed")return "", fmt.Errorf("cannot stat file: %w", err)[NIT] The comment 'this is unreachable' on the ModeSymlink dead code path is accurate, but the Lstat call itself remains. Since
EvalSymlinksguarantees the path is symlink-free, thefi.Mode()&os.ModeSymlinkcheck (which was removed) was the only code that used this fact. The remaining Lstat is still needed for the size check and existence check, so the code is correct — but the comment 'ModeSymlink can never be set here; this is unreachable' refers to the now-deleted guard. This is fine as written, just slightly confusing phrasing since nothing is actually 'unreachable' in the remaining code — the Lstat itself is very much reached.Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
0723b48c)Gpt Review
Summary
Solid security fix: resolving symlinks before confinement closes the directory-symlink bypass and reduces TOCTOU risk by parsing via the resolved path. Tests cover both the attack and the new in-repo symlink allowance, and CI is passing.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goRecommendation
APPROVE — The change correctly addresses the symlink bypass by resolving all components with filepath.EvalSymlinks and then applying the confinement check, and it removes the TOCTOU window by parsing via the resolved path. The expanded tests exercise both the negative (outside-root) and positive (in-repo symlink) cases and are portable with skips where symlinks aren't supported. To further harden and align with the function comment, add a check to ensure the target is a regular file (fi.Mode().IsRegular()) before proceeding. Optionally prefer os.Stat over os.Lstat post-resolution to simplify the code and comment. With these small tweaks considered, the PR is in good shape.
Review by gpt
Evaluated against
0723b48cOriginal reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
0723b48c)Security Review
Summary
The change correctly addresses the directory-symlink bypass by resolving all symlink components with EvalSymlinks and then enforcing confinement, and it reduces TOCTOU risk by using the resolved path for parsing. Tests cover both the bypass and the new in-repo symlink behavior, and CI passed.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goRecommendation
APPROVE — This PR effectively closes the dir-symlink bypass by resolving all symlink components before confinement checks and by parsing the docmap via the resolved, symlink-free path. Tests accurately reflect the new behavior, including allowing in-repo symlinks whose targets remain within the repository root. To further harden against edge-case DoS scenarios, consider verifying that the resolved path is a regular file (fi.Mode().IsRegular()) and mitigating the residual TOCTOU by either rechecking size after opening the file or using a bounded reader. With these minor improvements noted, the current changes are sound and improve security; approve as is.
Review by security
Evaluated against
0723b48c@@ -23,18 +23,19 @@ const maxDocmapBytes int64 = 10 * 1024 * 1024 // 10 MB// 1. The path resolves to a regular file within resolvedRoot (path[MINOR] validateDocmapPath does not verify that the resolved target is a regular file (e.g., fi.Mode().IsRegular()). While Git cannot store FIFOs/device nodes, if the workspace were tampered with out-of-band, opening a named pipe or special file could cause blocking or unexpected behavior. Adding a regular-file check would harden against such DoS vectors.
@@ -141,3 +138,3 @@// 3. Does not exceed maxDocmapBytes (prevent memory exhaustion from an// oversized committed file).if err := validateDocmapPath(*docmapFlag, resolvedRoot); err != nil {// validateDocmapPath returns the resolved path; use it directly to[MINOR] Residual TOCTOU remains between the size check (os.Lstat) and later open/parse in review.ParseDocMapConfig, which could theoretically allow a larger file to be swapped in after validation. Low practical risk in CI, but for defense-in-depth consider rechecking after open or using a size-limited reader.
Fix Plan against
0723b48ca4Addressing self-review findings (all 5 findings resolved):
Finding 1 [NIT] — Dead
ModeSymlinkcheck with misleading 'defense-in-depth' comment — fixed in3f8eb58(unreachable branch removed; Lstat comment updated to say EvalSymlinks guarantees this)Finding 2 [MINOR] — Function-level godoc invariant #2 still said "The path is not a symlink" — fixed in
3f8eb58(updated to reflect new semantic: resolved path within resolvedRoot)Finding 3 [MINOR] — Inline comment in
runValidateDocmapstill listed "Is not a symlink" as a validation step — fixed in3f8eb58(updated to "Resolved target stays within the root, in-repo symlinks allowed if they resolve inside")Finding 4 [MINOR] — TOCTOU gap:
ParseDocMapConfigcalled with original unresolved*docmapFlaginstead of the validated resolved path — fixed ina3dbf91(validateDocmapPathnow returns(string, error); caller passes resolved path toParseDocMapConfig)Finding 5 [NIT] — No positive test for in-repo file-level symlink whose resolved target stays within repo root — fixed in
0723b48(TestValidateDocmapPath_InRepoSymlinkAllowedadded)Self-Review: PR #152
Self-review against
0723b48ca4Phase 1: Independent Findings
cmd/review-bot/validatedocmap.goos.Lstatcall itself is clearly reached (needed for size/existence check). The comment intends to say theModeSymlinkguard that was removed was unreachable — the Lstat is not. Consider rewording to:// Lstat the resolved path — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set. Still needed for size and existence checks.cmd/review-bot/validatedocmap.gofi.Mode().IsRegular()check is performed. A named pipe (FIFO) within the repo root would pass validation and block onParseDocMapConfig's open; a directory would pass with size 0. Addingif !fi.Mode().IsRegular() { return "", fmt.Errorf("not a regular file") }would align behavior with the documented invariant.cmd/review-bot/validatedocmap.goos.Lstatsize check (~line 60) and the later file open inreview.ParseDocMapConfig. Low practical risk in ephemeral CI, but a size-limited reader wrapping the open would eliminate the window entirely. Same concern raised by security bot (4566).Phase 2: Prior Review Verification
From self-review against
4dce8e4(all 5 findings):ModeSymlinkcheck with misleading comment3f8eb58)3f8eb58)runValidateDocmapsaid "Is not a symlink"3f8eb58)ParseDocMapConfigcalled with original*docmapFlagvalidateDocmapPathnow returns resolved path;ParseDocMapConfigcalled withresolvedDocmap(a3dbf91)TestValidateDocmapPath_InRepoSymlinkAllowedadded (0723b48)From latest round of bot reviews (4563/4564/4566 against
0723b48c):resolvedDocmapnot user-supplied flag valuefi.Mode().IsRegular()check — godoc says "regular file"os.LstatafterEvalSymlinks— could useos.Statfi.Mode().IsRegular()check (FIFO/dir DoS vector)Assessment: ⚠️ Needs attention
The core security fix is correct and well-tested. All 5 findings from the prior self-review are fully resolved. Three new/carried findings remain:
The two MINOR findings (IsRegular + residual TOCTOU) should be addressed before merging. The NITs can be waived.
Self-Review: PR #152
Self-review against
0723b48ca4Phase 1: Independent Findings
cmd/review-bot/validatedocmap.goModeSymlinkcheck followed it. That check was removed in this PR, so the comment now dangles:Lstatitself is very much reachable; only the former dead guard was unreachable. Should say something like// Lstat the resolved path to get file info for the size check.cmd/review-bot/validatedocmap.govalidateDocmapPathgodoc states invariant #1 as "resolves to a regular file" but the code does not enforcefi.Mode().IsRegular(). A directory, FIFO, or device node with a.ymlextension passes all guards and would be handed to the YAML parser. Addingif !fi.Mode().IsRegular() { return "", fmt.Errorf("not a regular file") }closes this gap and makes the comment accurate.cmd/review-bot/validatedocmap.goresolvedDocmap(the EvalSymlinks result), not the original*docmapFlag. When a user passed a symlink path the error will report the resolved target, which may be surprising. Consider"failed to parse docmap %q (resolved: %q)"or reverting to the original flag value in the error.cmd/review-bot/validatedocmap.goos.Lstatsize check andreview.ParseDocMapConfigopening the file: a larger file could be swapped in after validation. Low risk in ephemeral CI, but for defense-in-depth consider using a bounded/wrapping reader inside ParseDocMapConfig or re-checking size after open.Phase 2: Prior Review Verification
0723b48c)0723b48c)fi.Mode().IsRegular()check despite godoc saying "regular file"0723b48c)0723b48c)0723b48c)Assessment: ⚠️ Needs attention
All five prior self-review findings (from
4dce8e4) are fully resolved on this HEAD — confirmed. The new findings come from the bot reviews posted against0723b48c(the current HEAD). Two MINORs remain unaddressed:IsRegular()check (godoc says "regular file" but code doesn't enforce it — agreed by both gpt and security bots)Plus two NITs (dangling Lstat comment, error message shows resolved path). The two MINORs should be addressed before merging.
Self-Review: PR #152
Self-review against
0723b48ca4Phase 1: Independent Findings
cmd/review-bot/validatedocmap.goos.Lstat(~line 53) reads:// Lstat the resolved path — EvalSymlinks guarantees resolvedPath is symlink-free, so ModeSymlink can never be set here; this is unreachable.The phrase "this is unreachable" is confusing because theLstatcall itself is very much reachable — only the former deadModeSymlinkguard was unreachable (and it was already removed). Suggest rewording:// Lstat the resolved path — EvalSymlinks guarantees it is symlink-free. Needed for file info (size check below).cmd/review-bot/validatedocmap.govalidateDocmapPathgodoc lists invariant #1 as "The path resolves to a regular file within resolvedRoot" but the code does not callfi.Mode().IsRegular(). A directory, named pipe (FIFO), or device node with a.ymlsuffix would pass all guards and reachParseDocMapConfig. For alignment with the documented invariant, add:if !fi.Mode().IsRegular() { return "", fmt.Errorf("not a regular file") }cmd/review-bot/validatedocmap.goos.Lstat(~line 62) but the file is opened later insidereview.ParseDocMapConfig(resolvedDocmap). A file larger thanmaxDocmapBytescould in theory be swapped in after the stat. Low practical risk in ephemeral CI, but for defense-in-depth aio.LimitedReaderwrap insideParseDocMapConfig(or re-checking size after open) would eliminate the window entirely.cmd/review-bot/validatedocmap.goresolvedDocmap(the EvalSymlinks result) rather than the user-supplied*docmapFlag. When the user passed a symlink the error will report the resolved target, which may be surprising. Consider including both values:"failed to parse docmap %q (resolved: %q)"or reverting to the original flag value in the message.Phase 2: Prior Review Verification
All 5 findings from the self-review against
4dce8e4(2026-05-15T14:49:58Z) are fully resolved on this HEAD. Remaining open items from bot reviews at0723b48c:Assessment: ⚠️ Needs attention
The core security fix is correct and all five previous self-review findings (from
4dce8e4) are fully resolved on this HEAD. The persisting findings from round 4 bot reviews:The two MINORs should be addressed before merging. The NITs can be waived.
Self-Review: PR #152
Self-review against
0723b48ca4Phase 1: Independent Findings
cmd/review-bot/validatedocmap.go"EvalSymlinks guarantees resolvedPath is symlink-free, so ModeSymlink can never be set here; this is unreachable"— theLstatcall itself is very much reachable (it's needed for size/existence checks). The "unreachable" phrasing refers to the now-deletedModeSymlinkguard, not theLstat. Confusing to future readers. Suggest:// Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set.cmd/review-bot/validatedocmap.gofi.Mode().IsRegular()check is performed. A FIFO, directory, or device node with a.ymlextension inside the repo root passes all guards. Addingif !fi.Mode().IsRegular() { return "", fmt.Errorf("not a regular file") }would align behavior with the documented invariant and close a DoS vector.cmd/review-bot/validatedocmap.gorunValidateDocmapnow formatsresolvedDocmap(the EvalSymlinks result) rather than*docmapFlag. When a user passed a symlink path, the error reports the resolved target — surprising UX. Consider including both:"failed to parse docmap %q (resolved: %q)".cmd/review-bot/validatedocmap.goos.Lstatsize check andreview.ParseDocMapConfigopening the file — a larger file could be swapped in after validation. Low practical risk in ephemeral CI, but defense-in-depth (bounded reader or post-open recheck) would fully close the window.Phase 2: Prior Review Verification
Bot reviews against
0723b48c(sonnet/4563, gpt/4564, security/4566):resolvedDocmapnot original*docmapFlagfi.Mode().IsRegular()check despite godoc saying "regular file"os.Lstatvsos.Statafter EvalSymlinks (style)All 5 prior self-review findings (from
4dce8e4) remain RESOLVED on this HEAD — confirmed.Assessment: ⚠️ Needs attention
The core security fix (EvalSymlinks before confinement, TOCTOU elimination via returned resolved path, positive in-repo-symlink test) is correct and solid. Two MINORs remain unaddressed from the bot round:
IsRegular()check (godoc says "regular file"; code doesn't enforce it — agreed by both gpt and security bots)Plus two NITs (Lstat comment phrasing, error message shows resolved path). The two MINORs should be addressed before merging.
Self-Review: PR #152
Self-review against
0723b48ca4c304353789c1ff27012e88009f475fPhase 1: Independent Findings
cmd/review-bot/validatedocmap.go"EvalSymlinks guarantees resolvedPath is symlink-free, so ModeSymlink can never be set here; this is unreachable". TheLstatcall itself is very much reachable (needed for size/existence checks). The "unreachable" phrasing refers to the previously-deleted deadModeSymlinkguard, not theLstat. Confusing to future readers — suggest:// Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set.cmd/review-bot/validatedocmap.gofi.Mode().IsRegular()check is performed. A FIFO, directory, or device node with a.ymlextension inside the repo root passes all guards and reachesParseDocMapConfig. Addingif !fi.Mode().IsRegular() { return "", fmt.Errorf("not a regular file") }would align code with the documented invariant and close a DoS vector.cmd/review-bot/validatedocmap.goos.Lstatsize check andreview.ParseDocMapConfigopening the file — a larger file could be swapped in after validation. Low practical risk in ephemeral CI, but defense-in-depth (bounded reader or post-open recheck) would fully close the window.cmd/review-bot/validatedocmap.gorunValidateDocmapnow formatsresolvedDocmap(the EvalSymlinks result) rather than*docmapFlag. When a user passed a symlink path, the error reports the resolved target — potentially surprising UX. Consider including both:"failed to parse docmap %q (resolved: %q)".Phase 2: Prior Review Verification
From self-review against
4dce8e4(all 5 findings — verified RESOLVED on this HEAD):ModeSymlinkcheck with misleading 'defense-in-depth' comment3f8eb58)3f8eb58)runValidateDocmapsaid "Is not a symlink"3f8eb58)ParseDocMapConfigcalled with original*docmapFlagvalidateDocmapPathnow returns(string, error);ParseDocMapConfigcalled withresolvedDocmap(a3dbf91)TestValidateDocmapPath_InRepoSymlinkAllowedadded (0723b48)From bot reviews against
0723b48c(sonnet/4563, gpt/4564, security/4566):resolvedDocmapnot original*docmapFlagfi.Mode().IsRegular()check despite godoc saying "regular file"os.Lstatvsos.Statafter EvalSymlinks (style)Assessment: ⚠️ Needs attention
The core security fix (EvalSymlinks before confinement, TOCTOU elimination via returned resolved path, positive in-repo-symlink test) is correct and all 5 prior self-review findings are fully resolved. Two MINORs from the bot round remain unaddressed:
IsRegular()check — godoc says "regular file" but code doesn't enforce it (agreed by both gpt and security bots)Plus two NITs (Lstat comment phrasing, error message shows resolved path). The two MINORs should be addressed before merging.
0723b48ca4to345f9a5aacSonnet Review
Summary
This PR correctly fixes the directory-symlink bypass by calling
filepath.EvalSymlinksbefore the confinement check, and updates the function signature to return the resolved path to eliminate a TOCTOU race. The implementation is clean, well-documented, and the tests are comprehensive.Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goresolvedDocmapis now used in the error format string instead of*docmapFlag. This is a semantic improvement (no TOCTOU) but changes the user-visible error message to show the resolved path rather than the path the user specified. For paths with symlinks this could be confusing to the user. Not a correctness issue, but worth noting as a UX trade-off.Recommendation
APPROVE — Approve. The security fix is correct and complete:
filepath.EvalSymlinksis called at the right point, the resolved path is returned and used directly (eliminating TOCTOU), the old dead symlink-mode check is cleanly removed, and the documentation accurately reflects the new semantics. Tests cover the attack vector (TestValidateDocmapPath_DirSymlinkBypass), the positive behavioral change (TestValidateDocmapPath_InRepoSymlinkAllowed), and the out-of-repo symlink case. CI passed. The two findings are nits that don't affect correctness.Review by sonnet
Evaluated against
345f9a5a@@ -63,4 +59,4 @@// Reject anything that is not a regular file (directories, FIFOs, device// nodes, etc.) — ParseDocMapConfig expects a plain YAML file and would// produce a confusing error on non-regular entries.[NIT] The comment on the Lstat call says 'ModeSymlink can never be set here; this is unreachable' — if it's truly unreachable, the symlink check was removed but the comment is slightly misleading since the Lstat itself is still reached (it's only the symlink mode check that's unreachable). The comment accurately documents the reasoning, but the phrasing 'this is unreachable' could be confused as referring to the Lstat rather than the removed symlink check. A minor clarity issue, not a correctness concern.
Security Review
Summary
The change correctly closes the directory-symlink bypass by resolving all symlink components before confinement checks and using the resolved path for parsing, reducing TOCTOU risk. Tests thoroughly cover the negative and positive cases, and CI has passed.
Findings
cmd/review-bot/validatedocmap.goRecommendation
APPROVE — The fix is sound and improves security by resolving symlinks with filepath.EvalSymlinks before confinement and size checks, and by parsing the docmap from the resolved path. Tests validate both the blocked directory-symlink escape and the newly allowed in-repo file-level symlinks whose targets remain within the repo root. Given the CI context, the small residual TOCTOU window is low risk, but for defense-in-depth you may consider parsing from an opened file handle right after validation (e.g., open, fstat, and pass the reader to the YAML parser) to ensure the same inode is used and to prevent symlink substitution between validation and use. Overall, approve.
Review by security
Evaluated against
345f9a5a@@ -157,1 +155,4 @@// Parse docmap YAML using the resolved path — eliminates any TOCTOU race// between validation and use.cfg, err := review.ParseDocMapConfig(resolvedDocmap)if err != nil {[MINOR] Residual TOCTOU window remains between validation (Lstat/size check) and parse, since the file is reopened later by path. While acceptable for CI threat model, consider opening the file immediately after validation and parsing from the open file descriptor to fully eliminate races where the file could be replaced (e.g., with a symlink) between checks and use.
Gpt Review
Summary
The change correctly closes the directory-symlink bypass by resolving all symlink components before confinement checks and updates call sites and tests accordingly. Error handling and semantics are consistent with repository conventions, and CI passed.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goRecommendation
APPROVE — The security fix is sound: resolving symlinks with filepath.EvalSymlinks before running the root-relative check ensures the confinement decision is based on the actual target path, eliminating the directory symlink bypass. Returning the resolved path and using it for parsing also avoids a symlink flip between validation and use. Tests reflect both the negative (out-of-repo target rejection) and positive (in-repo symlink allowed) cases and maintain the stricter policy for docs: entries by treating symlinks as stale. Only minor documentation nits were noted. I recommend approval.
Review by gpt
Evaluated against
345f9a5a@@ -23,18 +23,19 @@ const maxDocmapBytes int64 = 10 * 1024 * 1024 // 10 MB// 1. The path resolves to a regular file within resolvedRoot (path[NIT] The validateDocmapPath doc comment repeats the confinement invariant in bullets 1 and 2; consider consolidating to avoid redundancy.
@@ -52,2 +53,2 @@// Lstat the resolved path — at this point resolvedPath is symlink-free, so// ModeSymlink will never be set. We keep the check as defense-in-depth.// Lstat the resolved path — EvalSymlinks guarantees resolvedPath is// symlink-free, so ModeSymlink can never be set here; this is unreachable.[NIT] Comment claims the ModeSymlink case is "unreachable" after EvalSymlinks; while practically true, a TOCTOU change could theoretically reintroduce a symlink between calls. Consider softening wording to "should not be set".
Self-Review: PR #152
Self-review against
345f9a5aacPhase 1: Independent Findings
cmd/review-bot/validatedocmap.go"EvalSymlinks guarantees resolvedPath is symlink-free, so ModeSymlink can never be set here; this is unreachable."The phrase"this is unreachable"is misleading because theLstatcall itself is very much reachable — only the now-deleted deadModeSymlinkguard was unreachable. Suggest:// Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set.cmd/review-bot/validatedocmap.goresolvedDocmap(the EvalSymlinks result) rather than the user-supplied*docmapFlag. When the user passed a symlink path, the error reports the resolved target — potentially surprising. Consider"failed to parse docmap %q (resolved: %q)"or using the original flag value.cmd/review-bot/validatedocmap.goos.Lstatsize check andreview.ParseDocMapConfigopening the file. A file larger thanmaxDocmapBytescould be swapped in after the stat. Low practical risk in ephemeral CI, but a bounded reader (io.LimitedReader) insideParseDocMapConfigwould fully close the window.Phase 2: Prior Review Verification
From last self-review against
0723b48c(STILL PRESENT items):resolvedDocmapnot original*docmapFlagfi.Mode().IsRegular()check despite godoc saying "regular file"IsRegular()check added at lines 63-64; error message is"docmap must be a regular file"os.Lstatvsos.Statafter EvalSymlinks (style)Assessment: ⚠️ Needs attention
The core security fix (EvalSymlinks before confinement, TOCTOU elimination via returned resolved path,
IsRegular()check, in-repo-symlink test) is correct and solid. One MINOR remains:ParseDocMapConfigfile open (security bot, now confirmed on current HEAD)Plus three NITs (Lstat comment phrasing, error message shows resolved path, Lstat vs Stat style). The MINOR should be addressed before merging. NITs can be waived.
Fix Plan against
d6bab7a9cf6ce3d084d8f40a91a51c1a8fc084e7Addressing self-review findings from
345f9a5(all 3 findings resolved):// Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set.6e11107resolvedDocmap(resolved path) instead of original--docmapvalueresolvedDocmapto*docmapFlag4359518Lstatsize check andParseDocMapConfigfile openreview.ParseDocMapConfig(resolvedDocmap)with explicitos.Open+io.LimitReader(f, maxDocmapBytes+1)+io.ReadAll+review.ParseDocMapConfigContent. The file is now bounded at the read call itself, not just the preceding stat.d6bab7aAll tests pass:
go test ./...green.Sonnet Review
Summary
This PR correctly fixes a directory-symlink bypass in
validateDocmapPathby callingfilepath.EvalSymlinksbefore the confinement check, and updates the function signature to return the resolved path for TOCTOU-safe use. The changes are well-reasoned, properly documented, and accompanied by tests covering the attack vector and the new behavioral allowance for in-repo symlinks.Findings
cmd/review-bot/validatedocmap.godefer f.Close()has a// nolint:errcheckcomment. Per the conventions, errors should always be checked. For a read-only file used to parse YAML, a Close error is harmless in practice, but this suppression is a deviation from the project's 'check all error returns' convention. Consider assigning the Close error to a local variable and logging/ignoring it explicitly, or accept that adeferClose on a read-only file doesn't need a nolint suppressor (many projects treat this as acceptable).cmd/review-bot/validatedocmap_test.goTestValidateDocmapPath_InRepoSymlinkAllowedtest checksif resolved == symlinkPathto verify that the returned path differs from the symlink. On some systems (e.g., macOS where/tmpis a symlink to/private/tmp),t.TempDir()already returns an EvalSymlinks-resolved path, butsymlinkPathis constructed viafilepath.Join(dir, ...)which may not yet be resolved. The comparison is reliable here becausevalidateDocmapPathcallsEvalSymlinksinternally, soresolvedwill always differ fromsymlinkPath(a symlink entry). The assertion is correct but a comment clarifying why they must differ would help future readers.Recommendation
APPROVE — Approve. The security fix is correct and closes the documented directory-symlink bypass. The function signature change (
error→(string, error)) returning the resolved path is the right design improvement, as it eliminates the TOCTOU gap betweenvalidateDocmapPathand the subsequentos.Open. The removal of the redundant defense-in-depthModeSymlinkcheck is safe becausefilepath.EvalSymlinksguarantees no symlink components remain. Tests cover the attack vector (TestValidateDocmapPath_DirSymlinkBypass), the new positive case (TestValidateDocmapPath_InRepoSymlinkAllowed), and the updated rejection case (TestValidateDocmapPath_Symlink). CI passes. The two NITs are minor style issues that do not affect correctness.Review by sonnet
Evaluated against
d6bab7a9@@ -652,0 +690,4 @@if resolved == symlinkPath {t.Errorf("expected resolved path to differ from symlink path")}}[NIT] The new
TestValidateDocmapPath_InRepoSymlinkAllowedtest checksif resolved == symlinkPathto verify that the returned path differs from the symlink. On some systems (e.g., macOS where/tmpis a symlink to/private/tmp),t.TempDir()already returns an EvalSymlinks-resolved path, butsymlinkPathis constructed viafilepath.Join(dir, ...)which may not yet be resolved. The comparison is reliable here becausevalidateDocmapPathcallsEvalSymlinksinternally, soresolvedwill always differ fromsymlinkPath(a symlink entry). The assertion is correct but a comment clarifying why they must differ would help future readers.Security Review
Summary
Solid security fix that closes the directory-symlink bypass by resolving all symlink components before confinement checks. The change also adds a size-limited read to mitigate DoS and updates tests accordingly. One minor defense-in-depth opportunity remains regarding a potential TOCTOU race between validation and open.
Findings
cmd/review-bot/validatedocmap.goRecommendation
APPROVE — The fix correctly uses filepath.EvalSymlinks on the docmap path prior to applying the confinement check, closing the directory-symlink bypass while allowing in-repo symlinks whose targets remain within the root. The subsequent use of a size-limited reader further reduces DoS risk and narrows the TOCTOU window. CI has passed, tests cover the new behavior, and no disallowed dependencies were introduced. As a minor enhancement, consider adding a post-open verification (e.g., compare the pre-open Lstat result with f.Stat using os.SameFile) or platform-appropriate O_NOFOLLOW/openat patterns to eliminate the remaining race where a file could be swapped to a symlink between validation and open. Otherwise, the changes are sound and improve security.
Review by security
Evaluated against
d6bab7a9@@ -157,0 +157,4 @@// here. The limit is maxDocmapBytes+1 so we can detect a file that grew past// the cap after the stat without reading unbounded bytes.f, err := os.Open(resolvedDocmap)if err != nil {[MINOR] There is a small TOCTOU window between the Lstat validation and os.Open, during which an attacker with write access to the workspace could replace the validated regular file with a symlink pointing outside the repo. Although unlikely in this threat model and partially mitigated by the size-limited read, consider further hardening by comparing f.Stat() to the earlier Lstat result (os.SameFile) or using O_NOFOLLOW/openat where feasible to prevent symlink races.
Gpt Review
Summary
The change correctly hardens validateDocmapPath by resolving all symlink components with filepath.EvalSymlinks and applying confinement to the resolved path, closing the directory-symlink bypass. The refactor to return the resolved path and the TOCTOU-aware read path are sound, tests are updated/added appropriately, and CI passed.
Recommendation
APPROVE — This PR implements a solid security fix aligned with Go filesystem safety patterns: resolve symlinks before confinement checks and operate on the resolved path. The additional read path using io.LimitReader mitigates TOCTOU expansion. Tests accurately cover the regression and new behavior (allowing in-repo symlinks) and the suite passes. No further changes requested.
Review by gpt
Evaluated against
d6bab7a9Self-Review: PR #152
Self-review against
d6bab7a9cfPhase 1: Independent Findings
validatedocmap.goL57ModeSymlink can never be set here; this is unreachable— the Lstat itself is reached (it's used for the size and existence checks), sounreachableis misleading. Only a formerModeSymlinkguard would have been unreachable. The comment could read// EvalSymlinks guarantees no symlink components remain, so ModeSymlink cannot be set.instead. Harmless/cosmetic.validatedocmap.goL183defer f.Close() // nolint:errcheck— suppressing errcheck on a read-only file's defer close is pragmatic, but the project convention is to check all error returns. For consistency,_ = f.Close()or an explicit ignore comment like// read-only; close error is harmlesswould be cleaner. Minor style deviation.validatedocmap.goL196–200if int64(len(docmapData)) > maxDocmapBytes) is correct defense-in-depth but has no test exercising the grow-after-stat path. The existingTestValidateDocmapPath_SizeLimitonly covers the pre-open Lstat rejection. A small coverage gap — not a blocker.Phase 2: Prior Review Verification
Most-recent reviews against current HEAD (
d6bab7a9):All earlier MAJOR/MINOR findings from prior rounds (doc comment staleness, TOCTOU-via-original-flag, missing in-repo-symlink test) were addressed in commits up to and including
d6bab7a9. The function now:TestValidateDocmapPath_InRepoSymlinkAllowedcovering the positive in-repo symlink caseio.LimitReaderto bound the post-open readNo outstanding unresolved MAJOR or MINOR findings. The security MINOR (Lstat→open race) is a known residual accepted by all three bots.
Assessment: ✅ Clean
The diff is in good shape. Security fix is correct, TOCTOU surface is substantially reduced, tests are comprehensive. Three NITs remain (misleading comment, nolint style, missing post-open size test) — none are blockers. All prior MAJOR/MINOR findings have been addressed.
Fix Plan against
d6bab7a9cf6ce3d084d8f40a91a51c1a8fc084e7:Addressing bot-review findings from the current HEAD round (sonnet/4810, security/4812, gpt/4813):
defer f.Close() // nolint:errcheckdeviates from project convention (check all error returns)defer func() { _ = f.Close() }()— explicit ignore, no suppressor neededAfter getting the resolved path from validateDocmapPath, Lstat the path immediately before os.Open, then compare with f.Stat() after open using os.SameFile. If the file was swapped between validation and open (e.g., replaced with a symlink pointing outside the repo), the inode comparison catches it and returns an error. Also changes defer f.Close() // nolint:errcheck to defer func() { _ = f.Close() }() to follow the project convention of explicit ignores over suppressor comments. Addresses security bot finding (review 4812) againstd6bab7a9.Sonnet Review
Summary
This PR correctly fixes a directory-symlink bypass vulnerability by calling
filepath.EvalSymlinksbefore the confinement check, and improves the TOCTOU posture by returning the resolved path fromvalidateDocmapPathand using it directly for all subsequent I/O. The changes are well-reasoned, well-documented, and the new/updated tests cover both the attack vector and the positive case for in-repo symlinks. CI passes.Findings
cmd/review-bot/validatedocmap.goos.Lstat(resolvedDocmap)→os.Open(resolvedDocmap)→f.Stat()→os.SameFilepattern for TOCTOU detection is a genuine improvement, butos.SameFilecompares inode/device, not content. On Linux, an attacker who can atomically replace the file with another hardlink pointing to a different inode between Lstat and Open could still bypass this (though this is an extremely narrow and unlikely window). This is a known limitation of the pattern, not a bug introduced here, and the comment already calls it 'defense-in-depth'. No action required — just noting it for awareness.cmd/review-bot/validatedocmap.gofi.Mode()&os.ModeSymlink != 0check was removed. The comment is accurate and the removal is correct (the check was genuinely unreachable), but removing it does reduce defense-in-depth. Given the comment's explicit acknowledgment this is a deliberate and documented choice — no issue.Recommendation
APPROVE — Approve. The core security fix is correct:
filepath.EvalSymlinkson the full path before thefilepath.Relconfinement check closes the directory-symlink bypass described in issue #150. The function signature change to return(string, error)and the use of the resolved path throughout eliminates the TOCTOU race between validation and use. The new testTestValidateDocmapPath_DirSymlinkBypassdirectly reproduces the attack vector, andTestValidateDocmapPath_InRepoSymlinkAlloweddocuments and validates the intentional behavioral change. The two nits above are observations about known trade-offs in the chosen security model, not defects.Review by sonnet
Evaluated against
eb0ff3aa@@ -66,3 +62,3 @@// produce a confusing error on non-regular entries.if !fi.Mode().IsRegular() {return fmt.Errorf("docmap must be a regular file")return "", fmt.Errorf("docmap must be a regular file")[NIT] The comment on the Lstat call says 'ModeSymlink can never be set' after EvalSymlinks, but the old defense-in-depth
fi.Mode()&os.ModeSymlink != 0check was removed. The comment is accurate and the removal is correct (the check was genuinely unreachable), but removing it does reduce defense-in-depth. Given the comment's explicit acknowledgment this is a deliberate and documented choice — no issue.@@ -154,3 +154,2 @@// Parse docmap YAML.cfg, err := review.ParseDocMapConfig(*docmapFlag)// Open and read the docmap with a LimitedReader — closes the residual TOCTOU[NIT] The pre-open
os.Lstat(resolvedDocmap)→os.Open(resolvedDocmap)→f.Stat()→os.SameFilepattern for TOCTOU detection is a genuine improvement, butos.SameFilecompares inode/device, not content. On Linux, an attacker who can atomically replace the file with another hardlink pointing to a different inode between Lstat and Open could still bypass this (though this is an extremely narrow and unlikely window). This is a known limitation of the pattern, not a bug introduced here, and the comment already calls it 'defense-in-depth'. No action required — just noting it for awareness.Security Review
Summary
Security fix correctly closes the directory-symlink bypass by resolving all symlink components and tightening TOCTOU handling. Tests cover the attack and the new in-repo symlink behavior, and CI passed.
Recommendation
APPROVE — The approach of using filepath.EvalSymlinks prior to confinement checks and validating again at open time with os.SameFile is solid and addresses the reported bypass while limiting TOCTOU risk. Allowing in-repo symlinks whose targets resolve within the repo is a reasonable, documented semantic change. No further security issues were identified. Proceed with merge.
Review by security
Evaluated against
eb0ff3aaGpt Review
Summary
The fix correctly closes the directory-symlink bypass by resolving all symlink components before confinement checks and strengthens TOCTOU defenses when opening the docmap. Tests are updated appropriately, and CI passed.
Findings
cmd/review-bot/validatedocmap.gocmd/review-bot/validatedocmap.goRecommendation
APPROVE — The changes are sound and align with secure path-handling practices: resolving symlinks before applying confinement checks prevents directory-symlink escapes, and the added pre/post stat with os.SameFile plus LimitReader mitigates TOCTOU and memory-exhaustion risks. Tests cover the new behavior, including both rejecting out-of-repo symlink targets and allowing in-repo symlinks whose targets remain within the root. CI passed. Consider minor cleanups noted, but they are non-blocking. Approve the PR.
Review by gpt
Evaluated against
eb0ff3aa@@ -67,3 +63,3 @@if !fi.Mode().IsRegular() {return fmt.Errorf("docmap must be a regular file")return "", fmt.Errorf("docmap must be a regular file")}[NIT] After EvalSymlinks, using os.Lstat is equivalent to os.Stat since symlink components are resolved; keeping Lstat is fine but could be simplified to Stat for clarity.
@@ -144,16 +140,59 @@ func runValidateDocmap(args []string) int {// may reference a PR-controlled file (e.g. .review-bot/doc-map.yml).[NIT] Error messages after reading the docmap use the original --docmap path instead of the resolved path. This is acceptable (user-friendly) but consider whether using the resolved path would aid debugging in symlink scenarios.
Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed:
validateDocmapPathsignature change (error→(string, error)), deadModeSymlinkcheck removal,IsRegular()guard added, TOCTOU mitigation viaos.SameFile+io.LimitReader,defer func() { _ = f.Close() }()convention fix,ParseDocMapConfigContentcall with user-friendly error messages, andTestValidateDocmapPath_InRepoSymlinkAllowedclarifying comment. All changes are correct and internally consistent.Phase 2: Prior Review Verification
defer f.Close() // nolint:errcheckdeviates from project conventiondefer func() { _ = f.Close() }()inc76e7dcresolved != symlinkPathmust always differeb0ff3af.Stat()+os.SameFilecheck added inc76e7dcos.SameFilecompares inode — known race-window limitationModeSymlinkdead check reduces defense-in-depthos.Lstatvsos.Statafter EvalSymlinks--docmapnot resolved pathAssessment: ✅ Clean
All prior MAJOR/MINOR findings from every review round have been addressed. Current HEAD has only NITs, all either resolved or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()check prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces the TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassandTestValidateDocmapPath_InRepoSymlinkAllowedprovide solid regression coverageReady for human review.
Fix Plan against
eb0ff3aa69f152dd995de91c88227d3e32ac2917:Addressing bot-review findings from the latest round (sonnet/4814, security/4816, gpt/4817) — all evaluated against
eb0ff3aa:defer func() { _ = f.Close() }()— reviewer flagged as nolint deviation, but code already uses the_ = f.Close()convention (not a baredefer f.Close()with nolint suppressor). The code is correct per project convention.TestValidateDocmapPath_InRepoSymlinkAllowedcould clarify why resolved != symlinkPathos.Statinstead ofos.Lstatafter EvalSymlinks for clarityLstatis intentionally consistent withcheckStaleDocswhich usesLstatto avoid symlink-follow; mixingStat/Lstatin the same file would be more confusing. Documenting the deliberate choice is sufficient.*docmapFlag(user path) rather than resolved pathAll findings are NITs. The three defer justifications above are sound. No code changes required.
No commits needed — the code at
eb0ff3aais ready.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation and that no action is required — the code comment already calls it defense-in-depth. No change needed.
Finding #2 (ACK-NOT-VALID): Acknowledged. The comment saying 'ModeSymlink can never be set' is accurate — EvalSymlinks guarantees no symlinks remain, so that check was genuinely unreachable. The removal was intentional and documented. No action needed.
Finding #4 (ACK-NOT-VALID): Acknowledged.
Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid follow-on symlink semantics. Switching toStatpost-EvalSymlinks would be equivalent at runtime but would create an inconsistency across the file that could confuse future readers. KeepingLstatis the more conservative and consistent choice.Finding #5 (ACK-NOT-VALID): Acknowledged. User-facing error messages intentionally reference the original
--docmapflag value, not the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they'd see a path they never specified. Using the flag value is the correct UX here.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly states this is a known limitation of the
os.SameFilepattern and that no action is required — the code comment already calls it defense-in-depth. The narrow hardlink-swap race is a theoretical limitation of the approach, not a defect introduced here.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms no issue — the ModeSymlink removal is correct (the check was genuinely unreachable after EvalSymlinks), the comment is accurate, and the removal is a deliberate documented choice. The comment phrasing is slightly imprecise but does not affect correctness.
Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toStatpost-EvalSymlinks would be equivalent at runtime but would create a mixedStat/Lstatpattern in the same file that could confuse future readers. The deliberate choice is noted in the comment.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they would see a path they never specified. Using the flag value is the correct UX. The resolved path is used internally for all I/O; the flag value is only used in error messages.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that no action is required — the code comment already calls it defense-in-depth. The narrow hardlink-swap race is a theoretical limitation of the approach, not a defect introduced here.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms no issue — the ModeSymlink removal is correct (the check was genuinely unreachable after
filepath.EvalSymlinks), the comment is accurate, and the removal is a deliberate documented choice. The comment phrasing is slightly imprecise but does not affect correctness.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toStatpost-EvalSymlinks would be equivalent at runtime but would create a mixedStat/Lstatpattern in the same file that could confuse future readers. The deliberate choice is noted in the comment.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they would see a path they never specified. Using the flag value is the correct UX. The resolved path is used internally for all I/O; the flag value is only used in error messages.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. The narrow hardlink-swap window is a theoretical limitation of this approach, not a defect introduced here. No change needed.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms no issue — the ModeSymlink removal is correct (the check was genuinely unreachable after
filepath.EvalSymlinks), the comment is accurate, and the removal is a deliberate documented choice. No action needed.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toStatpost-EvalSymlinks would be equivalent at runtime but would create a mixedStat/Lstatpattern in the same file that could confuse future readers. Deliberate choice, no change needed.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they would see a path they never specified. The resolved path is used for all I/O; the original flag value is only used in error messages. Correct UX, no change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer notes this is a known limitation of
os.SameFileand explicitly states no action required — the code comment already calls it defense-in-depth. No change needed. (Finding #1 from fix plan againsteb0ff3aa)Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that no action is required — the code comment already calls it defense-in-depth. The narrow hardlink-swap race is a theoretical limitation of the approach, not a defect introduced here.Finding #2 (ACK-NOT-VALID): Acknowledged. The comment saying 'ModeSymlink can never be set' is accurate —
filepath.EvalSymlinksguarantees no symlinks remain, so the old check was genuinely unreachable. Its removal was intentional and documented. No action needed.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally kept here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. WhileStatandLstatare equivalent afterfilepath.EvalSymlinks, mixing them in the same file would create inconsistency that could confuse future readers. The deliberate choice is noted in the code comment.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they would see a path they never specified. The resolved path is used for all I/O; the flag value is only used in error messages, which is the correct UX.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.test reply
All findings from the current HEAD round (sonnet/4814, security/4816, gpt/4817 against
eb0ff3aa) have been addressed:Thread 27980 (sonnet NIT — os.SameFile TOCTOU limitation): ACK-NOT-VALID. Reviewer explicitly states "no action required" and the code comment already calls it defense-in-depth.
Thread 27981 (sonnet NIT — ModeSymlink comment after removal): ACK-NOT-VALID. Reviewer confirms no issue — removal is correct, comment is accurate.
Thread 27986 (gpt NIT — Lstat vs Stat after EvalSymlinks): ACK-NOT-VALID.
Lstatis intentionally used for consistency withcheckStaleDocsto avoid implicit symlink follow.Thread 27987 (gpt NIT — error messages use original path): ACK-NOT-VALID. User-facing errors intentionally reference
*docmapFlag(the original--docmapvalue), not the resolved path, for user clarity.No code changes required. All three bots APPROVED at
eb0ff3aa.Finding #1 (ACK-NOT-VALID): Acknowledged. This is a known OS-level limitation of the
os.SameFilepattern documented in the code comment as defense-in-depth. The reviewer explicitly states "no action required." The code is correct as-is.Finding #2 (ACK-NOT-VALID): Acknowledged. The
ModeSymlinkguard was genuinely unreachable afterEvalSymlinksand its removal is correct. The comment accurately documents why. The reviewer confirms this is a deliberate, documented choice — no issue.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink follow. MixingStat/Lstatin the same file would be more confusing than the minor clarity benefit. The reviewer notes this is fine and the PR is approved.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) for user clarity — showing a resolved path the user never specified would be surprising. The reviewer notes this is acceptable. No change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms no issue — the ModeSymlink removal is correct (the check was genuinely unreachable after EvalSymlinks), the comment is accurate, and the removal is a deliberate documented choice.
Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toStatpost-EvalSymlinks would be equivalent but would create an inconsistency within the file.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who specified a symlink — they want to see the path they actually provided.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of
os.SameFileand that "no action is required" — the code comment already labels it defense-in-depth. No change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly states this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms no issue — the
ModeSymlinkcheck removal is correct (the check was genuinely unreachable afterfilepath.EvalSymlinks), the comment is accurate, and this is a deliberate documented choice. No action needed.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toStatpost-EvalSymlinks would be equivalent in practice but inconsistent with the rest of the file. Deliberate choice — no change needed.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path would confuse users who specified a symlink — they would see a path they did not specify. Accepted UX tradeoff.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No change needed.Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed:
validateDocmapPathsignature change (error→(string, error)),filepath.EvalSymlinksinsertion before confinement, removal of deadModeSymlinkdefense-in-depth check (correctly unreachable post-EvalSymlinks),IsRegular()guard for non-regular files, TOCTOU mitigation via pre-openos.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader,defer func() { _ = f.Close() }()convention,ParseDocMapConfigContentcall using resolved path, andTestValidateDocmapPath_InRepoSymlinkAllowedpositive-case test. All changes are correct and internally consistent.Phase 2: Prior Review Verification
os.SameFileTOCTOU inode-race limitationModeSymlinkdead check reduces defense-in-depthos.Lstatvsos.Statafter EvalSymlinks--docmappath not resolved pathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs, all either resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksinserted afterfilepath.Absand before confinement check — correct and closes the dir-symlink bypassfi.Mode()&os.ModeSymlinkbranch correctly removed (unreachable post-EvalSymlinks; code comment explains why)IsRegular()guard: correct placement, prevents FIFO/device-node abusevalidateDocmapPathreturn type(string, error): signature change is backward-safe, all call sites updatedParseDocMapConfigContentcalled withresolvedDocmap— eliminates check-then-use raceos.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1): defense-in-depth TOCTOU mitigation is sound and well-commenteddefer func() { _ = f.Close() }()follows project conventionTestValidateDocmapPath_DirSymlinkBypassreproduces the attack;TestValidateDocmapPath_InRepoSymlinkAlloweddocuments intentional behavioral change;TestValidateDocmapPath_Symlinkupdated correctly for new semantics (out-of-repo symlink target rejected)TestValidateDocmapPath_InRepoSymlinkAllowedexplaining whyresolved != symlinkPath— clearPhase 2: Prior Review Verification
os.SameFileinode-race limitation (known OS constraint)ModeSymlinkcheck reduces defense-in-depthos.Lstatvsos.Statafter EvalSymlinks--docmappath, not resolved pathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all either resolved in code or accepted with documented rationale. Security fix is correct and well-tested. Ready for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150)fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; comment explains whyIsRegular()guard — correct placement after EvalSymlinks/Lstat, rejects FIFOs/device nodesvalidateDocmapPathreturn type(string, error)— backward-safe signature change, all call sites updatedParseDocMapConfigContentcalled withresolvedDocmap— eliminates check-then-use TOCTOU raceos.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documented in commentdefer func() { _ = f.Close() }()— follows project conventionTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers positive case for changed behavior;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests shareos.Stdin)Phase 2: Prior Review Verification
os.SameFileinode-race limitation (known OS constraint)ModeSymlinkcheck reduces defense-in-depthos.Lstatvsos.Statafter EvalSymlinks--docmappath, not resolved pathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested. Ready for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.go:validateDocmapPath:filepath.EvalSymlinks(absPath)inserted beforeos.Lstatandfilepath.Rel— correctly closes the dir-symlink bypass. Function signature updated to return(string, error)returning the resolved path for TOCTOU-safe use by callers. ✅runValidateDocmap: UsesresolvedDocmapfromvalidateDocmapPathdirectly for all I/O. Pre-openos.Lstat+ post-openf.Stat()+os.SameFilecomparison provides defense-in-depth TOCTOU guard.io.LimitReader(f, maxDocmapBytes+1)enforces size cap at read time as a second layer. ✅defer func() { _ = f.Close() }(): Explicit error-ignore, no nolint suppressor — correct project convention. ✅*docmapFlag(user-supplied path) in error output is correct UX — showing a path the user didn't specify would be confusing. ✅os.LstatafterEvalSymlinks: Technically equivalent toos.Stathere (no symlink components remain), but intentionally consistent withcheckStaleDocs; mixingStat/Lstatin the same file would be more confusing. ✅TestValidateDocmapPath_DirSymlinkBypassdirectly reproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo symlink case with clear comment explaining whyresolved != symlinkPath. ✅Phase 2: Prior Review Verification
Latest bot round evaluated against
eb0ff3aa(sonnet/4814, security/4816, gpt/4817):os.SameFileTOCTOU limitation acknowledgedresolved != symlinkPatheb0ff3aacommit explaining EvalSymlinks guarantees resolved ≠ symlink entryModeSymlinkcheck deliberateos.Lstat→os.Statfor clarity after EvalSymlinkscheckStaleDocs; mixingStat/Lstatin same file adds confusionAll prior findings addressed or justified. No MAJOR or MINOR open issues.
Assessment: ✅ Clean
All three bots approved at
eb0ff3aa. All findings are NITs with sound defer justifications. Security fix is correct. TOCTOU defense is solid. Tests cover the attack vector and behavioral change. Ready for merge.Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150)fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; comment explains rationaleIsRegular()guard — correct placement after Lstat, rejects FIFOs/device nodes as documentedvalidateDocmapPathreturn type(string, error)— backward-safe signature change; all call sites updatedParseDocMapConfigContentcalled withresolvedDocmap— eliminates check-then-use TOCTOU raceos.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documenteddefer func() { _ = f.Close() }()— follows project conventionTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests share globalos.Stdin)Phase 2: Prior Review Verification
os.SameFileinode-race limitationd6bab7aIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested. Ready for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150) ✅fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; comment explains rationale ✅IsRegular()guard — correct placement after Lstat, rejects FIFOs/device nodes as documented ✅validateDocmapPathreturn type(string, error)— backward-safe signature change; all call sites updated ✅ParseDocMapConfigContentcalled withresolvedDocmap— eliminates check-then-use TOCTOU race ✅os.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documented ✅defer func() { _ = f.Close() }()— follows project convention ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests share globalos.Stdin) ✅Phase 2: Prior Review Verification
os.SameFileinode-race limitationd6bab7aIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested. Ready for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150) ✅fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; comment explains rationale ✅IsRegular()guard — correct placement after Lstat, rejects FIFOs/device nodes as documented ✅validateDocmapPathreturn type(string, error)— backward-safe signature change; all call sites updated ✅ParseDocMapConfigContentcalled with boundeddocmapDatabytes — eliminates check-then-use TOCTOU race ✅os.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documented ✅defer func() { _ = f.Close() }()— follows project convention ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests share globalos.Stdin) ✅Phase 2: Prior Review Verification
os.SameFileinode-race limitationModeSymlinkcheckd6bab7aIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150) ✅fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; comment explains rationale ✅IsRegular()guard — correct placement after Lstat, rejects FIFOs/device nodes as documented ✅validateDocmapPathreturn type(string, error)— backward-safe signature change; all call sites updated ✅ParseDocMapConfigContentcalled with boundeddocmapDatabytes — eliminates check-then-use TOCTOU race ✅os.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documented ✅defer func() { _ = f.Close() }()— follows project convention ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests share globalos.Stdin) ✅Phase 2: Prior Review Verification
os.SameFileinode-race limitationModeSymlinkcheckd6bab7aIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:validateDocmapPathreturn type(string, error): Signature change is backward-safe; all call sites updated (the one test callsite updated toif _, err := validateDocmapPath(...)). ReturnsresolvedPath(EvalSymlinks result) on success — enables TOCTOU-safe use by caller. ✅filepath.EvalSymlinksbefore confinement check: Inserted afterfilepath.Abs, beforeos.Lstatandfilepath.Rel— correctly closes the dir-symlink bypass from issue #150. ✅fi.Mode()&os.ModeSymlinkbranch removed: Correctly unreachable afterEvalSymlinks; Lstat comment updated to read "Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set." — accurate and clear. ✅IsRegular()guard present: Rejects FIFOs, directories, and device nodes — aligns code with documented invariant #1. ✅runValidateDocmapTOCTOU mitigation: Pre-openos.Lstat+ post-openf.Stat()+os.SameFilecheck defends against file-swap between validation and open.io.LimitReader(f, maxDocmapBytes+1)enforces size cap at read time as a second layer.ParseDocMapConfigContentcalled with boundeddocmapDatabytes (not the original*docmapFlagpath). ✅defer func() { _ = f.Close() }(): Explicit error-ignore, no nolint suppressor — correct project convention. ✅*docmapFlag(user-supplied path) — correct UX; users see the flag value they provided. ✅os.LstatafterEvalSymlinks: Technically equivalent toos.Stat(no symlink components remain), but intentional for consistency withcheckStaleDocs. ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment ("validateDocmapPath calls filepath.EvalSymlinks internally, so the returned path is always the fully-resolved real path — it can never equal the symlink entry itself.");TestValidateDocmapPath_Symlinkupdated for new semantics. ✅Phase 2: Prior Review Verification
os.SameFileinode-race limitationModeSymlinkcheck reduces defense-in-depthd6bab7aIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69f152dd995de91c88227d3e32ac2917Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check: Inserted afterfilepath.Abs, beforeos.Lstatandfilepath.Rel— correctly closes the dir-symlink bypass from issue #150. ✅fi.Mode()&os.ModeSymlinkcheck removed: Correctly unreachable afterEvalSymlinks. Lstat comment updated to read "Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set." — accurate and clear. ✅IsRegular()guard: Rejects FIFOs, directories, and device nodes — aligns code with documented invariant. ✅validateDocmapPathreturn type(string, error): Backward-safe signature change; all call sites updated. ReturnsresolvedPath(EvalSymlinks result) on success — enables TOCTOU-safe use by caller. ✅os.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1).ParseDocMapConfigContentcalled with boundeddocmapDatabytes (not the original*docmapFlagpath). ✅defer func() { _ = f.Close() }(): Explicit error-ignore, no nolint suppressor — correct project convention. ✅*docmapFlag: Correct UX — user sees the flag value they provided. ✅os.LstatafterEvalSymlinks: Technically equivalent toos.Stathere, but intentional for consistency withcheckStaleDocs. ✅checkStaleDocsstill usesos.Lstatfor docs entries: Consistent; correctly treats symlinks as stale. ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment;TestValidateDocmapPath_Symlinkupdated for new semantics; not.Parallel()(correct — tests share globalos.Stdin). ✅Phase 2: Prior Review Verification
os.SameFileinode-race limitationModeSymlinkcheck reduces defense-in-depthIsRegular()check345f9a5os.Lstatvsos.Statafter EvalSymlinks--docmappathDoc consistency (adherence to docs)
fmt.Errorf("context: %w", err); no suppressed returns.defer func() { _ = f.Close() }()is explicit discard, not a missed check.validateDocmapPathuses early returns throughout; no deep nesting.TestCheckStaleDocs_PathTraversaluses table-driven subtests; single-scenario tests use non-table form appropriately.filepath,os,io,fmt,strings,bufio,flag).os.SameFile+io.LimitReaderare consistent with documented hardening intent.Assessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypass (issue #150)IsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Self-Review: PR #152
Self-review against
eb0ff3aa69Phase 1: Independent Findings
None — diff looks clean.
Reviewed
validatedocmap.goandvalidatedocmap_test.goas a stranger:filepath.EvalSymlinksbefore confinement check — correctly closes the dir-symlink bypass (issue #150) ✅fi.Mode()&os.ModeSymlinkbranch removed — correctly unreachable post-EvalSymlinks; Lstat comment reads "Lstat the resolved path for size and existence checks — EvalSymlinks guarantees no symlink components remain, so ModeSymlink can never be set." — accurate and clear ✅IsRegular()guard — correct placement after Lstat, rejects FIFOs/device nodes as documented ✅validateDocmapPathreturn type(string, error)— backward-safe signature change; all call sites updated ✅ParseDocMapConfigContentcalled with boundeddocmapDatabytes — eliminates check-then-use TOCTOU race ✅os.Lstat+ post-openf.Stat()+os.SameFile+io.LimitReader(f, maxDocmapBytes+1)— sound defense-in-depth; known OS-level inode-race limitation is documented in comment ✅defer func() { _ = f.Close() }()— follows project convention (explicit discard, no nolint suppressor) ✅*docmapFlag(user-supplied path) — correct UX; user sees the flag value they provided ✅os.LstatafterEvalSymlinks— intentionally consistent withcheckStaleDocs; technically equivalent toos.Statpost-EvalSymlinks ✅TestValidateDocmapPath_DirSymlinkBypassreproduces the attack vector;TestValidateDocmapPath_InRepoSymlinkAllowedcovers the positive in-repo-symlink behavioral change with explanatory comment ("validateDocmapPath calls filepath.EvalSymlinks internally, so the returned path is always the fully-resolved real path — it can never equal the symlink entry itself.");TestValidateDocmapPath_Symlinkupdated for new semantics; call site inTestValidateDocmapPath_DirSymlinkBypassupdated toif _, err := ...✅Phase 2: Prior Review Verification
defer f.Close() // nolint:errcheckdeviates from project conventiondefer func() { _ = f.Close() }()inc76e7dcresolved != symlinkPatheb0ff3af.Stat()+os.SameFilecheck added inc76e7dcos.SameFileTOCTOU inode-race limitation (known OS constraint)ModeSymlinkcheck reduces defense-in-depthos.Lstatvsos.Statafter EvalSymlinkscheckStaleDocs--docmappath not resolved pathDoc consistency (adherence to docs)
CONVENTIONS.md §Error Handling: Adheres — all errors wrapped withfmt.Errorf("context: %w", err);defer func() { _ = f.Close() }()is explicit discard per convention, not a missed check (code:validatedocmap.go:183)CONVENTIONS.md §Style (early returns): Adheres —validateDocmapPathuses early returns throughout; no deep nestingCONVENTIONS.md §Testing: Adheres — new testTestValidateDocmapPath_InRepoSymlinkAllowedfollows single-scenario non-table form appropriately; existingTestValidateDocmapPath_DirSymlinkBypassuses same formCONVENTIONS.md §Dependencies (strict allowlist): Adheres — no new imports; all standard library (filepath,os,io,fmt,strings,flag)SECURITY_REVIEW.md §Race conditions (TOCTOU): Adheres — pre/post-open stat +os.SameFile+io.LimitReaderdirectly address the TOCTOU concern documented in §Race conditions; known OS-level inode-race limitation documented as defense-in-depthAssessment: ✅ Clean
All MAJOR/MINOR findings from every prior review round have been addressed. Current HEAD carries only NITs — all resolved in code or accepted with documented rationale. Security fix is correct and well-tested:
filepath.EvalSymlinksbefore confinement closes the dir-symlink bypassIsRegular()prevents FIFO/device-node DoSos.SameFile+io.LimitReadersubstantially reduces TOCTOU surfaceTestValidateDocmapPath_DirSymlinkBypassreproduces the attack vectorTestValidateDocmapPath_InRepoSymlinkAllowedvalidates in-repo symlink policyReady for human review.
Finding #1 (ACK-NOT-VALID — Review #4814): The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and states "no action is required." The code comment already labels this guard as defense-in-depth. No change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed.Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. No code change needed. (Ref: fix plan comment 27994)Finding #1 (ACK-NOT-VALID): Acknowledged. The reviewer explicitly notes this is a known limitation of the
os.SameFilepattern and that "no action is required" — the code comment already labels it defense-in-depth. The narrow hardlink-swap window is a theoretical OS-level limitation, not a defect introduced here. No change needed.Finding #2 (ACK-NOT-VALID): Acknowledged. The reviewer confirms the ModeSymlink removal is correct — the check was genuinely unreachable after
filepath.EvalSymlinks, the comment is accurate, and the removal is a deliberate documented choice. No action needed.Finding #4 (ACK-NOT-VALID):
os.Lstatis intentionally used here for consistency withcheckStaleDocs, which also usesLstatto avoid implicit symlink-follow semantics. Switching toos.Statpost-EvalSymlinks would be equivalent at runtime but would create a mixedStat/Lstatpattern across the same file that could confuse future readers. The deliberate choice is noted in the comment.Finding #5 (ACK-NOT-VALID): User-facing error messages intentionally reference
*docmapFlag(the original--docmapvalue) rather than the resolved path. Showing the resolved path in errors would confuse users who passed a symlink — they would see a path they never specified. Using the flag value is the correct UX; the resolved path is used internally for all I/O.