bun:ffi: use LoadLibraryExW on Windows so dlopen accepts non-ASCII paths - #33712
Conversation
bun:ffi dlopen() passed the library path to LoadLibraryA, which decodes bytes as the system ANSI codepage. Bun hands it UTF-8, so any non-ASCII byte in the path (a CJK username, an accented project directory) was mangled and the load failed with ERROR_MOD_NOT_FOUND even though the file exists. Convert the path to UTF-16 and call LoadLibraryExW, matching how every other Windows path in bun_sys is handled.
|
Reproduced on Windows x64 (system ACP cp1252): For reference, Node's Verified on Windows x64: new test fails against the released bun and passes with this build; CI is red on unrelated lanes only. The
No |
|
Updated 7:29 PM PT - Jul 7th, 2026
⏳ @robobun, your commit daadb07 is still building in |
|
Warning Review limit reached
Next review available in: 9 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe Windows ChangesWindows dlopen wide-character support
Sequence Diagram(s)sequenceDiagram
participant Caller
participant dlopen
participant to_w_path
participant LoadLibraryExW
Caller->>dlopen: dlopen(filename, flags)
dlopen->>to_w_path: widen UTF-8 filename to UTF-16
to_w_path-->>dlopen: pooled W buffer
dlopen->>LoadLibraryExW: call with widened path, dwFlags=0
LoadLibraryExW-->>dlopen: handle or null
dlopen-->>Caller: Option<*mut c_void>
Related issues: None specified. Related PRs: None specified. Suggested labels: windows, ffi, bug Suggested reviewers: None specified. 🐰 A rabbit hops through paths unseen, Comment |
…open Node's process.dlopen goes through uv_dlopen, which on Windows calls LoadLibraryExW with LOAD_WITH_ALTERED_SEARCH_PATH so that a DLL's dependent DLLs are resolved relative to the DLL itself rather than bun.exe. Bun's own process.dlopen already does the same. MSDN documents that flag as undefined for relative paths, so only set it when the path is absolute; bare names like "kernel32.dll" keep the standard search order.
bun:ffi dlopen() throws unconditionally when ENABLE_TINYCC is false, which is the case on Windows ARM64. Hoist the existing isFFIUnavailable constant and gate the new test on it, matching the convention used by every other TinyCC-dependent test in this directory.
There was a problem hiding this comment.
Thanks for addressing the ARM64 gating. The encoding fix itself is clearly correct, but adding LOAD_WITH_ALTERED_SEARCH_PATH also changes dependent-DLL resolution for bun:ffi users (swaps the app directory for the DLL's directory in the search order) — well-reasoned and matches libuv, but worth a maintainer confirming that's the intended semantics for bun:ffi.
Extended reasoning...
Overview
Two files: src/sys/lib.rs swaps the Windows branch of sys::dlopen from LoadLibraryA(utf8_bytes) to LoadLibraryExW(widened_path, NULL, flags) using the crate's standard w_path_buffer_pool::get() + to_w_path() pattern; test/js/bun/ffi/ffi.test.js adds a Windows-x64-only subprocess test that copies version.dll into café/日本語 directories and dlopens it. My earlier feedback (gate on isFFIUnavailable so Windows ARM64 skips) was applied in 36b464d — the isFFIUnavailable constant was hoisted and the test now uses it.skipIf(!isWindows || isFFIUnavailable). The helper functions (to_w_path, is_absolute_windows, kernel32::LoadLibraryExW) all exist and are used the same way elsewhere in bun_sys.
Security risks
DLL search order on Windows is a classic hijacking vector. Beyond fixing the encoding bug, this PR adds LOAD_WITH_ALTERED_SEARCH_PATH for absolute paths, which replaces "the directory containing bun.exe" with "the directory containing the loaded DLL" as the first entry in the dependent-DLL search order. That matches libuv's uv_dlopen and Bun's own process.dlopen, and is generally the safer/expected behavior for plugin loading — but it is a user-visible semantic change for any bun:ffi DLL whose transitive dependencies were previously satisfied from next to bun.exe. Other Windows callers of sys::dlopen (tracy.rs, Terminal.rs) are dev-internal and unlikely to be affected.
Level of scrutiny
Medium. The UTF-8 → UTF-16 widening is a mechanical correctness fix with an obvious root cause and a fail-before/pass-after test. But the search-order flag is a deliberate design choice layered on top, affecting how bun:ffi — which loads arbitrary user native code — resolves transitive DLLs. It's well-argued (MSDN says the flag is undefined for relative paths, hence gating on is_absolute_windows; matches Node), but I'd rather a maintainer confirm this is the desired bun:ffi contract than auto-approve a change to Windows library-loading semantics.
Other factors
No bugs found by the bug-hunting pass. The test follows harness conventions (tempDir, bunEnv spread, concurrent pipe drain, combined-object assertion, exitCode asserted alongside output). The PR description documents fail-before/pass-after on Windows x64 and confirms the existing bare-name kernel32.dll lookup test still passes.
Repro (Windows)
Before: throws
Failed to open library "C:\tmp\日本語\version.dll": error code 126(ERROR_MOD_NOT_FOUND). The same DLL loads fine from an ASCII path. Also fails forcaféand any other non-ASCII component.Cause
sys::dlopenon Windows calledLoadLibraryAwith the UTF-8 path bytes.LoadLibraryAdecodes its argument as the system ANSI codepage (cp1252 on most Western installs, cp932 on Japanese, etc.), so every non-ASCII UTF-8 byte sequence is misread and the file is not found. This affects anybun:ffiuser whose DLL lives under a profile directory with a non-ASCII username, which is routine on non-English Windows.Fix
Convert the UTF-8 path to UTF-16 via
bun_paths::string_paths::to_w_pathand callLoadLibraryExW(path, NULL, 0), matching how every other Windows path inbun_sysis handled.dwFlags = 0preserves the default DLL search order so bare names like"kernel32.dll"keep working.Verification
On Windows x64 (system ACP cp1252):
Full
ffi.test.jsandffi-error-messages.test.tspass on Windows with the change (13 pass, 5 skip, 0 fail), including the existingdlopen("kernel32.dll", ...)name-only lookup test.The test is Windows-only (
it.skipIf(!isWindows)); on Linux it is skipped.