Compare commits

...

4 Commits

Author SHA1 Message Date
Dylan Conway
2dc1533291 fix(windows): fs.watch segfault when a failed watch is retried (#27705)
## What does this PR do?

Fixes a Windows-only segfault at `0xFFFFFFFFFFFFFFFF` in `fs.watch()`
that fires when a failed watch is retried on the same path. This is why
every report involves a file-watching library (Vite, NestJS CLI,
chokidar, watchpack) — their standard error-recovery is "catch → retry
same path", which is the exact detonation sequence.

Fixes #26254
Fixes #20203
Fixes #19635

## Root cause

`PathWatcher.init()` uses an `errdefer` to clean up the
`manager.watchers` map entry on failure. That `errdefer` became dead
code when the function's return type was refactored from `!*PathWatcher`
→ `Maybe(*PathWatcher)`: `return .{ .err = ... }` is a *successful*
return of a tagged union, not an error-union return, so `errdefer` never
fires.

When `uv_fs_event_start` fails (dangling junction, ACL-denied dir,
TOCTOU delete, AV lock, SMB share without `ReadDirectoryChangesW`), a
map entry is left behind whose **key** points into a recycled stack/pool
buffer and whose **value** was never written. The next `fs.watch()` on
the same path collides with it, `getOrPut` returns
`found_existing=true`, and we hand back garbage as a `*PathWatcher` →
segfault in `.handlers.put()`.

### Why the second call detonates instead of just leaking

| `event_path` source | Why it's the same slice on retry |
|---|---|
| `path` (caller's buffer) | `FSWatcher.init` gets `joined_buf` from
`bun.path_buffer_pool` (LIFO). After return it's released; the next
`fs.watch` pops the same buffer and writes the same path to the same
address. |
| `outbuf` (stack local) | `PathWatcher.init` is always called at the
same stack depth from `fs.watch`. Same frame offset → same `&outbuf`. |

Same `ptr`, same `len` → `StringContext.eql` says equal → `getOrPut`
hits the poisoned entry.

## Fix

Two changes in `src/bun.js/node/win_watcher.zig` inside
`PathWatcher.init`:

1. **Replace the dead `errdefer` with inline cleanup** at the
`uv_fs_event_start` failure return. Cleanup: remove the map entry, null
out `manager` so `deinit()` doesn't re-enter `unregisterWatcher`, and
call `deinit()` to `uv_close` the initialized handle.
2. **Remove the `uv_fs_event_init` error branch.** It unconditionally
returns 0 on Windows (`vendor/libuv/src/win/fs-event.c:140-153`), and if
that branch ever ran it would call `uv_close()` on a zeroed handle with
a NULL loop pointer (crash inside libuv). Converted to `bun.assert`.

## How did you verify your code works?

Added a test in `test/js/node/watch/fs.watch.test.ts` that:
1. Creates a dangling junction (junctions need no admin rights on
Windows)
2. Calls `fs.watch()` on it twice — first call poisons the map on
unpatched builds, second call segfaults
3. Verifies a third watch on a valid dir still works (map not corrupted)

Runs in a subprocess since an unpatched build segfaults the whole
runtime.

Windows-only test — CI will verify.

## Issues checked but excluded

| # | Why not this bug |
|---|---|
| [#19732](https://github.com/oven-sh/bun/issues/19732),
[#20079](https://github.com/oven-sh/bun/issues/20079) | Caller frame is
`resolver.zig resolveWithFramework` — bundler's module resolver map, not
the watcher map |
| [#19651](https://github.com/oven-sh/bun/issues/19651) | Caller frame
is `uws.zig onData` — HTTP server's map |
| [#20710](https://github.com/oven-sh/bun/issues/20710) |
`node_fs_watcher.zig:248 run` — fires *after* a watcher successfully
started and received an event; this bug crashes *before* start |
| [#24694](https://github.com/oven-sh/bun/issues/24694),
[#24708](https://github.com/oven-sh/bun/issues/24708) | No `fs_watcher`
in Features, `standalone_executable`; robobun dupe-linked these by text
similarity only |
| [#26153](https://github.com/oven-sh/bun/issues/26153) | Already closed
as duplicate of #26254 |
2026-03-03 13:35:37 -08:00
Dylan Conway
9f9b681917 fix(css): dead errdefer leaks in Result(T)-returning parsers (#27706)
## What does this PR do?

Found during a sweep for the bug pattern that caused #26254 (the Windows
`fs.watch` segfault, fixed in #27705).

`Result(T)` is `Maybe(T, ParseError)` — a **tagged union**. `return .{
.err = ... }` is a *successful* return, so `errdefer` never fires.

Both dead `errdefer`s in this PR also referenced **code that wouldn't
compile if live** — Zig's lazy analysis never checked them because they
were in dead positions:
- `decl_parser.deinit()` — method doesn't exist
- `light.deinit()` — needs a mutable receiver + an allocator argument;
was called on a `const` with zero args

[Zig lang proposal #2654](https://github.com/ziglang/zig/issues/2654)
(accepted since 2020, unimplemented) would have caught both at compile
time.

### `src/css/declaration.zig` — `DeclarationBlock.parse`

Leaks all declarations accumulated so far when one fails to parse and
`error_recovery` is off.

Fixed with inline `css.deepDeinit` (same cleanup `context.zig:19-20`
uses for these exact lists).

### `src/css/properties/custom.zig` — `light-dark()` parsing

Leaks the `light` TokenList when the comma or `dark` fails to parse. The
second `errdefer (dark.deinit)` was both dead and unreachable — nothing
fallible follows it — so it's simply removed. These already had `//
TODO: fix this` comments.

Fixed with inline `light.deinit(input2.allocator())` at both failure
returns.

## How did you verify your code works?

CI will verify compile. Both codepaths are CSS parser error paths —
previously they compiled only because Zig doesn't analyze dead
`errdefer` bodies; now they compile because they're correct.

## Detection pattern

All `errdefer` instances in functions that also do `return .{ .err = ...
}`:

```bash
for f in $(rg -l 'errdefer' src --type zig); do
  awk -v file="$f" '
    /^[[:space:]]*(pub )?(inline )?fn / {
      if (has_errdefer && has_err_return) print file ":" fn_line ": " fn_sig
      fn_line = NR; fn_sig = $0; has_errdefer = 0; has_err_return = 0
    }
    /errdefer/ { has_errdefer = 1 }
    /return[[:space:]]+\.\{[[:space:]]*\.err[[:space:]]*=/ { has_err_return = 1 }
    END { if (has_errdefer && has_err_return) print file ":" fn_line ": " fn_sig }
  ' "$f"
done
```

Results at time of sweep:
- `win_watcher.zig:163` — fixed in #27705 (segfault)
- `css/declaration.zig:65` — this PR
- `css/properties/custom.zig:953` — this PR
- `process.zig spawnProcessPosix/Windows` — **false positives**: return
type is `!Maybe(T)` (hybrid); `errdefer` fires on the `try` paths, and
`.err` paths use a manual `failed` flag + `defer` for cleanup
2026-03-03 13:34:41 -08:00
Dylan Conway
6c7e97231b fix(bundler): barrel optimization drops exports used by dynamic import (#27695)
## What does this PR do?

Fixes invalid JS output when a `sideEffects: false` barrel is used by
both a **static named import** and a **dynamic `import()`** in the same
build with `--splitting --format=esm`.

## Root Cause

`src/bundler/barrel_imports.zig` had a heuristic that skipped escalating
`requested_exports` to `.all` for `import()` when the target already had
a partial entry from a static import:

```zig
} else if (ir.kind == .dynamic) {
    // Only escalate to .all if no prior requests exist for this target.
    if (!this.requested_exports.contains(target)) {
        try this.requested_exports.put(this.allocator(), target, .all);
    }
}
```

This is unsafe — `await import()` returns the **full module namespace**
at runtime. The consumer can destructure or access any export and we
can't statically determine which ones.

## Failure chain

1. Static `import { a } from "barrel"` seeds `requested_exports[barrel]
= .partial{"a"}`
2. Dynamic `import("barrel")` is ignored because
`requested_exports.contains(barrel)` is true
3. Barrel optimization marks `export { b } from "./b.js"` as `is_unused`
→ `source_index` cleared
4. Code splitting makes the barrel a chunk entry point (dynamic import →
own chunk)
5. Linker can't resolve the barrel's re-export symbol for `b` → uses the
unbound re-export ref directly
6. Output: `export { b2 as b }` where `b2` has no declaration →
**SyntaxError at runtime**

With `--bytecode --compile` this manifests as an `OutputFileListBuilder`
assertion (`total_insertions != output_files.items.len`) because JSC
rejects the chunk during bytecode generation, leaving an unfilled slot.

## Real-world trigger

`@smithy/credential-provider-imds` is a `sideEffects: false` barrel used
by:
- `@aws-sdk/credential-providers` — static: `import {
fromInstanceMetadata } from "@smithy/credential-provider-imds"`
- `@smithy/util-defaults-mode-node` — dynamic: `const {
getInstanceMetadataEndpoint, httpRequest } = await
import("@smithy/credential-provider-imds")`

## Fix

Dynamic import **always** marks the target as `.all` in both Phase 1
seeding and Phase 2 BFS. This is the same treatment as `require()`.

## Tests

- **New:** `barrel/DynamicImportWithStaticImportSameTarget` — repros the
bug with `--splitting`, verifies both exports work at runtime. Fails on
system bun with `SyntaxError: Exported binding 'b' needs to refer to a
top-level declared variable.`
- **Updated:** `barrel/DynamicImportInSubmodule` — was testing that a
fire-and-forget `import()` doesn't force unused submodules to load. That
optimization can't be safely applied, so the test now verifies the
conservative (correct) behavior: all barrel exports are preserved.
2026-03-02 14:33:22 -08:00
Jarred Sumner
32edef77e9 markdown: add {index, depth, ordered, start} to listItem callback meta (#27688) 2026-03-02 04:48:43 -08:00
14 changed files with 671 additions and 78 deletions

View File

@@ -124,22 +124,32 @@ Return a string to replace the element's rendering. Return `null` or `undefined`
### Block callbacks
| Callback | Meta | Description |
| ------------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `heading` | `{ level: number, id?: string }` | Heading level 16. `id` is set when `headings: { ids: true }` is enabled |
| `paragraph` | — | Paragraph block |
| `blockquote` | — | Blockquote block |
| `code` | `{ language?: string }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
| `list` | `{ ordered: boolean, start?: number }` | Ordered or unordered list. `start` is the start number for ordered lists |
| `listItem` | `{ checked?: boolean }` | List item. `checked` is set for task list items (`- [x]` / `- [ ]`) |
| `hr` | — | Horizontal rule |
| `table` | — | Table block |
| `thead` | — | Table head |
| `tbody` | — | Table body |
| `tr` | — | Table row |
| `th` | `{ align?: "left" \| "center" \| "right" }` | Table header cell. `align` is set when alignment is specified |
| `td` | `{ align?: "left" \| "center" \| "right" }` | Table data cell. `align` is set when alignment is specified |
| `html` | — | Raw HTML content |
| Callback | Meta | Description |
| ------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `heading` | `{ level, id? }` | Heading level 16. `id` is set when `headings: { ids: true }` is enabled |
| `paragraph` | — | Paragraph block |
| `blockquote` | — | Blockquote block |
| `code` | `{ language? }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
| `list` | `{ ordered, start?, depth }` | `depth` is nesting level (0 = top-level). `start` is set for ordered lists |
| `listItem` | `{ index, depth, ordered, start?, checked? }` | See [List item meta](#list-item-meta) below |
| `hr` | — | Horizontal rule |
| `table` | — | Table block |
| `thead` | — | Table head |
| `tbody` | — | Table body |
| `tr` | — | Table row |
| `th` | `{ align? }` | Table header cell. `align` is `"left"`, `"center"`, `"right"`, or absent |
| `td` | `{ align? }` | Table data cell |
| `html` | — | Raw HTML content |
#### List item meta
The `listItem` callback receives everything needed to render markers directly:
- `index` — 0-based position within the parent list
- `depth` — the parent list's nesting level (0 = top-level)
- `ordered` — whether the parent list is ordered
- `start` — the parent list's start number (only when `ordered` is true)
- `checked` — task list state (only for `- [x]` / `- [ ]` items)
### Inline callbacks
@@ -205,6 +215,33 @@ const ansi = Bun.markdown.render("# Hello\n\nThis is **bold** and *italic*", {
});
```
#### Nested list numbering
The `listItem` callback receives everything needed to render markers directly — no post-processing:
```ts
const result = Bun.markdown.render("1. first\n 1. sub-a\n 2. sub-b\n2. second", {
listItem: (children, { index, depth, ordered, start }) => {
const n = (start ?? 1) + index;
// 1. 2. 3. at depth 0, a. b. c. at depth 1, i. ii. iii. at depth 2
const marker = !ordered
? "-"
: depth === 0
? `${n}.`
: depth === 1
? `${String.fromCharCode(96 + n)}.`
: `${toRoman(n)}.`;
return " ".repeat(depth) + marker + " " + children.trimEnd() + "\n";
},
// Prepend a newline so nested lists are separated from their parent item's text
list: children => "\n" + children,
});
// 1. first
// a. sub-a
// b. sub-b
// 2. second
```
#### Code block syntax highlighting
````ts

View File

@@ -1193,10 +1193,20 @@ declare module "bun" {
ordered: boolean;
/** The start number for ordered lists. */
start?: number;
/** Nesting depth. `0` for a top-level list, `1` for a list inside a list item, etc. */
depth: number;
}
/** Meta passed to the `listItem` callback. */
interface ListItemMeta {
/** 0-based index of this item within its parent list. */
index: number;
/** Nesting depth of the parent list. `0` for items in a top-level list. */
depth: number;
/** Whether the parent list is ordered. */
ordered: boolean;
/** The start number of the parent list (only set when `ordered` is true). */
start?: number;
/** Task list checked state. Set for `- [x]` / `- [ ]` items. */
checked?: boolean;
}
@@ -1234,8 +1244,8 @@ declare module "bun" {
code?: (children: string, meta?: CodeBlockMeta) => string | null | undefined;
/** Ordered or unordered list. `start` is the first item number for ordered lists. */
list?: (children: string, meta: ListMeta) => string | null | undefined;
/** List item. `meta.checked` is set for task list items (`- [x]` / `- [ ]`). Only passed for task list items. */
listItem?: (children: string, meta?: ListItemMeta) => string | null | undefined;
/** List item. `meta` always includes `{index, depth, ordered}`. `meta.start` is set for ordered lists; `meta.checked` is set for task list items. */
listItem?: (children: string, meta: ListItemMeta) => string | null | undefined;
/** Horizontal rule. */
hr?: (children: string) => string | null | undefined;
/** Table. */

View File

@@ -759,8 +759,12 @@ const JsCallbackRenderer = struct {
const StackEntry = struct {
buffer: std.ArrayListUnmanaged(u8) = .{},
block_type: md.BlockType = .doc,
data: u32 = 0,
flags: u32 = 0,
/// For ul/ol: number of li children seen so far (next li's index).
/// For li: this item's 0-based index within its parent list.
child_index: u32 = 0,
detail: md.SpanDetail = .{},
};
@@ -853,7 +857,22 @@ const JsCallbackRenderer = struct {
if (block_type == .h) {
self.#heading_tracker.enterHeading();
}
try self.#stack.append(self.#allocator, .{ .data = data, .flags = flags });
// For li: record its 0-based index within the parent list, then
// increment the parent's counter so the next sibling gets index+1.
var child_index: u32 = 0;
if (block_type == .li and self.#stack.items.len > 0) {
const parent = &self.#stack.items[self.#stack.items.len - 1];
child_index = parent.child_index;
parent.child_index += 1;
}
try self.#stack.append(self.#allocator, .{
.block_type = block_type,
.data = data,
.flags = flags,
.child_index = child_index,
});
}
fn leaveBlockImpl(ptr: *anyopaque, block_type: md.BlockType, _: u32) bun.JSError!void {
@@ -986,6 +1005,30 @@ const JsCallbackRenderer = struct {
// Metadata object creation
// ========================================
/// Walks the stack to count enclosing ul/ol blocks. Called during leave,
/// so the top entry is the block itself (skip it for li, count it for ul/ol's
/// own depth which excludes self).
fn countListDepth(self: *JsCallbackRenderer) u32 {
var depth: u32 = 0;
// Skip the top entry (self) — we want enclosing lists only.
const len = self.#stack.items.len;
if (len < 2) return 0;
for (self.#stack.items[0 .. len - 1]) |entry| {
if (entry.block_type == .ul or entry.block_type == .ol) depth += 1;
}
return depth;
}
/// Returns the parent ul/ol entry for the current li (top of stack).
/// Returns null if the stack shape is unexpected.
fn parentList(self: *JsCallbackRenderer) ?*const StackEntry {
const len = self.#stack.items.len;
if (len < 2) return null;
const parent = &self.#stack.items[len - 2];
if (parent.block_type == .ul or parent.block_type == .ol) return parent;
return null;
}
fn createBlockMeta(self: *JsCallbackRenderer, block_type: md.BlockType, data: u32, flags: u32) bun.JSError!?JSValue {
const g = self.#globalObject;
switch (block_type) {
@@ -1000,15 +1043,10 @@ const JsCallbackRenderer = struct {
return obj;
},
.ol => {
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("ordered"), .true);
obj.put(g, ZigString.static("start"), JSValue.jsNumber(data));
return obj;
return BunMarkdownMeta__createList(g, true, JSValue.jsNumber(data), self.countListDepth());
},
.ul => {
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("ordered"), .false);
return obj;
return BunMarkdownMeta__createList(g, false, .js_undefined, self.countListDepth());
},
.code => {
if (flags & md.BLOCK_FENCED_CODE != 0) {
@@ -1023,21 +1061,31 @@ const JsCallbackRenderer = struct {
},
.th, .td => {
const alignment = md.types.alignmentFromData(data);
if (md.types.alignmentName(alignment)) |align_str| {
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("align"), try bun.String.createUTF8ForJS(g, align_str));
return obj;
}
return null;
const align_js = if (md.types.alignmentName(alignment)) |align_str|
try bun.String.createUTF8ForJS(g, align_str)
else
JSValue.js_undefined;
return BunMarkdownMeta__createCell(g, align_js);
},
.li => {
// The li entry is still on top of the stack; parent ul/ol is at len-2.
const len = self.#stack.items.len;
const item_index = if (len > 1) self.#stack.items[len - 1].child_index else 0;
const parent = self.parentList();
const is_ordered = parent != null and parent.?.block_type == .ol;
// countListDepth() includes the immediate parent list; subtract it
// so that items in a top-level list report depth 0.
const enclosing = self.countListDepth();
const depth: u32 = if (enclosing > 0) enclosing - 1 else 0;
const task_mark = md.types.taskMarkFromData(data);
if (task_mark != 0) {
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("checked"), JSValue.jsBoolean(md.types.isTaskChecked(task_mark)));
return obj;
}
return null;
const start_js = if (is_ordered) JSValue.jsNumber(parent.?.data) else JSValue.js_undefined;
const checked_js = if (task_mark != 0)
JSValue.jsBoolean(md.types.isTaskChecked(task_mark))
else
JSValue.js_undefined;
return BunMarkdownMeta__createListItem(g, item_index, depth, is_ordered, start_js, checked_js);
},
else => return null,
}
@@ -1047,14 +1095,18 @@ const JsCallbackRenderer = struct {
const g = self.#globalObject;
switch (span_type) {
.a => {
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("href"), try bun.String.createUTF8ForJS(g, detail.href));
if (detail.title.len > 0) {
obj.put(g, ZigString.static("title"), try bun.String.createUTF8ForJS(g, detail.title));
}
return obj;
const href = try bun.String.createUTF8ForJS(g, detail.href);
const title = if (detail.title.len > 0)
try bun.String.createUTF8ForJS(g, detail.title)
else
JSValue.js_undefined;
return BunMarkdownMeta__createLink(g, href, title);
},
.img => {
// Image meta shares shape with link (src/href are both the first
// field). We use a separate cached structure would require a
// second slot, so just fall back to the generic path here —
// images are rare enough that it doesn't matter.
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("src"), try bun.String.createUTF8ForJS(g, detail.href));
if (detail.title.len > 0) {
@@ -1114,6 +1166,14 @@ const TagIndex = enum(u8) {
extern fn BunMarkdownTagStrings__getTagString(*jsc.JSGlobalObject, u8) JSValue;
// Fast-path meta-object constructors using cached Structures (see
// BunMarkdownMeta.cpp). Each constructs via putDirectOffset so the
// resulting objects share a single Structure and stay monomorphic.
extern fn BunMarkdownMeta__createListItem(*jsc.JSGlobalObject, u32, u32, bool, JSValue, JSValue) JSValue;
extern fn BunMarkdownMeta__createList(*jsc.JSGlobalObject, bool, JSValue, u32) JSValue;
extern fn BunMarkdownMeta__createCell(*jsc.JSGlobalObject, JSValue) JSValue;
extern fn BunMarkdownMeta__createLink(*jsc.JSGlobalObject, JSValue, JSValue) JSValue;
fn getCachedTagString(globalObject: *jsc.JSGlobalObject, tag: TagIndex) JSValue {
return BunMarkdownTagStrings__getTagString(globalObject, @intFromEnum(tag));
}

View File

@@ -0,0 +1,123 @@
#include "BunMarkdownMeta.h"
#include "JavaScriptCore/JSObjectInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "JavaScriptCore/JSCast.h"
using namespace JSC;
namespace Bun {
namespace MarkdownMeta {
// Builds a cached Structure with N fixed property offsets. Properties are
// laid out in declaration order so the extern "C" create functions can use
// putDirectOffset without name lookups.
static Structure* buildStructure(VM& vm, JSGlobalObject* globalObject, std::initializer_list<ASCIILiteral> names)
{
Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
globalObject,
globalObject->objectPrototype(),
names.size());
PropertyOffset offset;
PropertyOffset expected = 0;
for (auto name : names) {
structure = structure->addPropertyTransition(vm, structure, Identifier::fromString(vm, name), 0, offset);
ASSERT_UNUSED(expected, offset == expected);
expected++;
}
return structure;
}
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "index"_s, "depth"_s, "ordered"_s, "start"_s, "checked"_s });
}
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "ordered"_s, "start"_s, "depth"_s });
}
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "align"_s });
}
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "href"_s, "title"_s });
}
} // namespace MarkdownMeta
} // namespace Bun
// ──────────────────────────────────────────────────────────────────────────
// extern "C" constructors — callable from MarkdownObject.zig
// ──────────────────────────────────────────────────────────────────────────
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
JSGlobalObject* globalObject,
uint32_t index,
uint32_t depth,
bool ordered,
EncodedJSValue start,
EncodedJSValue checked)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListItemMetaStructure());
obj->putDirectOffset(vm, 0, jsNumber(index));
obj->putDirectOffset(vm, 1, jsNumber(depth));
obj->putDirectOffset(vm, 2, jsBoolean(ordered));
obj->putDirectOffset(vm, 3, JSValue::decode(start));
obj->putDirectOffset(vm, 4, JSValue::decode(checked));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
JSGlobalObject* globalObject,
bool ordered,
EncodedJSValue start,
uint32_t depth)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListMetaStructure());
obj->putDirectOffset(vm, 0, jsBoolean(ordered));
obj->putDirectOffset(vm, 1, JSValue::decode(start));
obj->putDirectOffset(vm, 2, jsNumber(depth));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
JSGlobalObject* globalObject,
EncodedJSValue align)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownCellMetaStructure());
obj->putDirectOffset(vm, 0, JSValue::decode(align));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
JSGlobalObject* globalObject,
EncodedJSValue href,
EncodedJSValue title)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownLinkMetaStructure());
obj->putDirectOffset(vm, 0, JSValue::decode(href));
obj->putDirectOffset(vm, 1, JSValue::decode(title));
return JSValue::encode(obj);
}

