TL;DR
- We fuzzed BBEdit 15.5.5 language modules in-process and found two local denial-of-service issues in parser/tokenizer code paths.
- Finding #1 (CWE-674): deeply nested Java generics trigger uncontrolled recursion leading to stack exhaustion in JavaFunctionScanner::scanInterface.
- Finding #2 (CWE-835): a crafted Lasso file containing NUL bytes stalls tokenization in LassoTokenizer::findNextToken, causing an infinite loop and 100% CPU UI hang.
- Bare Bones Software reproduced quickly and fixed both within days.
The threat model is a user opening an attacker-supplied source file, or BBEdit restoring a previously opened malicious file during session restoration.
Why We Picked BBEdit
At Cipher Security Labs, we look for mature software that processes complex, attacker-controlled input at high frequency. Text editors are ideal for this kind of research. They parse untrusted code files all day long across many grammars, under strict latency constraints, so users can keep typing.
BBEdit fit that profile perfectly: long-lived codebase, broad real-world usage on macOS, and rich language support implemented in native modules. That combination usually means two things:
- A large parser attack surface.
- That kind of architecture can leave parser edge cases that are difficult to exercise through normal QA alone.
During reconnaissance, we found a broad set of language modules under BBEdit's plugin architecture. For vulnerability research, that is a target-rich environment. We were not trying to produce sensational claims or inflate impact. The objective was straightforward: identify robustly reproducible parser failures, document them with evidence, and coordinate professionally with the vendor.
Architecture Recon: Why .bblm Modules Are Fuzz-Friendly
BBEdit language modules (.bblm) are Mach-O bundles loaded by the application. Practically, this means each language parser can be treated as a loadable target:
- Open module with dlopen.
- Resolve exported entry point (JavaMachO, LassoMachO, etc.).
- Provide the expected parameter/callback structures (BBLMParamBlock and BBLMCallbackBlock-style interface).
- Execute parser modes repeatedly with mutated buffers.
That is much easier than trying to fuzz the full GUI app end-to-end. A monolithic GUI fuzz loop pays heavy startup and state-management costs every iteration. Module-level execution gives tighter control, better determinism, and dramatically higher throughput.
This architecture choice drove the rest of the campaign.
Building the In-Process Fuzzer
Our first harness design used process-spawn execution. It worked, but performance was poor: around ~80 executions/sec per core in practice, once you factor in repeated loader overhead. For parser fuzzing, that is too slow.
We then switched to an in-process model in bbedit_vr/harnesses/bblm_fuzz_inproc.mm:
- Load the target .bblm bundle once at startup.
- Enter a tight mutation loop in memory.
- Reconstruct fresh parameter blocks per iteration.
- Invoke module entry directly.
- Catch crashes/timeouts externally and save repro inputs.
The throughput gain was dramatic: roughly ~2,000 execs/sec per core, and ~20,000 aggregate on a 10-core Apple Silicon setup.
The supervisor (bbedit_vr/harnesses/bblm_supervisor.py) enforced per-run alarms to capture and persist hangs as last_input artifacts. That was essential for debugging non-crashing failures.
Researcher Tip: The CFPreferences Trap
Early in harnessing, some modules crashed in CoreFoundation paths (including __CFGetTSDCreateIfNeeded) before meaningful parser execution. Root cause: our bare CLI context lacked expected app identity metadata.
The fix was to run the harness inside a minimal .app wrapper with an Info.plist identity compatible with expected preferences lookups. Once that shim was in place, those unrelated startup crashes disappeared, and parser fuzzing became stable.
This detail is easy to miss and can waste hours if you assume every early crash is a target bug.
Finding #1: Java Parser Stack Exhaustion (CWE-674)
What Triggered It
Target: Java.bblm in function-scan mode (funcs).
Early in fuzzing, we observed a stable crash pattern. After minimizing, the trigger was a malformed Java snippet with aggressively nested generic bounds, centered around a repeating pattern similar to:
public interface Mapp Example<T exte Example<T exte ...The minimized reproducer was 700 bytes.
Root Cause
The failing path involves recursive descent behavior around type/interface scanning. In practice, JavaFunctionScanner::scanInterface recursively processes nested constructs without an effective depth guard against malicious nesting.
At around ~200 recursive levels, stack growth reaches guard-page limits, resulting in EXC_BAD_ACCESS / SIGSEGV. This is a classic parser recursion-depth failure mode: not arbitrary code execution, but reliable process termination.
Crash traces repeatedly showed stack frames in the scanInterface path, consistent with unbounded recursive descent under crafted input depth.
Repro Metadata
- Affected product: BBEdit 15.5.5 (build 430000124)
- Payload size: 700 bytes
- SHA-256: db06cb2369095641206ca90a431f8ceb11aad2d2a6150648b6b80733e1a406f6
- CWE mapping: CWE-674 (Uncontrolled Recursion), with stack-exhaustion manifestation
Practical Impact
Opening the crafted .java file can immediately terminate BBEdit. The more painful effect is secondary: session restoration can reopen the same file on relaunch, repeatedly retriggering the crash and creating a user-facing crash loop.
This is still a local DoS class issue, but it can cause real productivity impact, potential unsaved-work loss, and lock users out until they bypass the recovery state. During coordination, Bare Bones also documented startup modifier-key workarounds to suppress reopening/restoration when needed.

