From 905a97bdd2b430e6193e30b15dcf4dacfc19116e Mon Sep 17 00:00:00 2001 From: ks734 Date: Fri, 17 Apr 2026 07:57:42 +0000 Subject: [PATCH 1/5] RDKEMW-12824: Propagate signal from DobbyInit to DobbyDaemon via 128+sig exit code --- daemon/init/source/InitMain.cpp | 30 +++++++++++++++++++++-- daemon/lib/source/DobbyManager.cpp | 39 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/daemon/init/source/InitMain.cpp b/daemon/init/source/InitMain.cpp index 66a44e0e..05fcd375 100644 --- a/daemon/init/source/InitMain.cpp +++ b/daemon/init/source/InitMain.cpp @@ -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) { @@ -369,12 +371,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); } diff --git a/daemon/lib/source/DobbyManager.cpp b/daemon/lib/source/DobbyManager.cpp index 6a08373f..3f799387 100644 --- a/daemon/lib/source/DobbyManager.cpp +++ b/daemon/lib/source/DobbyManager.cpp @@ -3178,6 +3178,45 @@ 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. + if (WIFEXITED(status)) + { + int exitCode = WEXITSTATUS(status); + if (exitCode > 128 && exitCode <= 128 + 64) + { + 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)); + + // Synthesise WIFSIGNALED status: signal number in bits 0-6. + // Also set bit 7 (WCOREDUMP) for signals that conventionally + // produce a core dump (SIGABRT, SIGSEGV, SIGFPE, SIGILL, + // SIGBUS, SIGQUIT) so that WCOREDUMP(status) returns true. + // Note: no actual core file is written since DobbyInit used + // _exit(); this is a best-effort convention match only. + auto signalDumpsCore = [](int s) -> bool { + switch (s) { + case SIGABRT: case SIGSEGV: case SIGFPE: + case SIGILL: case SIGBUS: case SIGQUIT: + return true; + default: + return false; + } + }; + + status = sig & 0x7f; + if (signalDumpsCore(sig)) + status |= 0x80; + } + } + AI_LOG_INFO("runc for container '%s' has quit (pid:%d status:0x%04x)", id.c_str(), containerPid, status); From 90e5304248d9784eb5ede5c3b2905f1446f16349 Mon Sep 17 00:00:00 2001 From: Karthick Swaminathan <85346280+ks734@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:36:20 +0530 Subject: [PATCH 2/5] RDKEMW-12824: Propagate direct child signal death to DobbyDaemon --- daemon/init/source/InitMain.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/daemon/init/source/InitMain.cpp b/daemon/init/source/InitMain.cpp index 05fcd375..2ee547b0 100644 --- a/daemon/init/source/InitMain.cpp +++ b/daemon/init/source/InitMain.cpp @@ -348,6 +348,15 @@ static int doForkExec(int argc, char * argv[]) ret = WEXITSTATUS(status); } } + 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. + int sig = WTERMSIG(status); + gReceivedSignal = sig; + ret = EXIT_FAILURE; + } // 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 From a8dbcbc5bbe77f732ed8409da762caee331b4245 Mon Sep 17 00:00:00 2001 From: Karthick Swaminathan <85346280+ks734@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:15:27 +0530 Subject: [PATCH 3/5] RDKEMW-12824: Fix copilot comments --- daemon/init/source/InitMain.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/daemon/init/source/InitMain.cpp b/daemon/init/source/InitMain.cpp index 2ee547b0..245413c3 100644 --- a/daemon/init/source/InitMain.cpp +++ b/daemon/init/source/InitMain.cpp @@ -346,6 +346,13 @@ 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) @@ -353,9 +360,17 @@ static int doForkExec(int argc, char * argv[]) // 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); - gReceivedSignal = sig; + 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 From 9490c09c4b15692101f4b3650a5961c5f6e7de3f Mon Sep 17 00:00:00 2001 From: Karthick Swaminathan <85346280+ks734@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:27:58 +0530 Subject: [PATCH 4/5] RDKEMW-12824: Use NSIG instead of hard-coded 64 for signal range --- daemon/lib/source/DobbyManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/lib/source/DobbyManager.cpp b/daemon/lib/source/DobbyManager.cpp index 3f799387..624d2059 100644 --- a/daemon/lib/source/DobbyManager.cpp +++ b/daemon/lib/source/DobbyManager.cpp @@ -3187,7 +3187,7 @@ void DobbyManager::onChildExit() if (WIFEXITED(status)) { int exitCode = WEXITSTATUS(status); - if (exitCode > 128 && exitCode <= 128 + 64) + if (exitCode > 128 && exitCode < 128 + NSIG) { int sig = exitCode - 128; AI_LOG_INFO("container '%s' exited with code %d, " From 91fb0aa9e75aa22a0584f1bfa9152a7452dd9ef9 Mon Sep 17 00:00:00 2001 From: ks734 Date: Mon, 27 Apr 2026 07:20:23 +0000 Subject: [PATCH 5/5] RDKEMW-12824: Extract signal status synthesis into testable helper and add L1 tests --- daemon/lib/source/DobbyManager.cpp | 76 ++++--- daemon/lib/source/include/DobbyManager.h | 6 + .../DaemonDobbyManagerTest.cpp | 195 ++++++++++++++++++ 3 files changed, 254 insertions(+), 23 deletions(-) diff --git a/daemon/lib/source/DobbyManager.cpp b/daemon/lib/source/DobbyManager.cpp index 624d2059..0114082d 100644 --- a/daemon/lib/source/DobbyManager.cpp +++ b/daemon/lib/source/DobbyManager.cpp @@ -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. @@ -3184,36 +3233,17 @@ void DobbyManager::onChildExit() // 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. - if (WIFEXITED(status)) { - int exitCode = WEXITSTATUS(status); - if (exitCode > 128 && exitCode < 128 + NSIG) + 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)); - - // Synthesise WIFSIGNALED status: signal number in bits 0-6. - // Also set bit 7 (WCOREDUMP) for signals that conventionally - // produce a core dump (SIGABRT, SIGSEGV, SIGFPE, SIGILL, - // SIGBUS, SIGQUIT) so that WCOREDUMP(status) returns true. - // Note: no actual core file is written since DobbyInit used - // _exit(); this is a best-effort convention match only. - auto signalDumpsCore = [](int s) -> bool { - switch (s) { - case SIGABRT: case SIGSEGV: case SIGFPE: - case SIGILL: case SIGBUS: case SIGQUIT: - return true; - default: - return false; - } - }; - - status = sig & 0x7f; - if (signalDumpsCore(sig)) - status |= 0x80; } } diff --git a/daemon/lib/source/include/DobbyManager.h b/daemon/lib/source/include/DobbyManager.h index 7e0dcd32..542ce3ac 100644 --- a/daemon/lib/source/include/DobbyManager.h +++ b/daemon/lib/source/include/DobbyManager.h @@ -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& container, const int status); void onChildExit(); diff --git a/tests/L1_testing/tests/DobbyManagerTest/DaemonDobbyManagerTest.cpp b/tests/L1_testing/tests/DobbyManagerTest/DaemonDobbyManagerTest.cpp index e1fe2b3d..32f6b8f1 100755 --- a/tests/L1_testing/tests/DobbyManagerTest/DaemonDobbyManagerTest.cpp +++ b/tests/L1_testing/tests/DobbyManagerTest/DaemonDobbyManagerTest.cpp @@ -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); +}