Slice modification might be incorrect
| Vulnerability potential | Low |
| DDoS potential | None |
The slice after modification might have unexpected value
Impact
Modifying a Go slice — especially via append — can have surprising effects
because the slice header and its backing array are decoupled. If there is spare
capacity, append writes in place and any other slice sharing that backing
array sees the change; if capacity is exhausted, append allocates a new array
and the original is left untouched. The same line of code therefore behaves
differently depending on runtime capacity, producing data that is sometimes
shared and sometimes not. The result is wrong values, lost or duplicated
elements, and aliasing bugs that pass tests on small inputs and fail on large
ones.
Vulnerability potential
This is primarily a correctness defect; Go’s memory safety prevents corruption.
- In a server that reuses a backing buffer across requests, an in-place modification can cause one client’s data to appear in another’s response — an information-disclosure bug.
appendthat unexpectedly mutates shared state can break invariants relied on elsewhere, occasionally enabling logic-level security bypasses (e.g. a filtered slice that still aliases unfiltered data).- Misjudged growth can also cause excess allocation/retention, a mild resource-exhaustion concern.
Technical details
append(s, x) returns a slice; you must assign the result back (s = append(s,
x)) because the header may change. Whether the underlying array changes depends
on cap(s):
In-place vs reallocating append
If len(s) < cap(s), append stores into the existing array and returns a
header pointing at the same memory — visible to every alias. If len(s) ==
cap(s), it allocates a larger array (growth is roughly geometric), copies, and
returns a header pointing at fresh memory, so aliases are decoupled.
The classic aliasing trap
b := a[:2]; b = append(b, x) overwrites a[2] when a has capacity,
silently clobbering an element of a. Guard against it with the three-index
form a[:2:2], which sets capacity to 2 so the next append is forced to
reallocate.
Modifying while ranging
for i := range s { s = append(s, ...) } is evaluated against the original
length, but element writes through an aliased slice during iteration can produce
stale or duplicated reads.
Catching the issue
Tooling
go vet reports the common x = append(y, ...)-without-using-result and
append-result-not-assigned mistakes; staticcheck/golangci-lint add more
slice-aliasing diagnostics.
Race detector
go test -race catches concurrent in-place modifications of a shared backing
array.
Tests
Test with inputs that both fit and exceed initial capacity, since the bug only
appears in one regime. go test -fuzz is effective at hitting the boundary.
Practice
Always assign append’s result; use slices.Clone or copy to get an
independent slice before modifying; use the three-index slice form when passing
a sub-slice that the callee might append to.
How to reproduce
Run the program; observe that appending to a sub-slice overwrites an element of
the original because spare capacity lets append write in place.
package main
import "fmt"
func main() {
a := make([]int, 3, 5) // len 3, cap 5 — spare capacity
a[0], a[1], a[2] = 1, 2, 3
b := a[:2] // shares backing array, cap is still 5
b = append(b, 99) // writes into a[2] in place (capacity available)
fmt.Println(b) // [1 2 99]
fmt.Println(a) // [1 2 99] <- a[2] was 3, now clobbered
}