Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Tidy up addClassImage API and class <-> image management by jdolan · Pull Request #39 · jdolan/Objectively · GitHub
Skip to content

Tidy up addClassImage API and class <-> image management - #39

Merged
jdolan merged 5 commits into
mainfrom
cleanup/class-images
Aug 18, 2026
Merged

Tidy up addClassImage API and class <-> image management#39
jdolan merged 5 commits into
mainfrom
cleanup/class-images

Conversation

@jdolan

Copy link
Copy Markdown
Owner

No description provided.

The marker symbol created the failure it then had to guard against: an
image could only forget to declare itself because declaring itself was
required, and dlsym does not stop at the image it is given, so one that
forgot resolved a dependency's marker and was registered under the wrong
base address. Guarding that meant asking the loader to relate a handle to
an image, which is the platform code the marker was meant to retire.
There are only two sources of truth for which image is behind a handle:
the loader, or the caller. Take it from the caller. An application loads
an image in order to call into it, so it holds an address within that
image already, and passing it costs a parameter and no convention.
This retires OBJECTIVELY_CLASS_IMAGE, markerBelongsToImage, and the
reservation of Objectively as a Class name.
Identify class images by an exported marker symbol
removeClassImage was handed a handle and had to resolve it to the base
address that Classes record, which has no one spelling: Windows hands out
the module as the handle, glibc answers from the link map, and macOS,
having neither, matched the handle against every loaded image by opening
and closing each one in turn.
The platforms report a base address for an address, not for a handle. So
require an image that provides Classes to say so, with a marker symbol
that addClassImage resolves and asks dladdr about once, at registration.
removeClassImage then matches on the handle alone and imageForHandle is
gone, along with the mach-o and link map includes it needed.
The marker is shaped like an archetype and returns NULL, so that a lookup
of a Class named Objectively resolves it harmlessly rather than calling
something that is not an archetype. dlsym searches an image ahead of its
dependencies, so an image resolves its own marker rather than one from
the library it links against.
The registry becomes a list, which retires MAX_CLASS_IMAGES and the
assert that guarded it - a bounds check that compiled out under NDEBUG,
leaving the ninth image to write past the array.
Registering an image that declares no Classes, or unregistering one that
was never registered, now abort. Both were silent, and both leave behind
exactly the Classes this exists to remove.
Remove the Windows __sync_ shims
Nothing calls them since Objectively moved to the __atomic_ builtins,
which clang-cl provides directly, so the Interlocked wrappers behind
them have no remaining caller.
Verify the class image marker belongs to the image
dlsym does not stop at the image it is given, so an image that omits
OBJECTIVELY_CLASS_IMAGE resolves a dependency's marker instead of failing.
It was then registered under the dependency's base address, which made
removeClassImage unregister the dependency's Classes and leave its own
behind, reachable by name and about to be unmapped - the failure the
marker exists to prevent, reached silently.
Confirm the marker was defined by the image behind the handle, by asking
which image defines it and reopening that one RTLD_NOLOAD to compare.
Windows hands out the module as the handle, so there the base address
answers directly.
Retire image entries in place rather than unlinking and freeing them. A
concurrent classForName walks this list, and freeing a node out from
under it turned a stale read into a use after free. Retired entries are
skipped on lookup and freed at teardown.
Also tie the marker's definition to its lookup through one macro, so the
two cannot drift, and drop the remark claiming an image declares nothing.
Order Class.c to follow Class.h
removeClassImage preceded addClassImage because imageForHandle, its only
helper, sat directly above it. That helper is gone, and the pair had been
left reading backwards with markerBelongsToImage stranded between them.
Definitions now follow the order the header declares them in, and each
static helper sits directly above its first caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 18, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how Objectively tracks dynamically loaded “class images” (e.g., plugins) by making image identification explicit via an in-image address, and updates the internal bookkeeping used by classForName and image removal.

Changes:

  • Updated addClassImage API to accept both a dlopen handle and a trusted in-image address used to resolve the image base.
  • Replaced the fixed-size image registry with a linked-list of registered images and adjusted lookup/removal behavior accordingly.
  • Removed unused __sync_* interlock shims from the VS15 compatibility layer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

FileDescription
Sources/Objectively/Class.hUpdates addClassImage signature and clarifies documentation around image/address-based registration and removal behavior.
Sources/Objectively/Class.cImplements address-to-image-base resolution and replaces the image registry with a linked list used by classForName and removeClassImage.
Objectively.vs15/Sources/Windowly.hRemoves now-unused __sync_* declarations/macros.
Objectively.vs15/Sources/Windowly.cRemoves now-unused __sync_* wrapper implementations; minor region pragma alignment.
Suppressed comments (2)

Sources/Objectively/Class.c:262

  • removeClassImage mutates i->handle / i->image without any synchronization while classForName may read them. Even if nodes are never freed, these plain reads/writes are a data race in C. Use atomic loads/stores for the head pointer and for retiring a node’s fields.
 for (ClassImage *i = _images; i; i = i->next) {
if (i->handle == handle) {
image = i->image;
i->handle = NULL;
i->image = NULL;

Sources/Objectively/Class.c:305

  • classForName walks _images via non-atomic loads and reads i->handle directly. With concurrent addClassImage/removeClassImage, this can observe a partially-published node or race with retirement stores. Load the list head and each node’s handle atomically (acquire) before calling dlsym.
 for (ClassImage *i = _images; i && archetype == NULL; i = i->next) {
if (i->handle) {
archetype = dlsym(i->handle, s);
}
}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSources/Objectively/Class.c Outdated
jdolanand others added 4 commits August 17, 2026 21:15
classForName walks the image list on any thread, while addClassImage
prepended to it and removeClassImage wrote through it with plain stores.
Two registrations could lose one another, and a lookup could read a
half-written node.
Push with a compare and swap, as a Class is pushed, and retire an image
with a single release store of the handle the walk matches on, so that
walk sees an image or does not and never part of one. Nothing is unlinked
or freed before teardown, so a walk in progress always has a next.
removeClassImage's unlinking of Classes is still not atomic against a
concurrent registration, and two concurrent calls still race with each
other. Both remain the caller's to serialize, as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
removeClassImage splices _classes while classForName walks it. Unlinking
is a read and a write over a list another thread is traversing, and
making each store atomic does not make the pair of them one operation:
the head store could drop a registration published between them, and
clearing next could end a live walk early, hiding every Class behind it.
A mutex around the three operations on the list, taken only for the
walk, the push and the splice, and never across dlsym, dlopen or a Class
initializer, each of which can reenter _initialize. The atomics on
_classes go away with it.
_images keeps its atomics. It is only ever pushed to and retired in
place, never spliced, and it is walked while calling dlsym, which the
lock must not cover.
Verified under ThreadSanitizer, six threads looking up names against two
thousand register and unregister cycles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retiring an image and dropping its Classes were two steps with a gap
between them, so two calls for the same handle could both find it
registered, both proceed, and neither report the duplicate the abort is
there to catch. Hold the lock from the search through the last unlink.
Removing two different images was already safe, since each retires its
own entry and the unlinking was already serialized. This closes the
case of the same handle arriving twice, and makes that abort exact.
Nothing in this call reaches the loader, so the lock may cover all of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdolan
jdolan merged commit fbb3f8c into mainAug 18, 2026
4 checks passed
@jdolan
jdolan deleted the cleanup/class-images branch August 18, 2026 01:39
Sign up for freeto 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.

2 participants

@jdolan