View File

@@ -0,0 +1,61 @@
#pragma once
#include "root.h"
#include "headers.h"
#include "JavaScriptCore/JSObjectInlines.h"
#include "ZigGlobalObject.h"
using namespace JSC;
namespace Bun {
namespace MarkdownMeta {
// Cached Structures for the small metadata objects passed as the second
// argument to Bun.markdown.render() callbacks. These have fixed shapes
// so JSC's property access inline caches stay monomorphic and we avoid
// the string-hash + property-transition cost of `put()`-style construction
// on every callback (which matters a lot for list items and table cells).
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject);
} // namespace MarkdownMeta
} // namespace Bun
// ListItemMeta: {index, depth, ordered, start, checked}
// `start` and `checked` are always present (jsUndefined() when not applicable)
// so the shape is fixed.
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
JSGlobalObject* globalObject,
uint32_t index,
uint32_t depth,
bool ordered,
EncodedJSValue start, // jsNumber or jsUndefined
EncodedJSValue checked // jsBoolean or jsUndefined
);
// ListMeta: {ordered, start, depth}
// `start` is always present (jsUndefined for unordered).
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
JSGlobalObject* globalObject,
bool ordered,
EncodedJSValue start, // jsNumber or jsUndefined
uint32_t depth);
// CellMeta: {align}
// `align` is always present (jsUndefined when no alignment).
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
JSGlobalObject* globalObject,
EncodedJSValue align // jsString or jsUndefined
);
// LinkMeta / ImageMeta: {href, title} or {src, title}
// `title` is always present (jsUndefined when missing). `href` and `src`
// share the structure slot (first property) — the property name differs
// but the shape is the same; two separate structures are used.
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
JSGlobalObject* globalObject,
EncodedJSValue href,
EncodedJSValue title // jsString or jsUndefined
);

