Skip to content

Guard against stack overflow on deeply nested input#229

Open
bartveneman wants to merge 1 commit into
mainfrom
claude/add-recursion-guard
Open

Guard against stack overflow on deeply nested input#229
bartveneman wants to merge 1 commit into
mainfrom
claude/add-recursion-guard

Conversation

@bartveneman

Copy link
Copy Markdown
Member

Summary

  • print_list, print_selector_list, print_block, print_rule and print_atrule all recurse with the input's nesting depth and had no guard. Confirmed a crash with ~2,500 nested rules or deeply nested calc()/parentheses — an opaque, unhandled RangeError: Maximum call stack size exceeded.
  • Added an explicit recursion-depth counter (enter_recursion, capped at 250, tracked via try/finally so it always unwinds correctly on both success and error paths) so the printer throws a clear format-css:-branded RangeError instead.
  • For even deeper input (~2,000+ levels), the underlying parse() call from @projectwallace/css-parser can itself exceed the call stack before our own guard ever runs, since parsing happens first. Wrapped that call so any RangeError from parsing is normalized into the same kind of clear, catchable error rather than leaking the dependency's raw crash.
  • Verified via the CLI that a pathological input now exits with a clean one-line error message instead of a stack trace.

Test plan

  • vitest run — all 258 tests pass (added 4 new cases covering deep rule nesting, deep paren nesting, recovery after a rejected input, and that realistic nesting depths still format fine)
  • tsc --noEmit — no type errors
  • oxlint / oxfmt --check — clean
  • Manually verified via the CLI that deeply nested input now produces a one-line error instead of a stack trace

Generated by Claude Code

print_list, print_selector_list, print_block, print_rule and
print_atrule recurse with the input's nesting depth and had no
guard, so pathological input (thousands of nested rules, or deeply
nested calc()/parentheses) crashed with an opaque, unhandled
"Maximum call stack size exceeded" RangeError.

Add an explicit recursion-depth guard, well below the real stack
limit, so the printer throws a clear format-css-branded RangeError
instead. Also wrap the initial parse() call: for extremely deep
input the underlying parser can itself exceed the call stack before
our own guard ever runs, so normalize that failure into the same
kind of clear error.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Duplicate Dependencies (found: 22, threshold: 1)

📦 Package 📋 Versions
@babel/parser
2 versions
  • root@
    • @vitest/coverage-v8@4.1.9
      • magicast@0.5.3
        • @babel/parser@7.29.7

  • root@
    • tsdown@0.22.3
      • rolldown-plugin-dts@0.26.0
        • @babel/generator@8.0.0
          • @babel/parser@8.0.0

@babel/types
2 versions
  • root@
    • @vitest/coverage-v8@4.1.9
      • magicast@0.5.3
        • @babel/parser@7.29.7
          • @babel/types@7.29.7

  • root@
    • tsdown@0.22.3
      • rolldown-plugin-dts@0.26.0
        • @babel/generator@8.0.0
          • @babel/parser@8.0.0
            • @babel/types@8.0.0

@babel/helper-string-parser
2 versions
  • root@
    • @vitest/coverage-v8@4.1.9
      • magicast@0.5.3
        • @babel/parser@7.29.7
          • @babel/types@7.29.7
            • @babel/helper-string-parser@7.29.7

  • root@
    • tsdown@0.22.3
      • ...
        • @babel/parser@8.0.0
          • @babel/types@8.0.0
            • @babel/helper-string-parser@8.0.0

@babel/helper-validator-identifier
2 versions
  • root@
    • @vitest/coverage-v8@4.1.9
      • magicast@0.5.3
        • @babel/parser@7.29.7
          • @babel/types@7.29.7
            • @babel/helper-validator-identifier@7.29.7

  • root@
    • tsdown@0.22.3
      • ...
        • @babel/parser@8.0.0
          • @babel/types@8.0.0
            • @babel/helper-validator-identifier@8.0.2

@rolldown/binding-wasm32-wasi
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-wasm32-wasi@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-wasm32-wasi@1.1.5

picomatch
2 versions
  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • tinyglobby@0.2.17
          • fdir@6.5.0
            • picomatch@4.0.4

  • root@
    • @vitest/coverage-v8@4.1.9
      • vitest@4.1.9
        • @vitest/mocker@4.1.9
          • vite@8.1.4
            • picomatch@4.0.5

rolldown
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • vitest@4.1.9
        • @vitest/mocker@4.1.9
          • vite@8.1.4
            • rolldown@1.1.5

@oxc-project/types
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @oxc-project/types@0.137.0

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @oxc-project/types@0.139.0

@rolldown/binding-android-arm64
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-android-arm64@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-android-arm64@1.1.5

@rolldown/binding-darwin-arm64
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-darwin-arm64@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-darwin-arm64@1.1.5

@rolldown/binding-darwin-x64
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-darwin-x64@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-darwin-x64@1.1.5

@rolldown/binding-freebsd-x64
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-freebsd-x64@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-freebsd-x64@1.1.5

@rolldown/binding-linux-arm-gnueabihf
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-arm-gnueabihf@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-arm-gnueabihf@1.1.5

@rolldown/binding-linux-arm64-gnu
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-arm64-gnu@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-arm64-gnu@1.1.5

@rolldown/binding-linux-arm64-musl
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-arm64-musl@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-arm64-musl@1.1.5

@rolldown/binding-linux-ppc64-gnu
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-ppc64-gnu@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-ppc64-gnu@1.1.5

@rolldown/binding-linux-s390x-gnu
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-s390x-gnu@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-s390x-gnu@1.1.5

@rolldown/binding-linux-x64-gnu
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-x64-gnu@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-x64-gnu@1.1.5

@rolldown/binding-linux-x64-musl
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-linux-x64-musl@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-linux-x64-musl@1.1.5

@rolldown/binding-openharmony-arm64
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-openharmony-arm64@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-openharmony-arm64@1.1.5

@rolldown/binding-win32-arm64-msvc
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-win32-arm64-msvc@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-win32-arm64-msvc@1.1.5

@rolldown/binding-win32-x64-msvc
2 versions
  • root@
    • tsdown@0.22.3
      • rolldown@1.1.3
        • @rolldown/binding-win32-x64-msvc@1.1.3

  • root@
    • @vitest/coverage-v8@4.1.9
      • ...
        • vite@8.1.4
          • rolldown@1.1.5
            • @rolldown/binding-win32-x64-msvc@1.1.5

💡 To find out what depends on a specific package, run: pnpm -r why example-package

⚠️ Package Size Increase

📦 Package 📏 Base Size 📏 Source Size 📈 Size Change
@projectwallace/format-css 8.6 kB 8.8 kB +236 B

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants