From cf6ec8556f48cc9221c4b993f471c587bbf842d6 Mon Sep 17 00:00:00 2001 From: Todd White Date: Tue, 30 Jun 2026 08:07:15 -0400 Subject: [PATCH] Acquire the runtime lock before the selector table lock during registration 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/libobjc2#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. --- selector_table.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/selector_table.cc b/selector_table.cc index 723e33d0..5fc68af8 100644 --- a/selector_table.cc +++ b/selector_table.cc @@ -447,6 +447,15 @@ extern "C" PRIVATE SEL objc_register_selector(SEL aSel) return registered; } assert(!(aSel->types && (strstr(aSel->types, "@\"") != nullptr))); + // Acquire the runtime lock before the selector table lock. Registering a + // new selector resizes the dtables (objc_resize_dtables), which takes the + // runtime lock, while the class loader (__objc_load) takes the runtime lock + // first and then registers selectors. Acquiring the two in a consistent + // runtime-before-table order avoids a lock-order inversion + // (gnustep/libobjc2#391). Holding the runtime lock for the whole + // registration also keeps the new selector's index (the selector_list size) + // and the matching dtable resize atomic with respect to other resizes. + LOCK_RUNTIME_FOR_SCOPE(); LockGuard g{selector_table_lock}; register_selector_locked(aSel); return aSel; @@ -463,6 +472,9 @@ SEL objc_register_selector_copy(UnregisteredSelector &aSel, BOOL copyArgs) { return copy; } + // Runtime lock before the selector table lock, for the duration of the + // registration; see objc_register_selector above and gnustep/libobjc2#391. + LOCK_RUNTIME_FOR_SCOPE(); LockGuard g{selector_table_lock}; copy = selector_lookup(aSel.name, aSel.types); if (nullptr != copy && selector_identical(aSel, copy))