View File

@@ -124,6 +124,7 @@
#include "JSSink.h"
#include "JSSocketAddressDTO.h"
#include "JSReactElement.h"
#include "BunMarkdownMeta.h"
#include "JSSQLStatement.h"
#include "JSStringDecoder.h"
#include "JSTextEncoder.h"
@@ -1802,6 +1803,23 @@ void GlobalObject::finishCreation(VM& vm)
init.set(Bun::JSReactElement::createStructure(init.vm, init.owner));
});
m_JSMarkdownListItemMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createListItemMetaStructure(init.vm, init.owner));
});
m_JSMarkdownListMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createListMetaStructure(init.vm, init.owner));
});
m_JSMarkdownCellMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createCellMetaStructure(init.vm, init.owner));
});
m_JSMarkdownLinkMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createLinkMetaStructure(init.vm, init.owner));
});
m_JSSQLStatementStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(WebCore::createJSSQLStatementStructure(init.owner));

View File

@@ -302,6 +302,10 @@ public:
Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); }
Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); }
Structure* JSReactElementStructure() const { return m_JSReactElementStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownListItemMetaStructure() const { return m_JSMarkdownListItemMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownListMetaStructure() const { return m_JSMarkdownListMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownCellMetaStructure() const { return m_JSMarkdownCellMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownLinkMetaStructure() const { return m_JSMarkdownLinkMetaStructure.getInitializedOnMainThread(this); }
Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); }
Structure* ImportMetaBakeObjectStructure() const { return m_importMetaBakeObjectStructure.getInitializedOnMainThread(this); }
Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); }
@@ -597,6 +601,10 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_commonJSModuleObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSocketAddressDTOStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSReactElementStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListItemMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownCellMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownLinkMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_memoryFootprintStructure) \
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireFunctionUnbound) \
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireResolveFunctionUnbound) \

