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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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