Skip to content

Commit a7bb35d

Browse files
dcwhiteclaude
andauthored
Fix dyld crash on macOS < 15 from unguarded quick_exit reference (#2564)
* Fix dyld crash on macOS < 15 from unguarded quick_exit reference quick_exit only entered libSystem in macOS 15.0, and the SDK declares it with no availability annotation: void quick_exit(int) __dead2; // _stdlib.h It is gated on __DARWIN_C_LEVEL/C11 -- a compile-time feature gate, not an OS-version gate -- so the compiler has no version information to act on. It cannot warn (-Wunguarded-availability) and cannot weak-link. Building against the macOS 15 SDK with an older deployment target therefore emits a hard reference and links cleanly against the SDK's libSystem stub, but the binary aborts on load on macOS < 15: dyld: Symbol not found: _quick_exit Referenced from: .../lib/libInterface_Application.dylib Expected in: /usr/lib/libSystem.B.dylib Xcode 16 ships only the macOS 15 SDK, so on macOS 14 this silently produces a binary that cannot load on the machine that built it. SCIRun_test hit this via the two regression-mode exits added in a0e0954. Extract the call behind Core::quickExit, which falls back to _Exit when the deployment target is below macOS 15.0. _Exit is C99 and always present, and since nothing registers at_quick_exit handlers the two are equivalent here. The literal 150000 is deliberate rather than __MAC_15_0: that constant is undefined on SDKs older than 15 and would evaluate to 0, inverting the test and reintroducing the crash on exactly the configurations it guards. Verified on macOS 14.5 / Xcode 16.2: no _quick_exit references remain in the build, both preprocessor branches compile (min 14.5 -> _Exit, min 15.0 -> quick_exit), and lldb confirms the regression-mode path reaches _Exit and propagates its exit code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add macOS 14 GUI build with an executable smoke test CI had no job building the GUI on macOS < 15, for two independent reasons: every mac-gui job runs on macOS-latest (currently the macos-26 image), and the only macOS 14 jobs are headless -- where src/CMakeLists.txt:632 skips ADD_SUBDIRECTORY(Interface) entirely. So nothing compiled src/Interface against an older SDK and deployment target. That gap is what let the previous commit's bug ship green: a macOS-15-only libSystem symbol (quick_exit) linked cleanly on the macOS 26 runners and aborted at load on macOS 14. Add mac-gui-14-arm, pinned to the oldest available image (macos-13 is retired; macos-14 is deprecated but still offered). Pinned rather than tracking macOS-latest on purpose: the newest macOS is already covered several times over, and only the oldest image exercises a lower deployment target. Also add a run-smoke-test input that executes the built binary. A green build cannot detect this class of failure -- the dynamic loader resolves linked libraries before main, so a reference to a symbol the host lacks links cleanly and only aborts on execution. --help is sufficient, since the loader has already resolved every dependency by the time main runs, and it routes to ConsoleApplication (src/Main/scirunMain.cc) so no window server is needed. --version is deliberately not used: it currently falls through to GuiApplication and crashes on exit on macOS. Unlike the existing test steps, the smoke step omits continue-on-error: it must be able to fail the job, or it gates nothing. It selects SCIRun_test where present (built on APPLE only, per src/Main/CMakeLists.txt) and falls back to SCIRun elsewhere, failing loudly if neither exists. Verified against a deliberately reverted build: the smoke test exits 134 with the original "dyld: Symbol not found: _quick_exit", and exits 0 with the fix in place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Enable the smoke test on the macOS headless builds These jobs already build SCIRun_test -- src/Main/CMakeLists.txt adds it unconditionally under IF(APPLE), regardless of BUILD_HEADLESS -- so executing it costs seconds on top of an existing build and catches load-time failures the build itself cannot. Scope note: these jobs cannot catch the quick_exit class of bug on their own. Headless builds skip ADD_SUBDIRECTORY(Interface) (src/CMakeLists.txt), so they never compile the GUI libraries where that symbol was referenced; mac-gui-14-arm remains the only job covering those. What these add is the same load-time check over the Core/Dataflow/Modules dylibs, across macOS 14/15/26 and both arm64 and x86_64. --help routes to ConsoleApplication in headless builds (the BUILD_HEADLESS branch of src/Main/scirunMain.cc), so no window server is involved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Always take _Exit on Apple instead of gating on deployment target The version gate this replaces could not have worked. Nothing in the build sets CMAKE_OSX_DEPLOYMENT_TARGET, so __MAC_OS_X_VERSION_MIN_REQUIRED follows the build host and reads 150000 on exactly the macOS 15 builders that emit the hard _quick_exit reference. The trigger is the SDK, not the host: a macOS 14 machine with Xcode 16 installed builds the broken binary. Both spellings also fail to compile against SDKs older than 15, which do not declare quick_exit at all -- bare quick_exit as an undeclared identifier, std::quick_exit as a reference to an unresolved using declaration, since libc++'s <cstdlib> has no ::quick_exit to pull in. _Exit is equivalent here as nothing registers at_quick_exit handlers, so take it unconditionally on Apple and drop the version arithmetic entirely. Verified against the 13.3 and 15.2 SDKs: compiles on both, and nm -u shows __Exit with no _quick_exit reference. For contrast, std::quick_exit against the 15.2 SDK compiles clean and emits _quick_exit, which is the load-time failure on macOS < 15. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dcbe7d9 commit a7bb35d

6 files changed

Lines changed: 145 additions & 6 deletions

File tree

.github/workflows/mac.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ jobs:
3939
build-with-python: true
4040
artifact-name: SCIRunMacInstaller-26-intel
4141

42+
# Oldest available macOS image, built with the GUI. Every other mac-gui job runs
43+
# on macOS-latest (currently the macos-26 image), and the only macOS 14 jobs are
44+
# headless -- but src/CMakeLists.txt skips ADD_SUBDIRECTORY(Interface) when
45+
# BUILD_HEADLESS is on, so without this job nothing compiles src/Interface for an
46+
# older macOS at all. That gap let a macOS-15-only libSystem symbol (quick_exit)
47+
# link cleanly on the 26 runners and abort at load on macOS 14 (PR #2564).
48+
#
49+
# Pinned deliberately to the oldest image rather than macOS-latest: running the
50+
# newest macOS is already covered several times over, and only the oldest one
51+
# exercises the lower deployment target. macOS-14 is deprecated by GitHub, so when
52+
# it is retired this should be bumped to the next-oldest image, not deleted.
53+
mac-gui-14-arm:
54+
uses: ./.github/workflows/reusable-build.yml
55+
with:
56+
runner: macOS-14
57+
qt-version: '6.10.2'
58+
scirun-qt-min-version: '6.3.1'
59+
variant: gui
60+
build-with-python: true
61+
run-smoke-test: true
62+
4263
mac-gui-nonpython:
4364
uses: ./.github/workflows/reusable-build.yml
4465
with:
@@ -69,13 +90,15 @@ jobs:
6990
runner: macOS-latest
7091
variant: headless
7192
build-testing: true
93+
run-smoke-test: true
7294

7395
mac-headless-14-arm:
7496
uses: ./.github/workflows/reusable-build.yml
7597
with:
7698
runner: macOS-14
7799
variant: headless
78100
build-testing: true
101+
run-smoke-test: true
79102

80103
mac-headless-14-arm-slim:
81104
uses: ./.github/workflows/reusable-build.yml
@@ -85,13 +108,15 @@ jobs:
85108
build-testing: true
86109
build-with-python: false
87110
run-unit-tests: true
111+
run-smoke-test: true
88112

89113
mac-headless-15-arm:
90114
uses: ./.github/workflows/reusable-build.yml
91115
with:
92116
runner: macOS-15
93117
variant: headless
94118
build-testing: true
119+
run-smoke-test: true
95120

96121
mac-headless-15-arm-slim:
97122
uses: ./.github/workflows/reusable-build.yml
@@ -101,13 +126,15 @@ jobs:
101126
build-testing: true
102127
build-with-python: false
103128
run-unit-tests: true
129+
run-smoke-test: true
104130

105131
mac-headless-15-intel:
106132
uses: ./.github/workflows/reusable-build.yml
107133
with:
108134
runner: macOS-15-intel
109135
variant: headless
110136
build-testing: true
137+
run-smoke-test: true
111138

112139
mac-headless-15-intel-slim:
113140
uses: ./.github/workflows/reusable-build.yml
@@ -117,10 +144,12 @@ jobs:
117144
build-testing: true
118145
build-with-python: false
119146
run-unit-tests: true
147+
run-smoke-test: true
120148

121149
mac-headless-26:
122150
uses: ./.github/workflows/reusable-build.yml
123151
with:
124152
runner: macOS-26
125153
variant: headless
126154
build-testing: true
155+
run-smoke-test: true

.github/workflows/reusable-build.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ on:
3838
required: false
3939
type: boolean
4040
default: false
41+
run-smoke-test:
42+
required: false
43+
type: boolean
44+
default: false
4145
run-regression-tests:
4246
required: false
4347
type: boolean
@@ -203,6 +207,34 @@ jobs:
203207
204208
Pop-Location
205209
210+
- name: Smoke test - executable loads and runs (Unix)
211+
if: ${{ inputs.run-smoke-test && runner.os != 'Windows' }}
212+
working-directory: bin/SCIRun
213+
# A successful build does NOT prove the binary can start. The dynamic loader
214+
# resolves linked libraries at load time, before main, so a reference to a
215+
# symbol the host OS lacks links cleanly and only aborts on execution.
216+
# Actually running the executable is what catches that class of failure;
217+
# --help suffices, because the loader has already resolved every dependency
218+
# by the time main runs, and --help routes to ConsoleApplication (see
219+
# src/Main/scirunMain.cc) so no window server is needed.
220+
#
221+
# Deliberately NO continue-on-error: unlike the test steps below, this step
222+
# must be able to fail the job, or it gates nothing.
223+
run: |
224+
set -eux
225+
# SCIRun_test is the unbundled executable, built on APPLE only
226+
# (src/Main/CMakeLists.txt); other platforms build SCIRun directly.
227+
if [ -x ./SCIRun_test ]; then
228+
APP=./SCIRun_test
229+
elif [ -x ./SCIRun ]; then
230+
APP=./SCIRun
231+
else
232+
echo "No SCIRun executable found in $(pwd)" >&2
233+
ls -la
234+
exit 1
235+
fi
236+
"$APP" --help
237+
206238
- name: Package (optional - Unix/macOS)
207239
if: ${{ inputs.artifact-name != '' && runner.os != 'Windows' }}
208240
shell: bash

src/Core/Utils/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ SET(Core_Utils_HEADERS
3838
FileUtil.h
3939
Lockable.h
4040
Macros.h
41+
QuickExit.h
4142
Singleton.h
4243
StringContainer.h
4344
StringUtil.h

src/Core/Utils/QuickExit.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
For more information, please see: http://software.sci.utah.edu
3+
4+
The MIT License
5+
6+
Copyright (c) 2020 Scientific Computing and Imaging Institute,
7+
University of Utah.
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a
10+
copy of this software and associated documentation files (the "Software"),
11+
to deal in the Software without restriction, including without limitation
12+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
13+
and/or sell copies of the Software, and to permit persons to whom the
14+
Software is furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included
17+
in all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25+
DEALINGS IN THE SOFTWARE.
26+
*/
27+
28+
29+
#ifndef CORE_UTILS_QUICKEXIT_H
30+
#define CORE_UTILS_QUICKEXIT_H
31+
32+
#include <cstdlib>
33+
34+
namespace SCIRun
35+
{
36+
namespace Core
37+
{
38+
39+
/// Terminates the process immediately, skipping static destructors and atexit
40+
/// handlers, while still propagating @a code to the caller (e.g. CTest). Used in
41+
/// regression mode to avoid hangs and crashes in async teardown paths where
42+
/// streaming execution threads outlive the GUI objects.
43+
///
44+
/// Apple always takes _Exit so that one build runs on every supported macOS.
45+
/// quick_exit entered libSystem in macOS 15.0, which breaks both directions:
46+
///
47+
/// - Against SDKs older than 15, it is not declared at all. Both spellings fail to
48+
/// compile -- "quick_exit" as an undeclared identifier, "std::quick_exit" as a
49+
/// reference to an unresolved using declaration, since libc++'s <cstdlib> has no
50+
/// ::quick_exit to pull in.
51+
/// - Against the 15 SDK it is declared ("void quick_exit(int) __dead2;" in
52+
/// _stdlib.h) gated only on __DARWIN_C_LEVEL/C11 -- a compile-time feature gate
53+
/// carrying no availability annotation -- so the compiler can neither warn nor
54+
/// weak-link. The object gets a hard _quick_exit reference and aborts on load on
55+
/// macOS < 15 with "dyld: Symbol not found: _quick_exit".
56+
///
57+
/// The trigger is the SDK, not the host: a macOS 14 machine with Xcode 16 installed
58+
/// builds the broken binary. Gating on __MAC_OS_X_VERSION_MIN_REQUIRED does not help
59+
/// either, because the deployment target is not pinned -- it follows the build host,
60+
/// and so reads 150000 on exactly the builders that emit the hard reference.
61+
///
62+
/// The two branches are equivalent only so long as nothing registers at_quick_exit
63+
/// handlers: _Exit does not run them.
64+
[[noreturn]] inline void quickExit(int code)
65+
{
66+
#ifdef __APPLE__
67+
std::_Exit(code);
68+
#else
69+
std::quick_exit(code);
70+
#endif
71+
}
72+
73+
}}
74+
75+
#endif

src/Interface/Application/GuiCommands.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <numeric>
3232
#include <Core/Algorithms/Base/AlgorithmVariableNames.h>
3333
#include <Core/Application/Preferences/Preferences.h>
34+
#include <Core/Utils/QuickExit.h>
3435
#include <Interface/Application/SCIRunMainWindow.h>
3536
#include <Interface/Application/GuiCommands.h>
3637
#include <Interface/Application/GuiLogger.h>
@@ -268,10 +269,10 @@ bool NetworkFileProcessCommand::execute()
268269
if (failTestOnError() && Application::Instance().parameters()->isRegressionMode())
269270
{
270271
GuiLogger::logErrorStd("Regression import failed, exiting non-zero: " + filename);
271-
// Mirrors SCIRunMainWindow::exitApplication's regression-mode behavior
272-
// (which calls quick_exit) so the regression test reports a failure. Done
273-
// directly here because exitApplication is a private slot.
274-
std::quick_exit(1);
272+
// Mirrors SCIRunMainWindow::exitApplication's regression-mode behavior so the
273+
// regression test reports a failure. Done directly here because
274+
// exitApplication is a private slot.
275+
quickExit(1);
275276
}
276277
return false;
277278
}

src/Interface/Application/SCIRunMainWindowQtOverrides.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
#include <cstdlib>
2929
#include <Core/Application/Application.h>
30+
#include <Core/Utils/QuickExit.h>
3031
#include <Core/Application/Preferences/Preferences.h>
3132
#include <Core/Logging/Log.h>
3233
#include <Core/Utils/Legacy/MemoryUtil.h>
@@ -66,12 +67,12 @@ void SCIRunMainWindow::exitApplication(int code)
6667
if (Application::Instance().parameters()->saveViewSceneScreenshotsOnQuit())
6768
{ networkEditor_->saveImages(); }
6869
returnCode_ = code;
69-
// In regression mode, use quick_exit to avoid hangs/crashes in async teardown
70+
// In regression mode, exit immediately to avoid hangs/crashes in async teardown
7071
// paths where streaming execution threads outlive the GUI objects (e.g. the
7172
// async streaming test networks). The exit code is still propagated to CTest.
7273
if (Application::Instance().parameters()->isRegressionMode())
7374
{
74-
std::quick_exit(code);
75+
quickExit(code);
7576
}
7677
close();
7778
qApp->exit(code);

0 commit comments

Comments
 (0)