View File

@@ -186,15 +186,9 @@ pub const PathWatcher = struct {
.manager = manager,
});
errdefer {
_ = manager.watchers.swapRemove(event_path);
this.manager = null;
this.deinit();
}
if (uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle).toError(.watch)) |err| {
return .{ .err = err };
}
// uv_fs_event_init on Windows unconditionally returns 0 (vendor/libuv/src/win/fs-event.c).
// bun.assert evaluates its argument before the inline early-return, so this runs in release too.
bun.assert(uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle) == .zero);
this.handle.data = this;
// UV_FS_EVENT_RECURSIVE only works for Windows and OSX
@@ -204,6 +198,12 @@ pub const PathWatcher = struct {
event_path.ptr,
if (recursive) uv.UV_FS_EVENT_RECURSIVE else 0,
).toError(.watch)) |err| {
// `errdefer` doesn't fire on `return .{ .err = ... }` (that's a successful return of a
// Maybe(T), not an error-union return). Clean up the map entry and the half-initialized
// watcher inline. See #26254.
_ = manager.watchers.swapRemove(event_path);
this.manager = null; // prevent deinit() from re-entering unregisterWatcher
this.deinit();
return .{ .err = err };
}
// we handle this in node_fs_watcher

View File

@@ -301,10 +301,9 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
// Handle import records without named bindings (not in named_imports).
// - `import "x"` (bare statement): tree-shakeable with sideEffects: false — skip.
// - `require("x")`: synchronous, needs full module — always mark as .all.
// - `import("x")`: mark as .all ONLY if the barrel has no prior requests,
// meaning this is the sole reference. If the barrel already has a .partial
// entry from a static import, the dynamic import is likely a secondary
// (possibly circular) reference and should not escalate requirements.
// - `import("x")`: returns the full module namespace at runtime — consumer
// can destructure or access any export. Must mark as .all. We cannot
// safely assume which exports will be used.
for (file_import_records.slice(), 0..) |ir, idx| {
const target = if (ir.source_index.isValid())
ir.source_index.get()
@@ -319,10 +318,9 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
gop.value_ptr.* = .all;
} else if (ir.kind == .dynamic) {
// Only escalate to .all if no prior requests exist for this target.
if (!this.requested_exports.contains(target)) {
try this.requested_exports.put(this.allocator(), target, .all);
}
// import() returns the full module namespace — must preserve all exports.
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
gop.value_ptr.* = .all;
}
}
@@ -354,8 +352,8 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
}
}
// Add bare require/dynamic-import targets to BFS as star imports (matching
// the seeding logic above — require always, dynamic only when sole reference).
// Add bare require/dynamic-import targets to BFS as star imports — both
// always need the full namespace.
for (file_import_records.slice(), 0..) |ir, idx| {
const target = if (ir.source_index.isValid())
ir.source_index.get()
@@ -366,8 +364,7 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
if (ir.flags.is_internal) continue;
if (named_ir_indices.contains(@intCast(idx))) continue;
if (ir.flags.was_originally_bare_import) continue;
const is_all = if (this.requested_exports.get(target)) |re| re == .all else false;
const should_add = ir.kind == .require or (ir.kind == .dynamic and is_all);
const should_add = ir.kind == .require or ir.kind == .dynamic;
if (should_add) {
try queue.append(queue_alloc, .{ .barrel_source_index = target, .alias = "", .is_star = true });
}

View File

@@ -70,8 +70,6 @@ pub const DeclarationBlock = struct {
.declarations = &declarations,
.options = options,
};
errdefer decl_parser.deinit();
var parser = css.RuleBodyParser(PropertyDeclarationParser).new(input, &decl_parser);
while (parser.next()) |res| {
@@ -80,6 +78,10 @@ pub const DeclarationBlock = struct {
options.warn(e);
continue;
}
// errdefer doesn't fire on `return .{ .err = ... }` — Result(T) is a tagged
// union, not an error union. Free any declarations accumulated so far.
css.deepDeinit(css.Property, input.allocator(), &declarations);
css.deepDeinit(css.Property, input.allocator(), &important_declarations);
return .{ .err = e };
}
}

