Skip to content

Fix #23979: GC never finalizes a class whose only destructors are field destructors - #23980

Merged
thewilsonator merged 2 commits into
dlang:masterfrom
flashburns:fix-class-field-dtor-finalize
Oct 7, 2026
Merged

thewilsonator merged 2 commits into
dlang:masterfrom
flashburns:fix-class-field-dtor-finalize

Conversation

@flashburns

@flashburns flashburns commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23979.

_d_newclassT only set BlkAttr.FINALIZE when the class had a user-declared destructor (__dtor). A class whose destruction comes only from its fields has just the compiler-generated __xdtor, so the GC never finalized it. This also checks for __xdtor, which matches what the old _d_newclass did by checking ClassFlags.hasDtor.

The new unittest sits next to the existing _d_newclassT tests and checks the block's FINALIZE attribute for a field-only class, a class with ~this(), and a plain class. It fails on master without the fix, and druntime's unittests pass with it.

Two things worth knowing when reviewing:

…e field destructors

`_d_newclassT` marked a new class instance as finalizable only when the
class had a user-declared destructor (`__dtor`). A class whose destruction
comes solely from its fields only gets a compiler-generated `__xdtor`, so it
was allocated without `BlkAttr.FINALIZE` and the GC never ran its fields'
destructors, although `destroy` and `scope` worked.

This regressed in 2.103.0 when `_d_newclass` became a template (dlang#14837);
the old runtime function checked `ClassFlags.hasDtor`, which includes field
destructors. Also check for `__xdtor`.

@rainers rainers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I'm not sure whether the failing tests are spurious or whether this uncovers an issue with the runtime shared across DLLs or with the tests.

I've restarted the failing builds.

@limepoutine

Copy link
Copy Markdown
Contributor

#22399 appears to be biting again? Now dllgc.Task gets a class destructor because core.sync.event.Event has one, but it is already gone by the time GC tries to run finalizers.

@flashburns

flashburns commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

This looks like a real bug and not a flaky test. Both Windows jobs crash in druntime/test/shared/src/dllgc.d with an access violation, in debug and release:
https://dev.azure.com/dlanguage/16224964-ae9b-4642-95fd-2a4ad31ca201/_apis/build/builds/55746/logs/98
https://dev.azure.com/dlanguage/16224964-ae9b-4642-95fd-2a4ad31ca201/_apis/build/builds/55746/logs/92

My guess is that the test's Task class has no ~this() but has an Event field, so this PR makes it finalizable. The only reference to it is held by its low-level thread, which the GC doesn't scan, so the GC collects it while the thread is still running. Before my change, the destructor didn't run, so nothing happened.

I tried to reproduce the Windows crash under Wine, but Wine does something different and the test passes there. I don't have a Windows setup, so if anyone can confirm on real Windows, I'd appreciate it. Otherwise I can push a change that keeps the task in a __gshared variable and let CI check it.

@flashburns

Copy link
Copy Markdown
Contributor Author

Would it be ok for me to push a untested fix and have the CI run it without having to setup windows locally for testing?

…druntime

rt_termSharedModule did not run the finalizers whose code is in the module
being unloaded, so the GC later called into the unmapped DLL and crashed.
Run them after the module destructors, as the ELF implementation does.

This affected any finalizable object whose class is defined in such a DLL;
druntime/test/shared/src/dllgc.d started hitting it once classes with only
field destructors became finalizable.
@thewilsonator

Copy link
Copy Markdown
Contributor

Of course.

@rainers

rainers commented Oct 7, 2026

Copy link
Copy Markdown
Member

I could not reproduce it locally on Windows, but I also think the Task is missing a reference to keep it alive. So saving it into a global variable might help.

@thewilsonator
thewilsonator merged commit 551b902 into dlang:master Oct 7, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REG 2.103.0] GC never finalizes a class whose only destructors are field destructors

4 participants