Skip to content
Open
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
54 changes: 52 additions & 2 deletions daemon/init/source/InitMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@

#endif


// Signal number received by DobbyInit, set by the signal handler so the
// main code path can propagate the signal death after children have exited.
static volatile sig_atomic_t gReceivedSignal = 0;

static void closeAllFileDescriptors(int logPipeFd)
{
Expand Down Expand Up @@ -344,8 +346,32 @@ static int doForkExec(int argc, char * argv[])
if (pid == exePid)
{
ret = WEXITSTATUS(status);

// If the main child exited normally, clear any signal
// recorded by the signal handler (e.g. SIGUSR1 used
// for app control) so we don't falsely report a
// signal death.
if (ret == EXIT_SUCCESS)
gReceivedSignal = 0;
}
}
else if (WIFSIGNALED(status) && pid == exePid)
{
// Direct child was killed by a signal — record it so
// the deferred _exit(128+sig) path propagates it to
// DobbyDaemon after all remaining children are reaped.
// Only set if signal handler hasn't already recorded a
// signal, to preserve the first (root cause) signal.
int sig = WTERMSIG(status);
if (gReceivedSignal == 0)
gReceivedSignal = sig;
ret = EXIT_FAILURE;

// The main child's orphaned descendants have been
// reparented to us (PID 1). Send them the same signal
// so they terminate and we don't block in wait() forever.
kill(-1, sig);
}

// if the process died because of a signal, or it didn't exit with
// success then log as an error, otherwise it's just info
Expand All @@ -369,12 +395,36 @@ static int doForkExec(int argc, char * argv[])

#endif

// If DobbyInit was signalled, exit with code 128+signal
// so the parent process (DobbyDaemon) can reconstruct the signal info.
//
// NOTE: We cannot use the conventional approach of resetting to SIG_DFL
// and calling raise() because DobbyInit is PID 1 inside the container's
// PID namespace. The Linux kernel protects namespace init (PID 1) from
// signals with SIG_DFL disposition sent from within the same namespace -
// including self-signals via raise(). The kernel simply drops the signal,
// so raise() returns without killing the process.
//
// Instead, we use the shell convention of _exit(128 + signum). The
// DobbyDaemon side detects this exit code pattern and synthesises the
// equivalent WIFSIGNALED wait status.
if (gReceivedSignal != 0)
{
int sig = gReceivedSignal;
LOG_NFO("DobbyInit received signal %d (%s), exiting with code %d",
sig, strsignal(sig), 128 + sig);
_exit(128 + sig);
}

return ret;
}

static void signalHandler(int sigNum)
{
// consume the signal but passes it onto all processes in the container
// record which signal we received so the main code path can propagate it
gReceivedSignal = sigNum;

// forward the signal to all processes in the container
kill(-1, sigNum);
}
Comment thread
ks734 marked this conversation as resolved.

Expand Down
69 changes: 69 additions & 0 deletions daemon/lib/source/DobbyManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3013,6 +3013,55 @@ bool DobbyManager::onPreDestructionHook(const ContainerId &id,
}
#endif //defined(LEGACY_COMPONENTS)

// -----------------------------------------------------------------------------
/**
* @brief Translates a raw wait status from DobbyInit into a synthesised
* WIFSIGNALED-style status when the exit code matches the 128+signum
* convention used by DobbyInit to propagate signal death info.
*
* DobbyInit is PID 1 inside the container's PID namespace and cannot be
* killed by a self-raised signal (the kernel drops signals with SIG_DFL
* disposition for namespace init). Instead DobbyInit exits with code
* 128+signum when it receives a signal. This helper detects that pattern
* and synthesises the equivalent WIFSIGNALED wait status so the rest of
* the code sees the true cause of death.
*
* @param[in] rawStatus The raw wait status from waitpid().
*
* @return Possibly-modified wait status.
*/
int DobbyManager::synthesizeContainerSignalStatus(int rawStatus)
{
if (WIFEXITED(rawStatus))
{
int exitCode = WEXITSTATUS(rawStatus);
if (exitCode > 128 && exitCode < 128 + NSIG)
{
int sig = exitCode - 128;

// Returns true for signals whose default action produces a
// core dump on Linux.
auto signalDumpsCore = [](int s) -> bool {
switch (s) {
case SIGABRT: case SIGSEGV: case SIGFPE:
case SIGILL: case SIGBUS: case SIGQUIT:
return true;
default:
return false;
}
};

// Synthesise WIFSIGNALED status: signal number in bits 0-6,
// bit 7 (WCOREDUMP) set for core-dumping signals.
int status = sig & 0x7f;
if (signalDumpsCore(sig))
status |= 0x80;
return status;
}
}
return rawStatus;
}

