Fix lock-order inversion between the runtime lock and the selector table lock#394
Merged
davidchisnall merged 1 commit intoJul 1, 2026
Conversation
…ration Registering a new selector resizes the dtables, and objc_resize_dtables takes the runtime lock while the selector table lock is already held. The class loader (__objc_load) does the opposite: it holds the runtime lock and then registers selectors, taking the selector table lock. These two orders form a lock-order inversion between the runtime mutex and the selector table lock (gnustep#391), reachable when a dynamic class load races with a selector registration that triggers a dtable resize. Acquire the runtime lock before the selector table lock in the two registration entry points so both paths take the locks in the same runtime-before-table order. This also keeps the new selector's index (the selector_list size) and the matching dtable resize atomic with respect to other resizes, which registration relies on. The runtime lock is recursive, so the nested acquisition inside objc_resize_dtables is unaffected, and the already-registered fast paths still take neither lock.
davidchisnall
approved these changes
Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #391.
Registering a new selector resizes the dtables, and
objc_resize_dtablestakes the runtime lock (dtable.c:618) while the selector table lock is already held (objc_register_selector_copy→register_selector_locked). The class loader does the opposite —__objc_loadholds the runtime lock (loader.c:186) and then registers selectors, taking the selector table lock (loader.c:217→selector_lookup). That's theruntime_mutex⇄selector_table_lockcycle, reachable when a concurrentdlopenof an Objective-C module races a selector registration that triggers a dtable resize.This holds the runtime lock for the duration of registration, acquiring it before the selector table lock in both registration entry points, so every path takes the two locks in the same
runtime → selectororder. It also keeps the new selector's index (theselector_listsize) and the matching dtable resize atomic with respect to other resizes, which registration relies on. The runtime lock is recursive, so the nested acquisition insideobjc_resize_dtablesis unaffected, and the already-registered fast paths still take neither lock.On the dedicated-lock idea from #391
I started down the "new lock that protects dtable resizes" path, but
objc_resize_dtableswalks the class table (class_table_next), andclass_table_insert/class_table_nexthave no locking of their own — they rely on the caller holding the runtime lock. So a resize can't move off the runtime lock without also moving all class-table protection onto the new lock, which cascades into decomposing the big runtime lock more broadly (the audit you mentioned). The lock that protects dtable resizes today is the runtime lock; this change does the minimal version of that suggestion — hold it for the duration of registration — using the lock that already guards the resize. Happy to pursue the finer-grained decomposition separately if you'd prefer.Validation
sel_registerName+ class load at startup): clean master reports the inversion deterministically (2 reports); with this change, 0. Harness runs to completion both ways.ctestsuite 194/194 pass, includingManyManySelectors.The remaining TSan data races on the
selector_listsize are a separate, pre-existing unsynchronized-read finding and are not addressed here.