Summary
pbc segfaults partway through parsing a large source tree when the process
has the common default 8 MB stack. It is a C-stack overflow, not an OOM, and
raising RLIMIT_STACK is enough to get past it.
I lost a fair amount of time treating this as a memory-size problem, so the
elimination evidence is below in case it saves someone else the trip.
Reproduction
Parsing a tree of ~5,500 .sr* files (a mix of .srd, .srw, .sru) inside
a container with the default 8 MB stack:
pbc --input <tree> --db out.duckdb --sql-worker-python python
-> Segmentation fault (core dumped), exit 139
It dies during phase A0, after a different number of files each run:
816 / 1421 / 2476 / 2577 files parsed before the crash
It is not memory, and not parallelism
Same input, varying only the RTS flags — every one segfaults:
| flags |
result |
-M4G -N4 |
SIGSEGV, 41s |
-M8G -N4 |
SIGSEGV, 41s |
-M12G -N4 |
SIGSEGV, 45s |
-M16G -N1 |
SIGSEGV, 46s |
-M16G -N2 |
SIGSEGV, 13s |
-M16G -N4 |
SIGSEGV, 44s |
A genuine heap overflow aborts cleanly with Heap exhausted, and -M4G would
certainly hit that ceiling first if this were one. The crash ignores the heap
cap entirely, and -N1 rules out a data race in the parallel path.
It is the stack
Changing nothing but RLIMIT_STACK:
| stack |
result |
| 8 MB (default) |
SIGSEGV after 816–2577 of 5473 files |
| unlimited |
5446 of 5473 files parsed |
This also explains the non-determinism: how deep the parser recurses depends
on which object it is on when cumulative depth crosses the limit, so the
crash lands on a different file each run, and larger trees fail more often
simply because they offer more chances to hit a deep object.
Workaround
docker run --ulimit stack=536870912:536870912 ...
A finite value rather than unlimited matters: glibc sizes a new thread's
stack from RLIMIT_STACK, but falls back to a fixed 8 MB default when it is
unlimited, so unlimited fixes only the main thread and leaves -N workers
at the broken size.
What I could not narrow down
I could not build a minimal synthetic reproducer. These all parse cleanly at
8 MB, so the deep recursion does not appear to be in expression parsing:
- a 50,000-term flat concatenation (
"a" + "b" + ...)
- 30,000-deep explicit paren nesting
- 4,000 trivially-simple generated window objects
Splitting a real tree by file type, at 8 MB stack, also did not isolate it:
the .srd files alone parsed clean, and the .sru files alone parsed clean.
The trigger seems to need a mix. I am not able to share the tree that
reproduces it.
Offer: pbc ships stripped, so the cores I have are addresses only. I am
happy to rebuild with --disable-executable-stripping --enable-debug-info
and post a backtrace — that would presumably identify the recursive function
outright and save you reproducing this at all. Say the word and I will run it.
I can also test any candidate patch against the tree that reproduces it, which
gets you a verification path without needing the source.
Environment
pbc built from haskell:9.10.2-bullseye, GHC 9.10.2, per the versions in
the project's own CI workflow
- Linux x86_64, 64 GB RAM, 16 cores
- Container memory limit well above peak usage in every run above
Summary
pbcsegfaults partway through parsing a large source tree when the processhas the common default 8 MB stack. It is a C-stack overflow, not an OOM, and
raising
RLIMIT_STACKis enough to get past it.I lost a fair amount of time treating this as a memory-size problem, so the
elimination evidence is below in case it saves someone else the trip.
Reproduction
Parsing a tree of ~5,500
.sr*files (a mix of.srd,.srw,.sru) insidea container with the default 8 MB stack:
It dies during phase A0, after a different number of files each run:
It is not memory, and not parallelism
Same input, varying only the RTS flags — every one segfaults:
-M4G -N4-M8G -N4-M12G -N4-M16G -N1-M16G -N2-M16G -N4A genuine heap overflow aborts cleanly with
Heap exhausted, and-M4Gwouldcertainly hit that ceiling first if this were one. The crash ignores the heap
cap entirely, and
-N1rules out a data race in the parallel path.It is the stack
Changing nothing but
RLIMIT_STACK:This also explains the non-determinism: how deep the parser recurses depends
on which object it is on when cumulative depth crosses the limit, so the
crash lands on a different file each run, and larger trees fail more often
simply because they offer more chances to hit a deep object.
Workaround
A finite value rather than
unlimitedmatters: glibc sizes a new thread'sstack from
RLIMIT_STACK, but falls back to a fixed 8 MB default when it isunlimited, so
unlimitedfixes only the main thread and leaves-Nworkersat the broken size.
What I could not narrow down
I could not build a minimal synthetic reproducer. These all parse cleanly at
8 MB, so the deep recursion does not appear to be in expression parsing:
"a" + "b" + ...)Splitting a real tree by file type, at 8 MB stack, also did not isolate it:
the
.srdfiles alone parsed clean, and the.srufiles alone parsed clean.The trigger seems to need a mix. I am not able to share the tree that
reproduces it.
Offer:
pbcships stripped, so the cores I have are addresses only. I amhappy to rebuild with
--disable-executable-stripping --enable-debug-infoand post a backtrace — that would presumably identify the recursive function
outright and save you reproducing this at all. Say the word and I will run it.
I can also test any candidate patch against the tree that reproduces it, which
gets you a verification path without needing the source.
Environment
pbcbuilt fromhaskell:9.10.2-bullseye, GHC 9.10.2, per the versions inthe project's own CI workflow