Skip to content

Recursive custom syntax definition can crash MacOS due to OOM #3878

Description

@dandavison

Hello, I was trying to make bat highlight files with uv run shebangs and an agent wrote a recursive syntax definition that crashes MacOS due to unbounded memory consumption. I thought you'd probably want to protect against this here or in syntect.

What steps will reproduce the bug?

The agent wrote this:

DON'T ACTUALLY USE THIS

  %YAML 1.2
  ---
  name: Python (uv script)
  file_extensions: []
  scope: source.python
  first_line_match: '^#!.*\buv\s+run\b.*--script\b'
  contexts:
    main:
      - include: scope:source.python
Here's an agent-written repro

A custom syntax whose scope: collides with the syntax it includes causes unbounded memory allocation (kernel panic)

⚠️ Do not run the repro below unsandboxed. On my machine bat allocated ~6 GB/s and twice took down the whole OS (macOS watchdog kernel panic, 48 GB RAM). The repro as written is bounded with timeout.

Summary

If a custom .sublime-syntax declares a scope: that is the same as the scope it pulls in via include: scope:..., the include resolves to the syntax itself. bat cache --build accepts this without complaint, and then highlighting any file that maps to that syntax allocates without bound until the machine dies. No output is ever produced.

I hit this accidentally — the offending 8-line syntax file was written by an LLM agent trying to add #!/usr/bin/env -S uv run --script shebang detection, and it took my laptop down twice before I found it. A malformed syntax definition shouldn't be able to do that.

Versions

  • bat 0.26.1 (Homebrew bottle), which vendors syntect 5.3.0
  • macOS 26.6 (25G72), arm64 (M5 Pro, 48 GB)

Repro

R=$(mktemp -d); mkdir -p "$R/config/syntaxes" "$R/cache"

cat > "$R/config/syntaxes/loop.sublime-syntax" <<'YAML'
%YAML 1.2
---
name: Loopy Python
file_extensions: [loopy]
scope: source.python              # <-- same scope as the bundled Python syntax
contexts:
  main:
    - include: scope:source.python  # <-- therefore resolves to *this* syntax
YAML

bat cache --build --source "$R/config" --target "$R/cache"   # succeeds, no warning

printf 'x = 1\n' > "$R/t.loopy"

# BOUNDED: kill after 2s. Without the timeout this eats all RAM.
BAT_CACHE_PATH="$R/cache" timeout 2 bat --color=always --paging=never "$R/t.loopy"

Sampling RSS of the bat process during those 2 seconds on a one-line input file:

t=250ms:    10 MB
t=500ms:  2388 MB
t=750ms:  3988 MB
t=1000ms: 5568 MB
t=1250ms: 7118 MB

It never emits the highlighted line. Left alone it reached 26.7 GB and 41.1 GB resident in two separate incidents before the kernel watchdog panicked (Compressor Info: 100% of compressed pages limit (BAD)), taking the machine down.

Control

Changing only the scope: line so the include is no longer self-referential makes it work correctly and instantly:

scope: source.python.uv
contexts:
  main:
    - include: scope:source.python

Expected

bat cache --build should reject (or at minimum warn about) a syntax whose context graph reaches itself through include: scope:, rather than producing a cache that makes bat unrunnable on the affected file types. Failing that, the highlighter should bail out with an error instead of allocating without bound.

Note that syntect 5.3.0 already contains a fix for #597 ("infinite loop when a syntax definition continually pushes contexts without consuming any input"), so this appears to be a distinct case that the existing guard doesn't cover.

What happens?
OOMs MacOS

What did you expect to happen instead?
Something less dramatic

How did you install bat?
homebrew


bat version and environment
bat 0.26.1

Software version

bat 0.26.1

Operating system

  • OS: macOS 26.6
  • Kernel: 25.6.0

Command-line

bat --style header,grid --theme GitHub --diagnostic 

Environment variables

BAT_CACHE_PATH=<not set>
BAT_CONFIG_PATH=<not set>
BAT_OPTS=<not set>
BAT_PAGER=<not set>
BAT_PAGING=<not set>
BAT_STYLE=<not set>
BAT_TABS=<not set>
BAT_THEME=GitHub
COLORTERM=truecolor
LANG=C.UTF-8
LC_ALL=en_US.UTF-8
LESS=-IR
MANPAGER=<not set>
NO_COLOR=<not set>
PAGER=<not set>
SHELL=/opt/homebrew/bin/zsh
TERM=tmux-256color
XDG_CACHE_HOME=/Users/dan/.cache
XDG_CONFIG_HOME=/Users/dan/.config

System Config file

Could not read contents of '/etc/bat/config': No such file or directory (os error 2).

Config file

Could not read contents of '/Users/dan/.config/bat/config': No such file or directory (os error 2).

Custom assets metadata

Could not read contents of '/Users/dan/.cache/bat/metadata.yaml': No such file or directory (os error 2).

Custom assets

'/Users/dan/.cache/bat' is empty

Compile time information

  • Profile: release
  • Target triple: aarch64-apple-darwin
  • Family: unix
  • OS: macos
  • Architecture: aarch64
  • Pointer width: 64
  • Endian: little
  • CPU features: aes,crc,dit,dotprod,dpb,dpb2,fcma,fhm,flagm,fp16,frintts,jsconv,lor,lse,neon,paca,pacg,pan,pmuv3,ras,rcpc,rcpc2,rdm,sb,sha2,sha3,ssbs,vh
  • Host: aarch64-apple-darwin

Less version

> less --version 
less 668 (POSIX regular expressions)
Copyright (C) 1984-2024  Mark Nudelman

less comes with NO WARRANTY, to the extent permitted by law.
For information about the terms of redistribution,
see the file named README in the less distribution.
Home page: https://greenwoodsoftware.com/less

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions