Zig 0.16 Linux linker, bundled LLD bug?

I encountered what appears to be an extended ELF section-index handling issue in Zig 0.16.0.

Environment

  • Zig 0.16.0
  • Native x86_64-linux-gnu
  • Ubuntu 26.04.1 under WSL2
  • RayClay 0.8.11 compiled as a single-header C implementation

The RayClay header is compiled with:

-std=gnu11
-DRAYCLAY_IMPLEMENTATION
-DRC_GLFW_X11=1
-DRC_GLFW_WAYLAND=1
-ffunction-sections
-fdata-sections

Initial failure

A Debug zig build produced undefined-symbol errors for RayClay functions, including:

undefined symbol: rcRunApp

The same happened for many other public RayClay functions.

Initially, this looked like the implementation object had not been compiled or attached to the Zig module. However, rayclay.o existed and contained the missing symbols:

rcGetStyle GLOBAL DEFAULT section 291
rcRunApp GLOBAL DEFAULT section 2140
rcAppRequestClose GLOBAL DEFAULT section 2146

For example:

readelf -sW rayclay.o | grep rcRunApp

produced:

44719: 0000000000000000 2109 FUNC GLOBAL DEFAULT 2140 rcRunApp

Therefore, the symbol was present in the object, but Zig’s linker behaved as though it were undefined.

Unusually large ELF section table

The Debug object had 78,010 sections:

Number of section headers: 0 (78010)

The zero is ELF extended section numbering, not an actual zero section count. The object also had a .symtab_shndx section.

The normal ELF section-index range becomes reserved starting at 0xff00, so this object requires extended section and symbol indexes.

The section breakdown was approximately:

Category Count
.text.* and text relocations 3,377
.data.* and data relocations 72,482
.rodata.* and relocations 1,071
.bss.* 1,054
Debug and other sections 26
Total 78,010

The largest contributors were:

36,252 .data.* sections
36,230 .rela.data.* sections

Many were compiler-generated entries such as:

.data..L__unnamed_23
.rela.data..L__unnamed_23
.data..L__unnamed_24
.rela.data..L__unnamed_24

The Debug object also referenced UBSan handlers:

__ubsan_handle_type_mismatch_v1
__ubsan_handle_pointer_overflow
__ubsan_handle_add_overflow
__ubsan_handle_out_of_bounds

My current understanding is that Debug C compilation generates a large amount of UBSan metadata for this approximately 80,000-line implementation header. At -O0, most of that metadata remains, and -fdata-sections places each metadata object in a separate ELF section. Objects containing references also receive separate relocation sections.

GNU readelf can parse the object and find the RayClay symbols, but Zig’s default linker fails to resolve them.

Bundled LLD result

I temporarily restored the original -fdata-sections configuration and enabled Zig’s bundled LLD.

The goal was to determine whether the issue was specific to Zig’s default ELF linker while retaining the existing data-section layout.

Instead, the bundled LLD process terminated with SIGSEGV.

I do not have a symbolized backtrace, so I cannot say exactly where LLD crashed. The crash appears correlated with the same 78,010-section object and its extended section-index tables.

Working workaround

Changing only the Debug build to:

-fno-data-sections

reduced the object from 78,010 sections to 3,509 sections:

Category Count
.text.* and text relocations 3,377
Writable data and relocations 4
Read-only data and relocations 108
BSS 1
Debug and other sections 19
Total 3,509

This does not disable UBSan or remove the generated metadata. It groups compatible data objects into shared sections instead of creating a separate section for every object.

The resulting Debug build links and runs successfully.

The current workaround is:

if (optimize == .Debug)
    "-fno-data-sections"
else
    "-fdata-sections"

Release behavior

The issue only occurred in Debug.

The release C objects did not contain references to __ubsan_* handlers and remained far below the extended-index threshold:

Mode Flag Sections Result
Debug -fdata-sections 78,010 Undefined-symbol failure
Debug -fno-data-sections 3,509 Works
ReleaseSafe -fdata-sections 3,100 Works
ReleaseFast -fdata-sections 2,914 Works
ReleaseSmall -fdata-sections Below threshold Works

I also tested ReleaseFast with -fno-data-sections. It worked, but the final executable was slightly larger:

ReleaseFast with -fdata-sections: 3,912,152 bytes
ReleaseFast with -fno-data-sections: 3,920,520 bytes

For that reason, I limited the workaround to Debug.

Questions

  1. Is extended ELF section numbering or SHT_SYMTAB_SHNDX handling a known limitation in Zig 0.16’s default Linux linker?
  2. Is the bundled LLD SIGSEGV likely a separate Zig/LLD integration bug?
  3. Is using -fno-data-sections only for Debug the recommended workaround?
  4. Is there a linker option that allows Zig to correctly consume this object while retaining -fdata-sections?
  5. Would a reduced reproducer or the original object file be useful for an issue report?

The final Debug and release executables otherwise build and run correctly on both the Wayland and X11 GLFW backends.

While I don’t know the answers to these specific questions, I know that 0.17 is getting a very updated linker, and the old one has limitations in regards to section handling.

Some examples:

https://codeberg.org/ziglang/zig/issues/35367
https://codeberg.org/ziglang/zig/issues/31272

(The second one is making me use the system linker for a number of projects until the 0.17 release is cut.)

Just based on what I’ve encountered and the improvements that have happened, I’d just try the current unreleased master of Zig and see how the linker improvements change things.