Skip to content

Intermittent native crash in Font constructor due to GC collecting DirectByteBuffer during STB native call #6117

@Thereallo1026

Description

@Thereallo1026

Description

When using a custom system font (Product Sans in my case), the game intermittently crashes with EXCEPTION_ACCESS_VIOLATION in lwjgl_stb.dll during font loading. The crash happens inside stbtt_PackFontRanges called from the Font constructor. It doesn't happen every launch, roughly maybe 1 in 3 times, which points to a GC timing issue rather than bad font data.

Root Cause Analysis

The crash originates in Font.java constructor, specifically at the stbtt_PackFontRanges call on line 62.

The LWJGL wrapper for this method works like this internally:

public static boolean stbtt_PackFontRanges(STBTTPackContext spc, ByteBuffer fontdata, int font_index, STBTTPackRange.Buffer ranges) {
    return nstbtt_PackFontRanges(spc.address(), memAddress(fontdata), font_index, ranges.address(), ranges.remaining()) != 0;
}

memAddress(fontdata) extracts the raw long pointer from the ByteBuffer. After that, only primitive long/int values are passed to the JNI native call. The JVM no longer sees any live object reference to the font data buffer during the native call, so it becomes eligible for garbage collection.

If G1 GC runs a concurrent cycle while STB is still reading font data through that pointer, the DirectByteBuffer's Cleaner frees the native memory, and STB hits an access violation trying to read from freed memory.

This is more likely to happen because:

  • Between stbtt_InitFont (line 34) and stbtt_PackFontRanges (line 62), there are ~13 .create() calls that allocate DirectByteBuffers, which can trigger GC
  • The stbtt_InitFont on line 34 also stores a pointer into the same buffer inside the fontInfo struct, but fontInfo is allocated with .create() and also has no reachability guarantee

Additionally, stbtt_InitFont return value is not checked. If it ever fails, the subsequent stbtt_PackFontRanges call will use invalid font info and crash deterministically.

Suggested Fix

Add Reference.reachabilityFence(buffer) after all native calls that use the buffer to prevent premature GC:

STBTruetype.stbtt_PackFontRanges(packContext, buffer, 0, packRange);
STBTruetype.stbtt_PackEnd(packContext);

// ...

scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height);

try (MemoryStack stack = MemoryStack.stackPush()) {
    IntBuffer ascent = stack.mallocInt(1);
    STBTruetype.stbtt_GetFontVMetrics(fontInfo, ascent, null, null);
    this.ascent = ascent.get(0);
}

Reference.reachabilityFence(buffer);

Or alternatively, store the buffer as a field in the Font class so it stays alive for the lifetime of the object.

Also worth adding:

  • Check the return value of stbtt_InitFont and handle failure gracefully
  • Free STB structs (fontInfo, packContext, packRange, cdata) after use since they currently leak native memory on every font load

Crash Log

# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffcf73fe131, pid=18072, tid=9192
#
# Problematic frame:
# C  [lwjgl_stb.dll+0x2e131]

Current thread (0x0000025c309bd840):  JavaThread "Render thread" [_thread_in_native, id=9192]

Native frames:
C  [lwjgl_stb.dll+0x2e131]

Java frames:
j  org.lwjgl.stb.STBTruetype.nstbtt_PackFontRanges(JJIJI)I+0
j  org.lwjgl.stb.STBTruetype.stbtt_PackFontRanges(Lorg/lwjgl/stb/STBTTPackContext;Ljava/nio/ByteBuffer;ILorg/lwjgl/stb/STBTTPackRange$Buffer;)Z+42
j  meteordevelopment.meteorclient.renderer.text.Font.<init>(Ljava/nio/ByteBuffer;I)V+292
j  meteordevelopment.meteorclient.systems.hud.HudRenderer.loadFont(I)Lmeteordevelopment/meteorclient/systems/hud/HudRenderer$FontHolder;+36
j  meteordevelopment.meteorclient.systems.hud.HudRenderer$$Lambda+0x00000008022c6b58.apply(Ljava/lang/Object;)Ljava/lang/Object;+7
j  meteordevelopment.meteorclient.systems.hud.elements.ActiveModulesHud.tick(Lmeteordevelopment/meteorclient/systems/hud/HudRenderer;)V+153
j  meteordevelopment.meteorclient.systems.hud.Hud.onTick(Lmeteordevelopment/meteorclient/events/world/TickEvent$Post;)V+84

siginfo: EXCEPTION_ACCESS_VIOLATION (0xc0000005), reading address 0x0000025c53be6098

Registers:
RAX=0x0000000000000578, RBX=0x0000025c11394534, RCX=0x0000000000000000, RDX=0x0000025c53be5b20

Environment

  • Meteor Client build 49 (1.21.11)
  • OpenJDK 21.0.3 (Microsoft build), G1 GC
  • JVM args: -Xms512m -Xmx5192m
  • Windows 11, Intel i5-12600 (12 cores), Intel UHD 770
  • Font: Product Sans Regular (system installed TTF, 109KB, valid header)

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