Repository navigation
fix(server): avoid mutating the cached channels slice on includeEncrypted - #98
Merged
Merged
Conversation
… append handleChannels (cmd/server/routes.go) appended the encrypted-channels slice onto the result of db.GetChannels()/store.GetChannels() when ?includeEncrypted=true. Both GetChannels implementations return a mutex-guarded, TTL-cached slice directly to every caller with no defensive copy. When that cached slice still had spare capacity (cap > len), append wrote the encrypted rows into the spare capacity IN PLACE, mutating the shared backing array that other concurrent callers — and the cache itself — also hold a reference to. That is a real data race between concurrent includeEncrypted=true requests, detectable with `go test -race`, and silent memory corruption of the cache's unused capacity. Fix both append call sites to use a full slice expression (channels[:len(channels):len(channels)]) so append always allocates a fresh backing array instead of writing into the cache's spare capacity. GetChannels itself, its caching semantics, TTL, and locking are unchanged. Added cmd/server/channels_cache_append_test.go, which drives GET /api/channels through the real HTTP handler (both the s.db and s.store branches), seeds an encrypted channel so includeEncrypted=true actually reaches the buggy append, probes for a cached slice with spare capacity (bounded retries with padding rows), and asserts the cache's full backing array is untouched after concurrent includeEncrypted=true requests. A companion subtest fires many concurrent includeEncrypted=true requests against a pre-warmed cache and relies on `go test -race` to catch the race directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dborup
marked this pull request as ready for review
September 26, 2026 05:46
adminopenclaw8-sketch
pushed a commit
that referenced
this pull request
Sep 26, 2026
…-channel-proposals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
handleChannels(cmd/server/routes.go) appended the encrypted-channels slicedirectly onto the result of
db.GetChannels()/store.GetChannels()when?includeEncrypted=true. BothGetChannelsimplementations return amutex-guarded, TTL-cached slice directly to every caller with no defensive
copy. When that cached slice still had spare capacity (
cap > len),appendwrote the encrypted rows into the spare capacity in place, mutating the
shared backing array that other concurrent callers — and the cache itself —
also hold a reference to. That is a real data race between concurrent
includeEncrypted=truerequests, detectable withgo test -race, and silentmemory corruption of the cache's unused capacity.
Fix
Both append call sites in
handleChannels(thes.dbbranch and thes.storebranch) now use a full slice expression:This forces
appendto always allocate a fresh backing array instead ofwriting into the cache's spare capacity.
GetChannelsitself, its cachingsemantics, TTL, and locking are unchanged.
Tests
cmd/server/channels_cache_append_test.godrivesGET /api/channelsthroughthe real HTTP handler (both the
s.dbands.storebranches), seeds anencrypted channel so
includeEncrypted=trueactually reaches the buggyappend, probes for a cached slice with spare capacity (bounded retries with
padding rows), and asserts the cache's full backing array is untouched after
concurrent
includeEncrypted=truerequests. A companion subtest fires manyconcurrent
includeEncrypted=truerequests against a pre-warmed cache andrelies on
go test -raceto catch the race directly.go test -race -run TestChannelsCacheAppend -v ./...):the corruption assertion failed with an explicit before/after diff of the
cache's spare-capacity slot, and
-racereportedWARNING: DATA RACEatthe
appendcall insidehandleChannelsfor both the DB and store paths.-race, 3 consecutiveruns, plus the full
cmd/serversuite under-race.turns the DB-path test red while the store-path test stays green, and vice
versa — proving the test exercises both call sites independently.
Note
This same fix is also present inside the (separate, larger) shared-channels
feature branch, since it was originally found and fixed there as part of that
work before being extracted into this standalone branch. This PR should be
merged first, since the feature branch's history already contains this
exact fix as part of one of its commits.