Repository navigation
Land the context, connection-hook and reverse-cache race work on master - #2
Conversation
This test detects the race condition at cachinghandler.go:108 where reflect.DeepEqual reads filesystem internal state while another goroutine modifies it through file operations. The test currently FAILS with -race flag, demonstrating the bug. Run with: go test -race -run TestCachingHandlerReflectDeepEqualRace ./helpers/
Replace reflect.DeepEqual(candidate.f, f) with interface comparison (==). Problem: - reflect.DeepEqual traverses all internal fields of filesystem objects - This includes mutable maps (storage.files, storage.children) - When another goroutine modifies the filesystem (Create, Write, etc.), those maps are modified while reflect.DeepEqual is reading them - Result: DATA RACE Solution: - Use interface comparison: candidate.f == f - Interface == compares type and underlying pointer only - Does not read internal state, so no race - Semantically correct: we want to check if it's the same FS instance The test was adjusted to use separate filesystems per writer goroutine to avoid triggering unrelated races in memfs (which is not thread-safe).
This test detects the race condition where getReverseHandles returns a slice reference that can be modified while being iterated. The race occurs because: 1. getReverseHandles returns c.reverseHandles[path] (slice reference) 2. After RLock is released, searchReverseCache iterates over this slice 3. Concurrent appendReverseHandle/evictReverseCache modify the same slice The test currently FAILS with -race flag, demonstrating the bug. Run with: go test -race -run TestCachingHandlerSliceReferenceRace ./helpers/
Instead of returning a slice reference from getReverseHandles and iterating outside the lock, hold RLock for the entire iteration in searchReverseCache. Problem: - getReverseHandles returned c.reverseHandles[path] (direct reference) - After RLock was released, the slice could be modified by concurrent appendReverseHandle (append) or evictReverseCache (slice copy) - Result: DATA RACE when iterating in searchReverseCache Solution: - Hold RLock for entire iteration in searchReverseCache - Access c.reverseHandles[path] directly within the lock - Remove unused getReverseHandles function This is more efficient than copying the slice on every call, and writers (appendReverseHandle, evictReverseCache) will simply wait for the short iteration to complete. Safe because: activeHandles.Get() has its own internal locking (LRU).
PR SummaryMedium Risk Overview
Reviewed by Cursor Bugbot for commit f59b77c. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Bugbot's summary raises one thing I did not flag in the description, and I've checked it — it's right, and it's worth a reader's attention before this merges.
This branch fixes the first and leaves the second — its only change to The mechanism is the one To be straight about the evidence: this rests on reading the code plus the diagnosis the branch's own commit makes about the identical call site. I tried to pin it down with a two-connection Not fixing it here. This PR's head is
Neither blocks this merge; both are cheap, and I'll open them as one small PR once this is in. CI is green on this head (Build, CodeQL, Analyze). Generated by Claude Code |
Why
masterand the commit we actually run have diverged. Our consumer pins this fork atf59b77c, which is onadd-contextand is not an ancestor ofmaster— somasterhas never carried the seven commits below, and the RENAME/READLINK status fixes that just landed onmasterin #1 cannot reach us without converging the two.This PR does the converging: it merges
add-contextintomaster. After it lands,masteris the branch to pin, and there's one line of history again instead of two.I did not write these seven commits. I'm opening this to converge the branches, not claiming a line-by-line review of them — hence the draft. Flagging below what I think actually needs a reader.
What's in it
Seven commits, in three groups:
Context threading (
810c58b,f59b77c) — addscontext.Contextto fourHandlermethods and passes it through everyon*handler:Most of the 27-file diff is this, one line per handler.
Connection hooks (
8b8e0fb,f59b77c) — additiveServer.OnConnect/Server.OnDisconnect, called fromconn.serve.OnConnectreturns both the context and thenet.Conn, so a caller can wrap the connection as well as the context.Reverse-cache race fixes (
d9b8781…3776384) —searchReverseCachenow holdsRLockacross the whole iteration rather than copying the slice out under a brief lock, and compares filesystems with==instead ofreflect.DeepEqual. With tests.What needs a reader
The
Handlerchange is breaking for downstream consumers. Anything implementingHandleroutside this repo must add the parameters. That also means these commits are not cherry-pickable towillscott/go-nfswithout coordination — worth deciding separately whether we ever propose it upstream, since it widens the gap between this fork and upstream.reflect.DeepEqual→==insearchReverseCacheis a semantic change, not just a race fix.DeepEqualtreated two distinct-but-structurally-identical filesystems as the same;==matches only the same instance. For a reverse cache keyed on a live filesystem that reads like the intended semantics, and droppingDeepEqualgenuinely fixes the race (it walked mutable maps while other operations wrote them). Two caveats worth a conscious nod:==panics if the dynamic type is non-comparable — abilly.Filesystemimplemented as a value struct containing a map, slice or func would take down the connection. Every implementation in play here is a pointer type (*memfs.Memory,osfs's*ChrootOS/*BoundOS, ours), so this is latent rather than live, but it's a sharper edge thanDeepEqualhad.server.gocarries a stray blank-line-only change. Harmless, just noise in the diff.The
ctxparameters are currently unused inCachingHandler's three methods — that's the point (they exist for implementations that do use them), but it's why the linter stays quiet about them.Verification
I verified the merge locally rather than assuming GitHub's:
ort, no manual resolution) — the only overlap with Map RENAME and READLINK failures to the NFS statuses that name them #1 isnfs_onrename.go/nfs_onreadlink.go, and the changes are on different lines.add-context's ctx-carryingHandler, and its diff against the currently-pinnedf59b77cis exactly the five files from Map RENAME and READLINK failures to the NFS statuses that name them #1 — so no API surface a consumer could trip on changes here.go build ./...clean;go test -race ./...green across both packages, including Map RENAME and READLINK failures to the NFS statuses that name them #1's new tests running on top of the ctx interface.gofmtclean, andgolangci-lintv1.64.8 (the version this repo'sGoworkflow installs) reports zero findings.Merging
Prefer a merge commit over a squash here: the seven commits have more than one author, and squashing them into one loses that attribution. #1 was squash-merged, which was right for a single-author PR; this one isn't that.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AeywBLV66AeSH8WuDfvv7Q
Generated by Claude Code