Skip to content

beta.18 macOS artifact segfaults at load on macOS 26; LDC >=1.42 ldc2.conf (directory + '~=' operator) breaks DCD stdlib detection #423

Description

@chikega

Environment

  • macOS 26.5.1 (Tahoe), Apple Silicon (arm64)
  • Homebrew LDC 1.42.0 (DMD frontend 2.112), dub, DCD 0.16.2
  • serve-d v0.8.0-beta.18 (both the published macOS release artifact and a local rebuild — see below)
  • Editors affected: any serve-d client; observed via Nova by Panic (D-Velop extension), reproduced outside any editor with a standalone LSP stdio driver

On this combination, D language support via the current release is fully broken, for two independent reasons. I've root-caused both; details and a minimal repro for each below.


Problem 1: the published macOS release binary segfaults at load on macOS 26

The beta.18 macOS artifact crashes before main()serve-d --version alone is enough:

$ ./serve-d --version
[1]    segmentation fault   exit code 139

macOS writes an .ips crash report on every launch attempt. Faulting-thread backtrace (parsed from ~/Library/Logs/DiagnosticReports/serve-d-*.ips):

os_version: macOS 26.5.1 (25F80)
exception : EXC_BAD_ACCESS / SIGSEGV / KERN_INVALID_ADDRESS at 0x00006c81faf6a770
Thread 0 crashed:
  libsystem_pthread.dylib pthread_getspecific
  serve-d                +0x6843b0
  dyld                   invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const
  dyld                   mach_o::UnsafeHeader::forEachLoadCommand(void (load_command const*, bool&) block_pointer) const
  dyld                   mach_o::UnsafeHeader::forEachSection(void (mach_o::UnsafeHeader::SectionInfo const&, bool&) block_pointer) const
  dyld                   dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const
  dyld                   dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const
  dyld                   dyld4::JustInTimeLoader::runInitializers(dyld4::RuntimeState&) const
  dyld                   dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, ...) const
  dyld                   dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const
  dyld                   dyld4::prepare(dyld4::APIs&, mach_o::UnsafeHeader const*)

The crash is in druntime's thread-local-storage setup (pthread_getspecific) inside a static initializer, while dyld is still running initializers — i.e., a load-time incompatibility, not a runtime bug:

  • otool -L shows the binary links only /usr/lib/libSystem.B.dylib → not a missing dependency
  • codesign -dv shows ad-hoc/linker signing, no quarantine attribute → not Gatekeeper

Confirmation that it's the artifact, not the source: rebuilding the same tag (v0.8.0-beta.18) locally with LDC 1.42 on macOS 26 produces a binary that runs fine:

$ git checkout v0.8.0-beta.18 && dub build --compiler=ldc2 --build=release
$ ./serve-d --version
serve-d standalone v0.8.0-beta.18
Included features: "d", "workspaces"
Built: Sun Jul  5 16:29:17 2026
with compiler LDC v2.112 on osx aarch64
dub, dfmt and dscanner are bundled within (compiled in)

The published artifact (built ~Oct 2024) appears to predate macOS 26's loader/TLS changes. Every macOS 26 user who installs serve-d through an editor extension's auto-download (code-d, D-Velop, etc.) gets a server that crash-loops silently.


Problem 2: stdlib auto-detection cannot parse LDC ≥ 1.42's ldc2.conf

After the local rebuild (Problem 1 solved), completion still fails: "Could not initialize DCD". Root cause is in stdlib path auto-detection (stdlib_detect.d / parseLdcConfImports), and it has two layers:

2a. ldc2.conf is now a directory — fixed in master, but unreleased

Homebrew LDC ≥ 1.42 ships /opt/homebrew/etc/ldc2.conf as a directory of config fragments (*.conf), not a single file. beta.18 tries to read it as a file:

FileException: /opt/homebrew/etc/ldc2.conf: Is a directory

Master already fixes this (commit 41b0edb, "Support directory form of ldc2.conf") by iterating the fragments. 👍 But no tagged release contains it — beta.18 is still the latest.

2b. The config parser doesn't understand the ~= append operator — present in master too

I backported 41b0edb onto beta.18 locally to verify. Directory iteration then works (all five fragments are found), but parsing fatally fails on 30-compiler.conf:

[error] extension.d:993:startDCDServer object.Exception@source/served/utils/stdlib_detect.d(425): Could not read ldc2 config file: /opt/homebrew/etc/ldc2.conf/30-compiler.conf: Error while reading config file: /opt/homebrew/etc/ldc2.conf/30-compiler.conf
line 3: Was expecting token ':' or '='. Got "unknown token" (~) instead.

LDC 1.42's fragments use the ~= (append) operator so drop-in fragments can extend the shared default section:

"default":
{
    switches ~= [ "-defaultlib=phobos2-ldc,druntime-ldc" ];
    post-switches ~= [ "-I/opt/homebrew/Cellar/ldc/1.42.0/include/dlang/ldc" ];
};

serve-d's bundled ldc2.conf parser only accepts = / :. Two aggravating factors:

  1. 30-compiler.conf is exactly the fragment that carries the -I phobos/druntime path, so skipping it loses the one value auto-detection exists to find; and
  2. the per-fragment parse error is rethrown as fatal, so one unparseable fragment aborts DCD initialization entirely rather than degrading.

Net effect: even with master's directory fix, stdlib auto-detect cannot work against LDC ≥ 1.42.


Reproduction

  • Problem 1: macOS 26 (arm64) + the published beta.18 macOS artifact → ./serve-d --version segfaults.
  • Problem 2: macOS 26 + Homebrew LDC ≥ 1.42 + a serve-d built from current source → open any dub project in any serve-d client → "Could not initialize DCD". Reproducible headlessly with a small Python LSP stdio driver and --loglevel all (stderr shows the exception above) — happy to attach the script.

Workaround for affected users

  1. Build serve-d from source with current LDC (dub build --compiler=ldc2 --build=release at tag v0.8.0-beta.18) — fixes Problem 1.
  2. Set an explicit d.stdlibPath (e.g. ["/opt/homebrew/include/dlang/ldc"]) — bypasses Problem 2 entirely, since auto-detection is skipped.

Suggested fixes / ask

In increasing order of effort:

  1. Cut a beta.19 release from current master on current macOS CI. This alone fixes Problem 1 for everyone and ships the existing 2a fix.
  2. Make a single unparseable fragment non-fatal in parseLdcConfImports (skip + warn instead of rethrow). Small change; turns a hard DCD failure into graceful degradation, and lets an explicit stdlibPath coexist with a partially-parsed config.
  3. Support the ~= append operator in the ldc2.conf parser. This is the complete fix for LDC ≥ 1.42 auto-detection (the -I path lives in a ~= assignment).

Thanks for serve-d — happy to test branches or provide any further diagnostics from this setup.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions