Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,7 @@ endif()

# macOS app bundle
if(APPLE)
target_sources(AetherSDR PRIVATE src/MacStartupAbortGuard.cpp)
set_target_properties(AetherSDR PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER "com.aethersdr.AetherSDR"
Expand Down Expand Up @@ -2875,6 +2876,16 @@ add_executable(radio_status_ownership_test
target_include_directories(radio_status_ownership_test PRIVATE src)
target_link_libraries(radio_status_ownership_test PRIVATE Qt6::Core)
enable_testing()

if(APPLE)
add_executable(mac_startup_abort_guard_test
tests/mac_startup_abort_guard_test.cpp
src/MacStartupAbortGuard.cpp
)
target_include_directories(mac_startup_abort_guard_test PRIVATE src)
add_test(NAME mac_startup_abort_guard_test COMMAND mac_startup_abort_guard_test)
endif()

add_test(NAME radio_status_ownership_test COMMAND radio_status_ownership_test)

# Approved V12 cross-needle meter construction: versioned face resource,
Expand Down
55 changes: 55 additions & 0 deletions src/MacStartupAbortGuard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "MacStartupAbortGuard.h"

#include <unistd.h>

namespace {

constexpr char kStartupAbortMessage[] =
"AetherSDR startup error: macOS aborted GUI initialization. "
"GUI services may be unavailable (for example, in a restricted sandbox). "
"Launch AetherSDR from Finder or a normal Terminal session.\n";

void handleStartupAbort(int)
{
// QApplication construction can abort from inside AppKit/HIServices before
// Qt logging exists. Only async-signal-safe operations are valid here.
const ssize_t ignored = write(STDERR_FILENO,
kStartupAbortMessage,
sizeof(kStartupAbortMessage) - 1);
(void)ignored;
_exit(AetherSDR::MacStartupAbortGuard::kFailureExitCode);
}

} // namespace

namespace AetherSDR {

MacStartupAbortGuard::MacStartupAbortGuard()
{
struct sigaction action {};
action.sa_handler = handleStartupAbort;
sigemptyset(&action.sa_mask);

m_armed = sigaction(SIGABRT, &action, &m_previousAction) == 0;
}

MacStartupAbortGuard::~MacStartupAbortGuard()
{
(void)disarm();
}

bool MacStartupAbortGuard::disarm()
{
if (!m_armed) {
return true;
}

if (sigaction(SIGABRT, &m_previousAction, nullptr) != 0) {
return false;
}

m_armed = false;
return true;
}

} // namespace AetherSDR
29 changes: 29 additions & 0 deletions src/MacStartupAbortGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <signal.h>

namespace AetherSDR {

// Converts a macOS abort during QApplication construction into a normal
// startup failure. The guard must be disarmed as soon as construction succeeds
// so aborts from the running application retain their normal disposition.
class MacStartupAbortGuard final
{
public:
static constexpr int kFailureExitCode = 1;

MacStartupAbortGuard();
~MacStartupAbortGuard();

MacStartupAbortGuard(const MacStartupAbortGuard&) = delete;
MacStartupAbortGuard& operator=(const MacStartupAbortGuard&) = delete;

bool isArmed() const { return m_armed; }
bool disarm();

private:
struct sigaction m_previousAction {};
bool m_armed{false};
};

} // namespace AetherSDR
28 changes: 28 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
#include "core/MacMicPermission.h"
#include "core/AutomationServer.h"

#ifdef Q_OS_MAC
#include "MacStartupAbortGuard.h"
#endif

#include <QApplication>
#include <QSurfaceFormat>
#include <memory>
Expand All @@ -20,6 +24,8 @@
#include <QDateTime>
#include <QStandardPaths>
#include <QTimer>
#include <cstdio>
#include <cstdlib>

#ifdef _WIN32
#include <io.h>
Expand Down Expand Up @@ -177,8 +183,30 @@ int main(int argc, char* argv[])
// Together with WA_DontCreateNativeAncestors on those leaves, this avoids
// redundant window-sized Core Animation backing stores (#4339).
QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);

// A restricted launcher can deny QCocoa access to macOS GUI services.
// AppKit then calls abort() from HIServices while QApplication is being
// constructed. Scope the handler to this constructor only: runtime aborts
// must keep their normal crash semantics.
AetherSDR::MacStartupAbortGuard startupAbortGuard;
if (!startupAbortGuard.isArmed()) {
std::fputs("AetherSDR startup error: could not install the macOS GUI "
"initialization guard; refusing to start.\n",
stderr);
return EXIT_FAILURE;
}
#endif
QApplication app(argc, argv);

#ifdef Q_OS_MAC
if (!startupAbortGuard.disarm()) {
std::fputs("AetherSDR startup error: could not restore the SIGABRT "
"handler after GUI initialization; refusing to continue.\n",
stderr);
return EXIT_FAILURE;
}
#endif

app.setApplicationName("AetherSDR");
app.setApplicationVersion(AETHERSDR_VERSION);
app.setOrganizationName("AetherSDR");
Expand Down
116 changes: 116 additions & 0 deletions tests/mac_startup_abort_guard_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include "MacStartupAbortGuard.h"

#include <csignal>
#include <cstdlib>
#include <cstdio>
#include <string>

#include <sys/wait.h>
#include <unistd.h>

namespace {

volatile sig_atomic_t g_previousHandlerCalled = 0;

void previousAbortHandler(int)
{
g_previousHandlerCalled = 1;
}

int fail(const char* message)
{
std::fprintf(stderr, "FAIL: %s\n", message);
return 1;
}

} // namespace

int main()
{
int pipeFds[2] {};
if (pipe(pipeFds) != 0) {
return fail("could not create stderr capture pipe");
}

const pid_t child = fork();
if (child < 0) {
close(pipeFds[0]);
close(pipeFds[1]);
return fail("could not fork guarded child");
}

if (child == 0) {
close(pipeFds[0]);
if (dup2(pipeFds[1], STDERR_FILENO) < 0) {
_exit(90);
}
close(pipeFds[1]);

AetherSDR::MacStartupAbortGuard guard;
if (!guard.isArmed()) {
_exit(91);
}
std::abort();
_exit(92);
}

close(pipeFds[1]);
std::string errorText;
char buffer[512];
ssize_t bytesRead = 0;
while ((bytesRead = read(pipeFds[0], buffer, sizeof(buffer))) > 0) {
Comment thread
jensenpat marked this conversation as resolved.
errorText.append(buffer, static_cast<std::size_t>(bytesRead));
}
close(pipeFds[0]);

int childStatus = 0;
if (waitpid(child, &childStatus, 0) != child) {
return fail("could not wait for guarded child");
}
if (!WIFEXITED(childStatus)
|| WEXITSTATUS(childStatus) != AetherSDR::MacStartupAbortGuard::kFailureExitCode) {
return fail("guarded SIGABRT did not become exit code 1");
}
if (errorText.find("AetherSDR startup error: macOS aborted GUI initialization")
== std::string::npos) {
return fail("guarded SIGABRT did not emit the startup error");
}

struct sigaction originalAction {};
if (sigaction(SIGABRT, nullptr, &originalAction) != 0) {
return fail("could not read the original SIGABRT action");
}

struct sigaction previousAction {};
previousAction.sa_handler = previousAbortHandler;
sigemptyset(&previousAction.sa_mask);
if (sigaction(SIGABRT, &previousAction, nullptr) != 0) {
return fail("could not install the previous SIGABRT action");
}

int result = 0;
{
AetherSDR::MacStartupAbortGuard guard;
if (!guard.isArmed()) {
result = fail("startup guard did not arm");
} else if (!guard.disarm()) {
result = fail("startup guard did not disarm");
}
}

if (result == 0) {
raise(SIGABRT);
if (g_previousHandlerCalled != 1) {
result = fail("disarm did not restore the previous SIGABRT action");
}
}

if (sigaction(SIGABRT, &originalAction, nullptr) != 0 && result == 0) {
result = fail("could not restore the original SIGABRT action");
}

if (result == 0) {
std::puts("macOS startup abort guard: PASS");
}
return result;
}
Loading