Not safe, do not pass go, do not collect $200, absolutely do not use.
[1] https://github.com/sirgallo/cmapv2/blob/280e3017ae4ba212f6f8...
---
CopyNode broken
`CopyNode` duplicates only the parent; every child pointer is still shared
nodeCopy.setChildren(make([]*node, len(n.children)))
copy(nodeCopy.children, n.children) // pointers reused
https://github.com/sirgallo/cmapv2/blob/main/node.go#L11-L17Any later mutation (for example `setValue`) writes through those shared pointers, so readers and historical snapshots are modified concurrently -- invalid, data race, memory model violation
---
Bitmap corruption
`SetBit` uses XOR rather than “set”:
func SetBit(bitmap uint32, position int) uint32 { return bitmap ^ (1 << position) }
https://github.com/sirgallo/cmapv2/blob/main/utils.go#L41-L4...Calling it twice on the same index flips the bit back to 0. During branch-creation on insert and during delete, this function is invoked multiple times on the same index, clearing a bit that should remain set and leaving orphaned children.
---
Invalid assumptions re: interior pointers
Only the root pointer is read with `atomic.LoadPointer`. All deeper fields like `children[pos]`, `bitmap`, and the byte-slice keys/values, are accessed directly after a successful CAS. Readers therefore race with writers that mutate these fields in place -- race condition, memory model violation, etc.
pos := cMap.getPosition(node.Bitmap(), hash, level)
if node.Child(pos).IsLeaf() && bytes.Equal(key, node.Child(pos).Key()) {
return node.Child(pos).Value()
}
https://github.com/sirgallo/cmapv2/blob/main/operation.go#L5...---
All xxxRecursive functions rely on those invalid interior pointer assumptions
Sequence in `putRecursive` / `deleteRecursive` is
1. `curr := atomic.LoadPointer(ptr)`
2. Build `nodeCopy`
3. Recurse; grandchildren are mutated in place
4. `atomic.CompareAndSwap(ptr, curr, nodeCopy)`
https://github.com/sirgallo/cmapv2/blob/main/operation.go#L1...If another goroutine has already swapped in a different copy of `curr` (and mutated it) the CAS still succeeds because the pointer value is unchanged, merging incompatible sub-tries and corrupting the data
---
Use-after-free in sync.Pool
On CAS failure the freshly built `nodeCopy` is immediately returned to a `sync.Pool` -- undefined behavior
cMap.pool.PutNode(nodeCopy) // may race with outstanding readers
https://github.com/sirgallo/cmapv2/blob/main/operation.go#L1...Other goroutines still holding references to that node can now access a reclaimed object, oops.
---
K/V Aliasing
Keys and values (both []byte slices, which are not safe for concurrent r/w access) are stored by reference, a mistake:
n.setKey(key)
n.setValue(value)
If the caller mutates those slices later (or concurrently in another goroutine), data races ahoy---
Reader panics, etc.
- `getRecursive` accesses `children[pos]` without bounds or nil checks, concurrent shrink can make `pos` invalid
- `GetIndex` allows a negative `shiftSize` once `level >= 7` with `chunkSize = 5`, producing nonsense indices and potential slice-out-of-boundsYou must clone the slice on both write and read, right?
I get that cloning incurs a memory allocation and a copy operation, but this is the price for safety when concurrent access is possible or your data may be bodified outside your structure.
You could probably intern immutable keys, or avoid storing if keys already exist and are immutable, or use an object pool (like sync.Pool) to reduce allocations if this happens at scale. Anything else I am missing?
I haven't looked at the code, but that doesn't make sense to me. If you can't read the slice safely, you also can't clone it safely.
Like okay, I read "both []byte slices, which are not safe for concurrent r/w access", but then, what is the solution? If the claim is indeed true.
- Make sure no one mutates the slice ever, making it safe to read
- Guarding the slice behind a mutex, requiring anyone who reads or writes it to lock the mutex first
- Using some kind of thread-safe slice implementation
On a mutation, I do a complete node copy where I also copy the key/value slices. When I set a child node for the first time or update a child, I create a branch new leaf node with a copy of the key/value. This way previous nodes maintain the original copy.
For example this code
https://github.com/sirgallo/cmapv2/blob/6bcaa0253b1b0b261e8a...
and in particular its use of this code
https://github.com/sirgallo/cmapv2/blob/6bcaa0253b1b0b261e8a...
is still completely unsound.
Looking at *only this code path* -- and there are *many more* --
---
Put
- Snapshots the current root pointer with atomic.LoadPointer
- Makes an updated root pointer via putRecursive, given the snapshotted root pointer
- Spins on a CAS of the root ptr and the updated ptr with runtime.Gosched() between attempts
---
atomic.LoadPointer isn't a real snapshot
- It's atomic only over the root ptr, not any interior field
- Those interior fields are mutated in-place via e.g. setBitmap, setChild, etc.
- Any goroutine can see partial data, violating the memory model, etc.
---
putRecursive is unsound
- copyNode performs a shallow copy, child pointers are shared, subsequent setChild, extendTable, etc. mutate nodes other goroutines can still hold -- this is a fundamental bug that seems to remain un-addressed from the previous review
- Those mutations use plain writes (no atomics/locks/etc.) -- data race, memory model violation, etc.
- Get later returns the internal []byte slice directly -- data race, memory model violation, etc.
- Newly created nodes are cast to unsafe.Pointer without an atomic store, bypassing the write barrier required by the GC
---
That compareAndSwap is unsound
- It compares only the root pointer, a shallow copy
- After a successful CAS other writers can still mutate any shared children (see above), so readers following any shared path see data races, memory model violations, etc.
- The retry loop can livelock, details elided
---
The implementation still seems to confuse "atomic pointer swap" with "atomic update of a complex, shared value", misunderstands the requirements of the Go memory model, and consistently mis-uses unsafe.Pointer.
tl;dr here is probably to just stop using package unsafe altogether, until you have some time to properly understand its semantics, requirements, and limitations...
But really, the premise of using a shared map with concurrent readers and writers seems like a good generator of hard-to-reproduce bugs. IMHO shared-nothing (when feasible) is much easier to reason about, and possibly do periodic merging of thread-local updates, but I would avoid concurrent updates entirely (in particular if 2 threads race to update the same key... that goes to deeper design issues in the application).
sirgallo•7mo ago
pstuart•7mo ago
sirgallo•7mo ago
latchkey•7mo ago
https://github.com/cornelk/hashmap
jzelinskie•7mo ago
sirgallo•7mo ago
latchkey•7mo ago
sirgallo•7mo ago