two names for one value that changes under both

Aliasing & Mutable Defaults

A default created once and shared across calls, or two variables bound to the same object: mutate through one name and every other name sees it, because they were never separate values.

When two references share one mutable value, a write through either is a surprise to the other.

01in the wild

In the wild

Shared Mutable Defaults

A default value created once at definition time is silently shared across every call.

example.py
# CLASSIC TRAP: the default list is created once and shared
def append(item, bucket=[]):
    bucket.append(item)
    return bucket

append(1)   # [1]
append(2)   # [1, 2]  <-- not what anyone expects

# RIGHT: sentinel default, fresh value each call
def append(item, bucket=None):
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket
Default arguments are evaluated once at definition time. The mutable list leaks state between unrelated calls.
// observed
trap:  append(1)->[1], append(2)->[1, 2]
right: append(1)->[1], append(2)->[2]
example.js
// SMELL: one object literal shared as a default by every caller
const SHARED = {};
function tag(item, into = SHARED) {   // default is the SAME object each call
  into[item] = true;
  return into;
}
tag("a"); tag("b");   // { a: true, b: true } -- leaked across calls

// RIGHT: build a fresh container when none is passed
function tag(item, into) {
  into = into ?? {};
  into[item] = true;
  return into;
}
A shared default object accumulates state between unrelated calls. Default to a sentinel and allocate a fresh value inside.
// observed
shared: { a: true, b: true }
fresh:  { a: true }, { b: true }

Reference Aliasing

Two names bound to one object are not two values -- a write through one is visible through the other.

example.py
# SMELL: b is not a copy -- it's the same list
a = [1, 2, 3]
b = a
b.append(4)
print(a)        # [1, 2, 3, 4]  <-- a changed too

# RIGHT: copy when you mean a separate value
b = a.copy()    # or list(a) / a[:]
b.append(4)
print(a)        # [1, 2, 3]
Assignment binds a new name to the same object. Mutating through either name mutates the one shared list.
// observed
alias: a == [1, 2, 3, 4]
copy:  a == [1, 2, 3]
example.go
// SMELL: slices share the same backing array
a := []int{1, 2, 3}
b := a[:2]
b = append(b, 99)     // writes into a's backing array
// a is now [1 2 99]

// RIGHT: copy into a fresh slice when you need independence
b := make([]int, 2)
copy(b, a[:2])
b = append(b, 99)     // a is untouched
A re-slice points at the same backing array, so append can write through the alias until the array has to grow.
// observed
alias: a == [1 2 99]
copy:  a == [1 2 3]
02cross-pollination

Where this compounds

Runtime Errors
03weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.