Skip to content

analogous() and monochromatic() loop unboundedly on negative or fractional counts, exhausting memory #280

Description

@BigAchiever

analogous(results) and monochromatic(results) decrement their loop counter and test it for truthiness, so a counter that never lands exactly on 0 never terminates. Each iteration pushes a tinycolor instance, so the process exhausts its heap rather than hanging quietly.

// tinycolor.js:659  analogous
for (hsl.h = (hsl.h - (part * results >> 1) + 720) % 360; --results;) { ... }

// tinycolor.js:673  monochromatic
while (results--) { ... }

From results = -1 the sequence is -2, -3, -4, …. From results = 1.5 it is 0.5, -0.5, -1.5, …. Neither reaches 0.

Reproduction

Node 20, current main (npm/cjs/tinycolor.js), heap capped so it fails fast:

$ node --max-old-space-size=256 -e "require('./tinycolor.js')('red').analogous(-1)"
<--- Last few GCs --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
$ echo $?
134

Confirmed for both functions at -1, 1.5 and 0.5 — six cases, all exit 134. 0, 2 and null behave normally (0 and null fall back to the default of 6).

Why this seems worth guarding

polyad() already validates the same shape of input:

if (isNaN(number) || number <= 0) {
  throw new Error("Argument to polyad must be a positive number");
}

So the hazard is recognised in one of the three combination functions but not the other two. Any caller passing a user-supplied count through to analogous() or monochromatic() — a palette-size field in a colour tool, say — has an unauthenticated way to exhaust the process heap.

Suggested fix

Either apply the guard polyad() uses, or coerce with Math.max(1, Math.floor(results)). The first is consistent with existing behaviour; the second avoids breaking any caller currently passing a fractional value that happens to work today.


Found while building a Rust port of TinyColor and differentially fuzzing it against the original — the port returns a finite list for these inputs, and the disagreement surfaced the loop. Happy to open a PR if a preferred approach is indicated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions