code that rewrites code, out of sight
Macro & Mixin Misuse
Textual macros and order-dependent mixins inject behavior at a distance, so what runs is not what the call site appears to say.
Metaprogramming expands or mixes behavior the reader never sees.
01in the wild
In the wild
Unhygienic Macros
A textual macro substitutes without parentheses or types, so it expands into something you did not write.
example.c
/* SMELL: function-like macro with unguarded args */
#define SQUARE(x) x * x
SQUARE(1 + 2) /* expands to 1 + 2 * 1 + 2 = 5, not 9 */
/* SMELL: argument evaluated twice */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX(i++, j++) /* increments the winner twice */
/* RIGHT: a real (inline) function -- typed, one evaluation */
static inline int square(int x) { return x * x; }Macros are textual substitution with no type checking and no scope. An inline function gives the same speed with none of the traps.
// observed
macro: SQUARE(1+2) = 5 (surprise) func: square(1+2) = 9
example.js
// SMELL: a mixin that mutates the target's state
const Timestamped = (obj) => Object.assign(obj, {
touched: Date.now(), // impure + shared mutation
});
// RIGHT: compose explicitly, keep data and behavior apart
const withTimestamp = (obj, now) => ({ ...obj, touched: now });Object.assign mutates its target and the impure Date.now() hides inside the mixin. Composition keeps state explicit.
// observed
mixin: original object is mutated; time hidden inside pure: caller supplies `now`; original untouched
Clobbered by Mixin Order
Two mixins define the same member, so which one wins depends only on the order they were applied.
example.js
const Serializable = { save() { return "json"; } };
const Auditable = { save() { return "audit-log"; } };
// SMELL: last mixin silently wins; the other save() vanishes
const model = Object.assign({}, Serializable, Auditable);
model.save(); // "audit-log" -- did you mean to lose json?
// RIGHT: name the behaviors; never let them collide
const model = { saveJson: Serializable.save, audit: Auditable.save };Object.assign copies left to right, so a later mixin overwrites an earlier method with no warning.
// observed
mixed: save() -> 'audit-log' (json lost) explicit: saveJson() and audit() both reachable
example.py
# SMELL: cooperative methods clobber unless each calls super()
class A: # mixin
def setup(self): self.log = ["A"]
class B: # mixin
def setup(self): self.log = ["B"]
class W(A, B): # MRO is A, B -- B.setup never runs
pass
W().setup() # log == ['A']
# RIGHT: cooperate via super() so every mixin contributes
class A:
def setup(self): super().setup(); self.log.append("A")Python's MRO picks the first matching method; without super() chaining, later mixins are silently skipped.
// observed
naive: log == ['A'] (B skipped) super: log == ['B', 'A'] (both run)
02cross-pollination
Where this compounds
Nondeterminism
- Plugin Registration / Dispatch-Order Divergence × Async IO Misuse
Runtime Errors
- Stale API After Upgrade (AttributeError) × Version & Library Mismanagement
- Schema-Drift on Load: Old Payload Sets Unexpected Attributes × Insecure Deserialization
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.