Finding #2: Lasso Tokenizer Infinite Loop (CWE-835)
What Triggered It
Target: Lasso.bblm.
Unlike the Java issue, this one initially appeared as repeated timeout kills (SIGALRM, exit 142) instead of a crash. The supervisor preserved the offending input, and the minimized reproducer was 815 bytes.
Why /usr/bin/sample Was Key
For hangs, lldb is often less informative as a first move. We used /usr/bin/sample against the stuck process to collect statistical call stacks without stopping execution. The hot path was clear and stable:
LassoTokenizer::findNextToken
BBLMTextUtils::skipWhitespaceThe process stayed alive but pinned the CPU, matching an algorithmic non-termination profile.
Root Cause
The parser loop effectively behaves like while (!EOF) { ... }. Under crafted input containing embedded NUL bytes (0x00) in specific Lasso tokenization contexts, the tokenizer state can fail both checks:
- No valid token is matched.
- Input offset does not advance.
When "no token" and "no progress" happen together, the loop re-evaluates the same bytes indefinitely. That is a textbook forward-progress invariant violation and maps cleanly to CWE-835 (Loop with Unreachable Exit Condition / infinite loop behavior).
Vendor triage later confirmed a bad looping condition in the NUL-byte scenario.
Repro Metadata
- Affected product: BBEdit 15.5.5 (build 430000124)
- Payload size: 815 bytes
- SHA-256: 73aea6aff396fa7bef3dd30c24f8128d14ae58ee61de1df9f2d7f1b5f31b13ba
- CWE mapping: CWE-835
- Hex signature pattern: includes NULL interruptions such as defin\x00e and =\x00>
Practical Impact
Opening the crafted .lasso file can wedge the UI thread at ~100% CPU, producing a persistent beachball and requiring Force Quit. As with the Java case, restoration behavior can retrigger the condition on relaunch if the offending document is restored automatically.
Again: this is DoS, not code execution. But for affected users, it can still be disruptive in day-to-day use.


Responsible Disclosure Timeline
We coordinated this through Bare Bones Software support, with Rich Siegel handling communication.
- Apr 24, 2026: Initial vulnerability outreach sent.
- Apr 25, 2026 (early): We provided two technical finding reports and supporting PoC media.
- Apr 25, 2026 (same day): Vendor requested zipped payload samples for easy internal reproduction; we provided a reproduction bundle.
- Apr 25, 2026 (later): Rich confirmed they could reproduce both symptoms.
- Apr 27, 2026: Vendor reported both issues resolved:
- Java issue tied to recursion/backtracking behavior in tokenizer/function scanning paths.
- Lasso issue tied to looping logic with NULL-byte handling.
- They also reported no similar patterns found elsewhere in the review.
- May 22 2026: BBEdit 16.0 was released publicly. The release notes confirmed fixes for both reported issues: the Lasso null-byte hang and the Java unbalanced-generic recursion crash.
This was a good example of coordinated disclosure: fast reproduction, direct technical discussion, and clear closure.
Engineering Takeaways
1) Recursive Parsers Need Hard Depth Limits
If parser logic can recurse based on input structure, depth must be bounded explicitly and tested. "Normally shallow" assumptions do not hold under adversarial files. A predictable parse error is always better than stack exhaustion.
2) Every Scanner Loop Needs a Forward-Progress Invariant
Any loop of the form while (!EOF) should enforce: if no valid token/transition occurred, consume at least one byte (or fail fast with error state). "No match + no advance" is a known infinite-loop footgun.
3) In-Process Module Fuzzing Is a Force Multiplier
When architecture allows plugin-level entry points, in-process fuzzing can outperform process-spawn by orders of magnitude and expose bugs quickly. Throughput matters, especially for parser bugs, where many failures are rare edge-state interactions.
4) Operational Details Matter as Much as Mutation Logic
Harness correctness (app identity shims, deterministic alarm supervision, preserved repro artifacts) directly affects finding quality. A fast fuzzer without trustworthy reproductions creates noise. A moderately fast fuzzer with deterministic evidence closes bugs.
5) Desktop DoS Should Not Be Dismissed
In desktop tooling, DoS can translate to workflow paralysis and data-loss risk, especially when session restoration retriggers the crash/hang automatically. Severity labels may stay "medium," but user pain can be high.
Reproducibility and Evidence Discipline
One practical lesson from this case is that "we saw a crash once" is not enough for a strong vendor report or a trustworthy public write-up. We treated reproducibility as a first-class requirement:
- Preserve every minimized trigger input with stable hashes and byte sizes.
- Verify deterministic behavior across repeated runs (crash/timeout consistency).
- Collect process-level evidence from the real product context, not only a synthetic harness.
- Keep timeline artifacts and communication clean so vendor triage can move without ambiguity.
For the Java issue, deterministic crashes and stack traces made triage straightforward. For Lasso, CPU telemetry plus sample signatures turned a vague "hang" into concrete, actionable engineering data. This style of evidence also makes postmortem writing easier: the publication reflects what was actually observed and verified, not hindsight storytelling.
Closing Notes
We appreciate Bare Bones Software's response quality and turnaround speed in this case, and we appreciate Rich Siegel's direct coordination throughout triage and remediation.
From our perspective, this case study is valuable not because it produced dramatic exploitation headlines, but because it demonstrates a disciplined workflow:
- Target selection
- Architecture-aware fuzzing
- Deterministic minimization
- Evidence-backed reporting
- Coordinated release timing
That workflow is repeatable and scalable.
Acknowledgments
Thanks to Rich Siegel and the Bare Bones Software team for prompt investigation, fix turnaround, and professional disclosure handling.
https://www.barebones.com/support/bbedit/notes-16.0.html

Metadata
- Author: Nir Yehoshua, Cipher Security Labs
- Product: BBEdit 15.5.5 (build 430000124)
- Fixed in: BBEdit 16.0
- Platform tested: macOS 26.3 (25D125)
- Finding classes: Java stack exhaustion (CWE-674), Lasso infinite loop (CWE-835)