/**
* @brief Perform all the necessary cleanup and run plugins required when
* a container has terminated.
Expand Down Expand Up @@ -3178,6 +3227,26 @@ void DobbyManager::onChildExit()
{
const ContainerId &id = it->first;

// DobbyInit is PID 1 inside the container's PID namespace and
// cannot be killed by a self-raised signal (the kernel drops
// signals with SIG_DFL disposition for namespace init). Instead
// DobbyInit exits with code 128+signum when it receives a signal.
// Detect that convention here and synthesise a WIFSIGNALED-style
// wait status so the rest of the code sees the true cause of death.
{
int origStatus = status;
status = synthesizeContainerSignalStatus(status);
if (status != origStatus)
{
int exitCode = WEXITSTATUS(origStatus);
int sig = exitCode - 128;
AI_LOG_INFO("container '%s' exited with code %d, "
"interpreting as killed by signal %d (%s) "
"(PID 1 namespace init convention)",
id.c_str(), exitCode, sig, strsignal(sig));
}
}

AI_LOG_INFO("runc for container '%s' has quit (pid:%d status:0x%04x)",
id.c_str(), containerPid, status);

Expand Down
6 changes: 6 additions & 0 deletions daemon/lib/source/include/DobbyManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ class DobbyManager
bool createBundle(const ContainerId& id, const std::string& jsonSpec);
#endif //defined(LEGACY_COMPONENTS)

public:
// Translates a raw wait status whose exit code matches the 128+signum
// convention (used by DobbyInit) into a synthesised WIFSIGNALED status.
// Returns the status unchanged for normal exits or already-signalled statuses.
static int synthesizeContainerSignalStatus(int rawStatus);

private:
void handleContainerTerminate(const ContainerId &id, const std::unique_ptr<DobbyContainer>& container, const int status);
void onChildExit();
Expand Down
195 changes: 195 additions & 0 deletions tests/L1_testing/tests/DobbyManagerTest/DaemonDobbyManagerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4417,5 +4417,200 @@ TEST_F(DaemonDobbyManagerTest, hibernateContainer_successWithParametersCombinati
}
}

// =============================================================================
// Unit tests for DobbyManager::synthesizeContainerSignalStatus()
//
// These validate the 128+signum exit code to WIFSIGNALED status synthesis
// that bridges the DobbyInit (PID 1) convention to standard wait-status.
// =============================================================================

// Helper: encode a normal _exit(code) as a raw waitpid() status word.
// On Linux the encoding is (exitCode << 8) with bits 0-6 = 0.
static int makeExitStatus(int exitCode)
{
return (exitCode & 0xff) << 8;
}

// ---------------------------------------------------------------------------
// Normal exit codes - must pass through unchanged
// ---------------------------------------------------------------------------

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_NormalExitZero_Unchanged)
{
int raw = makeExitStatus(0);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);
EXPECT_EQ(out, raw);
EXPECT_TRUE(WIFEXITED(out));
EXPECT_EQ(WEXITSTATUS(out), 0);
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_NormalExitOne_Unchanged)
{
int raw = makeExitStatus(1);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);
EXPECT_EQ(out, raw);
EXPECT_TRUE(WIFEXITED(out));
EXPECT_EQ(WEXITSTATUS(out), 1);
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode128_Unchanged)
{
// 128 is NOT > 128, so it should NOT be synthesised.
int raw = makeExitStatus(128);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);
EXPECT_EQ(out, raw);
EXPECT_TRUE(WIFEXITED(out));
EXPECT_EQ(WEXITSTATUS(out), 128);
}

// ---------------------------------------------------------------------------
// 128+signum exit codes - core-dumping signals
// ---------------------------------------------------------------------------

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode134_SIGABRT)
{
// 128 + 6 = 134 -> SIGABRT (core-dumping signal)
int raw = makeExitStatus(134);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGABRT);
#ifdef WCOREDUMP
EXPECT_TRUE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode139_SIGSEGV)
{
// 128 + 11 = 139 -> SIGSEGV (core-dumping signal)
int raw = makeExitStatus(139);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGSEGV);
#ifdef WCOREDUMP
EXPECT_TRUE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode136_SIGFPE)
{
// 128 + 8 = 136 -> SIGFPE (core-dumping signal)
int raw = makeExitStatus(136);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGFPE);
#ifdef WCOREDUMP
EXPECT_TRUE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode132_SIGILL)
{
// 128 + 4 = 132 -> SIGILL (core-dumping signal)
int raw = makeExitStatus(132);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGILL);
#ifdef WCOREDUMP
EXPECT_TRUE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode131_SIGQUIT)
{
// 128 + 3 = 131 -> SIGQUIT (core-dumping signal)
int raw = makeExitStatus(131);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGQUIT);
#ifdef WCOREDUMP
EXPECT_TRUE(WCOREDUMP(out));
#endif
}

// ---------------------------------------------------------------------------
// 128+signum exit codes - non-core-dumping signals
// ---------------------------------------------------------------------------

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode137_SIGKILL)
{
// 128 + 9 = 137 -> SIGKILL (no core dump)
int raw = makeExitStatus(137);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGKILL);
#ifdef WCOREDUMP
EXPECT_FALSE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode143_SIGTERM)
{
// 128 + 15 = 143 -> SIGTERM (no core dump)
int raw = makeExitStatus(143);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGTERM);
#ifdef WCOREDUMP
EXPECT_FALSE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode130_SIGINT)
{
// 128 + 2 = 130 -> SIGINT (no core dump)
int raw = makeExitStatus(130);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGINT);
#ifdef WCOREDUMP
EXPECT_FALSE(WCOREDUMP(out));
#endif
}

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCode129_SIGHUP)
{
// 128 + 1 = 129 -> SIGHUP (no core dump)
int raw = makeExitStatus(129);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);

EXPECT_TRUE(WIFSIGNALED(out));
EXPECT_EQ(WTERMSIG(out), SIGHUP);
#ifdef WCOREDUMP
EXPECT_FALSE(WCOREDUMP(out));
#endif
}

// ---------------------------------------------------------------------------
// Boundary: exit code above signal range - must NOT be synthesised
// ---------------------------------------------------------------------------

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_ExitCodeAboveSignalRange_Unchanged)
{
// 128 + NSIG is out of range (signals go from 1 to NSIG-1)
int raw = makeExitStatus(128 + NSIG);
int out = DobbyManager::synthesizeContainerSignalStatus(raw);
EXPECT_EQ(out, raw);
EXPECT_TRUE(WIFEXITED(out));
}

// ---------------------------------------------------------------------------
// Already-signalled status (not WIFEXITED) - must pass through unchanged
// ---------------------------------------------------------------------------

TEST_F(DaemonDobbyManagerTest, synthesizeContainerSignalStatus_AlreadySignalled_Unchanged)
{
// A raw WIFSIGNALED status with signal 9 (SIGKILL): bits 0-6 = 9
int raw = SIGKILL; // 9 - WIFSIGNALED true, WTERMSIG = 9
ASSERT_TRUE(WIFSIGNALED(raw));
int out = DobbyManager::synthesizeContainerSignalStatus(raw);
EXPECT_EQ(out, raw);
}

Loading