View File

@@ -951,19 +951,23 @@ pub const UnresolvedColor = union(enum) {
options: *const css.ParserOptions,
parser: *ComponentParser,
pub fn parsefn(this: *@This(), input2: *css.Parser) Result(UnresolvedColor) {
const light = switch (input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2)) {
// errdefer doesn't fire on `return .{ .err = ... }` — Result(T) is a
// tagged union, not an error union. Clean up `light` inline.
var light = switch (input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
};
// TODO: fix this
errdefer light.deinit();
if (input2.expectComma().asErr()) |e| return .{ .err = e };
if (input2.expectComma().asErr()) |e| {
light.deinit(input2.allocator());
return .{ .err = e };
}
const dark = switch (TokenListFns.parse(input2, this.options, 0)) {
.result => |vv| vv,
.err => |e| return .{ .err = e },
.err => |e| {
light.deinit(input2.allocator());
return .{ .err = e };
},
};
// TODO: fix this
errdefer dark.deinit();
return .{ .result = UnresolvedColor{
.light_dark = .{
.light = light,

View File

@@ -541,7 +541,9 @@ describe("bundler", () => {
});
// --- Ported from Rolldown: dynamic-import-entry ---
// A submodule dynamically imports the barrel back
// A submodule dynamically imports the barrel back. import() returns the full
// module namespace — all barrel exports must be preserved, even if the
// import() result is discarded (we can't statically prove it isn't used).
itBundled("barrel/DynamicImportInSubmodule", {
files: {
@@ -562,17 +564,103 @@ describe("bundler", () => {
export const a = 'dyn-a';
import('./index.js');
`,
// b.js has a syntax error — only a is imported, so b should be skipped
"/node_modules/dynlib/b.js": /* js */ `
export const b = <<<SYNTAX_ERROR>>>;
export const b = 'dyn-b';
`,
},
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/entry.js").toContain("dyn-a");
// b must be included — import() needs the full namespace
api.expectFile("/out/entry.js").toContain("dyn-b");
},
});
// Dynamic import returns the full namespace at runtime — consumer can access any export.
// When a file also has a static named import of the same barrel, the barrel
// optimization must not drop exports the dynamic import might use.
// Previously, the dynamic import was ignored if a static import already seeded
// requested_exports, producing invalid JS (export clause referencing undeclared symbol).
itBundled("barrel/DynamicImportWithStaticImportSameTarget", {
files: {
"/entry.js": /* js */ `
import { a } from "barrel";
console.log(a);
const run = async () => {
const { b } = await import("barrel");
console.log(b);
};
run();
`,
"/node_modules/barrel/package.json": JSON.stringify({
name: "barrel",
main: "./index.js",
sideEffects: false,
}),
"/node_modules/barrel/index.js": /* js */ `
export { a } from "./a.js";
export { b } from "./b.js";
`,
"/node_modules/barrel/a.js": /* js */ `
export const a = "A";
`,
"/node_modules/barrel/b.js": /* js */ `
export const b = "B";
`,
},
splitting: true,
format: "esm",
target: "bun",
outdir: "/out",
run: {
stdout: "A\nB",
},
});
// Same as above but static and dynamic importers are in separate files.
// This was parse-order dependent — if the static importer's
// scheduleBarrelDeferredImports ran first, it seeded .partial and the dynamic
// importer's escalation was skipped. Now import() always escalates to .all.
itBundled("barrel/DynamicImportWithStaticImportSeparateFiles", {
files: {
"/static-user.js": /* js */ `
import { a } from "barrel2";
console.log(a);
`,
"/dynamic-user.js": /* js */ `
const run = async () => {
const { b } = await import("barrel2");
console.log(b);
};
run();
`,
"/node_modules/barrel2/package.json": JSON.stringify({
name: "barrel2",
main: "./index.js",
sideEffects: false,
}),
"/node_modules/barrel2/index.js": /* js */ `
export { a } from "./a.js";
export { b } from "./b.js";
`,
"/node_modules/barrel2/a.js": /* js */ `
export const a = "A";
`,
"/node_modules/barrel2/b.js": /* js */ `
export const b = "B";
`,
},
entryPoints: ["/static-user.js", "/dynamic-user.js"],
splitting: true,
format: "esm",
target: "bun",
outdir: "/out",
run: [
{ file: "/out/static-user.js", stdout: "A" },
{ file: "/out/dynamic-user.js", stdout: "B" },
],
});
// --- Ported from Rolldown: multiple-entries ---
// Multiple entry points that each import different things from barrels

View File

@@ -146,6 +146,130 @@ describe("Bun.markdown.render", () => {
expect(result).toBe('<ol start="3"><li>first</li><li>second</li></ol>');
});
test("listItem receives {index, depth, ordered, start, checked}", () => {
const metas: any[] = [];
Markdown.render("3. first\n4. second\n5. third\n", {
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
});
// Shape is fixed (5 properties) so JSC inline caches stay monomorphic;
// `start` is undefined for unordered, `checked` is undefined for non-task items.
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: true, start: 3, checked: undefined },
{ index: 1, depth: 0, ordered: true, start: 3, checked: undefined },
{ index: 2, depth: 0, ordered: true, start: 3, checked: undefined },
]);
// All items share the same hidden class.
expect(Object.keys(metas[0])).toEqual(["index", "depth", "ordered", "start", "checked"]);
});
test("listItem meta for unordered list (start is undefined)", () => {
const metas: any[] = [];
Markdown.render("- a\n- b\n", {
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
});
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: false, start: undefined, checked: undefined },
{ index: 1, depth: 0, ordered: false, start: undefined, checked: undefined },
]);
});
test("listItem depth tracks nesting", () => {
const metas: any[] = [];
Markdown.render("1. outer\n 1. inner-a\n 2. inner-b\n2. outer2\n", {
listItem: (_: string, m: any) => {
metas.push(m);
return "";
},
list: () => "",
});
// Callbacks fire bottom-up: inner items first, then outer.
expect(metas).toEqual([
{ index: 0, depth: 1, ordered: true, start: 1, checked: undefined },
{ index: 1, depth: 1, ordered: true, start: 1, checked: undefined },
{ index: 0, depth: 0, ordered: true, start: 1, checked: undefined },
{ index: 1, depth: 0, ordered: true, start: 1, checked: undefined },
]);
});
test("list meta includes depth", () => {
const metas: any[] = [];
Markdown.render("- outer\n - inner\n", {
list: (c: string, m: any) => {
metas.push(m);
return c;
},
listItem: (c: string) => c,
});
// Inner list fires first (bottom-up). Fixed shape: start is always present.
expect(metas).toEqual([
{ ordered: false, start: undefined, depth: 1 },
{ ordered: false, start: undefined, depth: 0 },
]);
});
test("listItem meta includes checked alongside index/depth/ordered", () => {
const metas: any[] = [];
Markdown.render(
"- [x] done\n- [ ] todo\n- plain\n",
{
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
},
{ tasklists: true },
);
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: false, start: undefined, checked: true },
{ index: 1, depth: 0, ordered: false, start: undefined, checked: false },
{ index: 2, depth: 0, ordered: false, start: undefined, checked: undefined },
]);
});
test("listItem index resets across sibling lists", () => {
const metas: any[] = [];
Markdown.render("1. a\n2. b\n\npara\n\n1. c\n2. d\n", {
listItem: (c: string, m: any) => {
metas.push({ text: c, index: m.index });
return c;
},
list: (c: string) => c,
paragraph: (c: string) => c,
});
expect(metas).toEqual([
{ text: "a", index: 0 },
{ text: "b", index: 1 },
{ text: "c", index: 0 },
{ text: "d", index: 1 },
]);
});
test("listItem enables direct marker rendering (no post-processing)", () => {
// The motivating use case: ANSI terminal renderer with depth-aware numbering.
const toAlpha = (n: number) => String.fromCharCode(96 + n);
const result = Markdown.render("1. first\n 1. sub-a\n 2. sub-b\n2. second\n", {
listItem: (c: string, m: any) => {
const n = (m.start ?? 1) + m.index;
const marker = !m.ordered ? "-" : m.depth === 0 ? `${n}.` : `${toAlpha(n)}.`;
const indent = " ".repeat(m.depth);
return indent + marker + " " + c.trimEnd() + "\n";
},
// Nested lists are concatenated directly after the parent item's text;
// prefix a newline so the outer listItem's trimEnd() works correctly.
list: (c: string) => "\n" + c,
});
expect(result).toBe("\n1. first\n a. sub-a\n b. sub-b\n2. second\n");
});
test("strikethrough callback", () => {
const result = Markdown.render("~~deleted~~\n", {
strikethrough: (children: string) => `<del>${children}</del>`,

View File

@@ -1,5 +1,5 @@
import { pathToFileURL } from "bun";
import { bunRun, bunRunAsScript, isWindows, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, bunRun, bunRunAsScript, isWindows, tempDir, tempDirWithFiles } from "harness";
import fs, { FSWatcher } from "node:fs";
import path from "path";
@@ -725,3 +725,64 @@ describe("immediately closing", () => {
for (let i = 0; i < 100; i++) fs.watch(testDir, { persistent: false, recursive: false }).close();
});
});
// On Windows, if fs.watch() fails after getOrPut() inserts into the internal path->watcher
// map (e.g. uv_fs_event_start fails on a dangling junction, an ACL-protected dir, or a
// directory deleted mid-watch), an errdefer that was silently broken by a !*T -> Maybe(*T)
// refactor left the entry in place with a dangling key and an uninitialized value. The next
// fs.watch() on the same path collided with the poisoned entry, returned the garbage value
// as a *PathWatcher, and segfaulted at 0xFFFFFFFFFFFFFFFF calling .handlers.put() on it.
//
// https://github.com/oven-sh/bun/issues/26254
// https://github.com/oven-sh/bun/issues/20203
// https://github.com/oven-sh/bun/issues/19635
//
// Must run in a subprocess: on an unpatched build this segfaults the whole runtime.
test.skipIf(!isWindows)("retrying a failed fs.watch does not crash (windows)", async () => {
using dir = tempDir("fswatch-retry-failed", { "index.js": "" });
const base = String(dir);
const fixture = /* js */ `
const { mkdirSync, rmdirSync, symlinkSync, watch } = require("node:fs");
const { join } = require("node:path");
const base = ${JSON.stringify(base)};
const target = join(base, "target");
const link = join(base, "link");
mkdirSync(target);
symlinkSync(target, link, "junction"); // junctions need no admin rights on Windows
rmdirSync(target); // junction now dangles
// Call 1: readlink(link) SUCCEEDS (returns the vanished target path into
// a stack-local buffer), then uv_fs_event_start(target) fails ENOENT.
// On unpatched builds: map entry left with dangling key + uninit value.
try { watch(link); throw new Error("expected first watch to fail"); }
catch (e) { if (e.code !== "ENOENT") throw e; }
// Call 2: identical stack frame layout -> identical outbuf address ->
// identical key slice -> getOrPut returns found_existing=true ->
// returns uninitialized value as a *PathWatcher -> segfault on unpatched builds.
// Correct behaviour: throw ENOENT again.
try { watch(link); throw new Error("expected second watch to fail"); }
catch (e) { if (e.code !== "ENOENT") throw e; }
// Call 3: a valid watch must still work (map must not be corrupted).
watch(base).close();
console.log("OK");
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
cwd: base,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("OK");
expect(exitCode).toBe(0); // unpatched: exitCode is 3 (Windows segfault)
});