已合并
reinit log file heads with child PID after fork() #1127
zhangjie创建于 3月23日
reinit log file heads with child PID after fork() #1127
已合并
zhangjie创建于 3月23日
5 个文件变更+424-0
Msrc/dfx/log/liblog/slog/plog/plog_file_mgr.c+62-0
@@ -867,6 +867,68 @@ LogStatus PlogFileMgrInit(void)
867 return LOG_SUCCESS;867 return LOG_SUCCESS;
868}868}
869 869 
870+/**
871+ * @brief : re-initialize log file heads with the child process's own PID
872+ * after fork(). Must be called in the child's atfork/fork handler
873+ * so that the child's log files are named with the child PID
874+ * rather than inheriting the parent's PID from g_plogFileList.
875+ *
876+ * Root cause of the bug this fixes:
877+ * PlogInitHostLogList() / PlogInitDeviceMaxFileNum() call ToolGetPid() at
878+ * *parent* init time and bake the result into list->pid and list->aucFileHead.
879+ * After fork() the child inherits g_plogFileList verbatim; without this
880+ * function, every filename the child generates still contains the parent PID
881+ * → multiple child processes all write into the same parent-PID-named file.
882+ */
883+void PlogReinitFileHeadsForChild(void)
884+{
885+ if (g_plogFileList == NULL) {
886+ return;
887+ }
888+ uint32_t childPid = (uint32_t)ToolGetPid();
889+ 
890+ /* Re-init host log file heads */
891+ for (int32_t i = (int32_t)DEBUG_LOG; i < (int32_t)LOG_TYPE_NUM; i++) {
892+ PlogFileList *list = &(g_plogFileList->hostLogList[i]);
893+ list->pid = childPid;
894+ list->fileNum = 0;
895+ /* Clear current file name so next write creates a fresh child-PID file */
896+ if ((list->aucFileName != NULL) && (list->currIndex < list->maxFileNum)) {
897+ (void)memset_s(list->aucFileName[list->currIndex],
898+ MAX_FILENAME_LEN + 1U, 0, MAX_FILENAME_LEN + 1U);
899+ }
900+ (void)PlogInitHostLogFileHead(list->aucFileHead, MAX_NAME_HEAD_LEN + 1U, childPid);
901+ }
902+ 
903+ /* Re-init device log file heads */
904+ for (uint32_t iType = 0; iType < (uint32_t)LOG_TYPE_NUM; iType++) {
905+ if (g_plogFileList->deviceLogList[iType] == NULL) {
906+ continue;
907+ }
908+ for (uint32_t idx = 0; idx < g_plogFileList->deviceNum; idx++) {
909+ PlogFileList *list = &(g_plogFileList->deviceLogList[iType][idx]);
910+ list->pid = childPid;
911+ list->fileNum = 0;
912+ if ((list->aucFileName != NULL) && (list->currIndex < list->maxFileNum)) {
913+ (void)memset_s(list->aucFileName[list->currIndex],
914+ MAX_FILENAME_LEN + 1U, 0, MAX_FILENAME_LEN + 1U);
915+ }
916+ (void)PlogInitDeviceLogFileHead(list->aucFileHead, MAX_NAME_HEAD_LEN + 1U, childPid);
917+ }
918+ }
919+ SELF_LOG_INFO("plog file heads reinitialized for child process, child_pid=%u.", childPid);
920+}
921+ 
922+/**
923+ * @brief : return the internal file manager list pointer for unit-test access.
924+ * Production code must NOT call this function.
925+ * @return : pointer to g_plogFileList
926+ */
927+PlogFileMgrInfo *PlogGetFileMgrInfo(void)
928+{
929+ return g_plogFileList;
930+}
931+ 
870void PlogFileMgrExit(void)932void PlogFileMgrExit(void)
871{933{
872 ONE_ACT_WARN_LOG(g_plogFileList == NULL, return, "file list is null.");934 ONE_ACT_WARN_LOG(g_plogFileList == NULL, return, "file list is null.");
Msrc/dfx/log/liblog/slog/plog/plog_file_mgr.h+2-0
@@ -48,6 +48,8 @@ typedef struct { // log file list paramter
48 48 
49LogStatus PlogFileMgrInit(void);49LogStatus PlogFileMgrInit(void);
50void PlogFileMgrExit(void);50void PlogFileMgrExit(void);
51+void PlogReinitFileHeadsForChild(void);
52+PlogFileMgrInfo *PlogGetFileMgrInfo(void);
51 53 
52LogStatus PlogWriteDeviceLog(char *msg, const PlogDeviceLogInfo *info);54LogStatus PlogWriteDeviceLog(char *msg, const PlogDeviceLogInfo *info);
53LogStatus PlogWriteHostLog(int32_t logType, char *msg, uint32_t len);55LogStatus PlogWriteHostLog(int32_t logType, char *msg, uint32_t len);
Msrc/dfx/log/liblog/slog/plog/plog_host_log.c+7-0
@@ -69,6 +69,9 @@ STATIC void PlogChildUnlock(void)
69{69{
70 g_alogFlushTid = 0;70 g_alogFlushTid = 0;
71 PlogUnlock();71 PlogUnlock();
72+ /* Fix: reinitialize file heads with the child's own PID so the child's
73+ * logs land in a file named after the child PID, not the parent PID. */
74+ PlogReinitFileHeadsForChild();
72}75}
73 76 
74STATIC void PlogAtForkCallback(int32_t type)77STATIC void PlogAtForkCallback(int32_t type)
@@ -256,6 +259,10 @@ STATIC void PlogForkCallback(void)
256 PlogUnlock();259 PlogUnlock();
257 PlogCleanUpBuff(BUFFER_TYPE_SEND);260 PlogCleanUpBuff(BUFFER_TYPE_SEND);
258 g_alogFlushTid = 0;261 g_alogFlushTid = 0;
262+ /* Fix: also reinitialize file heads here for the LOG_FORK path so that
263+ * any caller going through alog.so's fork callback also gets the correct
264+ * child PID in its log filenames. */
265+ PlogReinitFileHeadsForChild();
259}266}
260 267 
261static LogStatus PlogGetSyncEnv(int32_t *syncStatus)268static LogStatus PlogGetSyncEnv(int32_t *syncStatus)
Mtests/ut/slog/ut/ep/alog_host/CMakeLists.txt+1-0
@@ -45,6 +45,7 @@ set(libAlogSrc
45 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/alog_host_perf_utest.cc45 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/alog_host_perf_utest.cc
46 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/plog_buffer_utest.cc46 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/plog_buffer_utest.cc
47 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/plog_host_log_utest.cc47 ${CMAKE_CURRENT_SOURCE_DIR}/testcase/plog_host_log_utest.cc
48+ ${CMAKE_CURRENT_SOURCE_DIR}/testcase/plog_file_mgr_utest.cc
48 ${CMAKE_CURRENT_SOURCE_DIR}/stub/ascend_hal_stub.c49 ${CMAKE_CURRENT_SOURCE_DIR}/stub/ascend_hal_stub.c
49 ${CMAKE_CURRENT_SOURCE_DIR}/stub/plog_stub.cc50 ${CMAKE_CURRENT_SOURCE_DIR}/stub/plog_stub.cc
50 ${UT_SOURCE_PATH}/ep/stub/system_api_stub.c51 ${UT_SOURCE_PATH}/ep/stub/system_api_stub.c
Atests/ut/slog/ut/ep/alog_host/testcase/plog_file_mgr_utest.cc+352-0
@@ -0,0 +1,352 @@
1+/**
2+ * Copyright (c) 2025 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+/**
12+ * @file plog_file_mgr_utest.cc
13+ * @brief Unit tests for plog_file_mgr.c, with focus on the fork() PID naming
14+ * bug fix introduced in PlogReinitFileHeadsForChild().
15+ *
16+ * Bug summary:
17+ * After fork(), the child process inherits g_plogFileList verbatim from the
18+ * parent. Because PlogInitHostLogList() and PlogInitDeviceMaxFileNum() bake
19+ * the parent's PID into list->pid and list->aucFileHead at init time, the
20+ * child wrote logs into files named after the parent PID, causing all child
21+ * processes to collide on the same filename in the run/plog/ directory.
22+ *
23+ * Fix:
24+ * PlogReinitFileHeadsForChild() is called from PlogChildUnlock() (atfork
25+ * ATFORK_CHILD path) and PlogForkCallback() (alog.so LOG_FORK path). It
26+ * re-runs ToolGetPid() in the child context and updates every list->pid and
27+ * list->aucFileHead in both hostLogList and deviceLogList.
28+ */
29+ 
30+#include "gtest/gtest.h"
31+#include "mockcpp/mockcpp.hpp"
32+ 
33+#include "self_log_stub.h"
34+#include "plog_file_mgr.h"
35+#include "log_system_api.h"
36+#include "securec.h"
37+ 
38+#include <sys/wait.h>
39+#include <unistd.h>
40+#include <string>
41+#include <cstring>
42+ 
43+/* ── constants mirrored from plog_file_mgr.c ─────────────────────────────── */
44+static const char HOST_FILE_HEAD_PREFIX[] = "plog-"; /* PROC_HEAD + "-" */
45+static const char DEV_FILE_HEAD_PREFIX[] = "device-"; /* DEVICE_HEAD */
46+ 
47+/* ── helper: check that aucFileHead encodes the expected pid ──────────────── */
48+static ::testing::AssertionResult FileHeadContainsPid(const char *fileHead, uint32_t pid)
49+{
50+ char expected[32] = {};
51+ (void)snprintf_s(expected, sizeof(expected), sizeof(expected) - 1, "%u", pid);
52+ if (strstr(fileHead, expected) != nullptr) {
53+ return ::testing::AssertionSuccess();
54+ }
55+ return ::testing::AssertionFailure()
56+ << "aucFileHead \"" << fileHead << "\" does not contain pid " << pid;
57+}
58+ 
59+/* ── test fixture ─────────────────────────────────────────────────────────── */
60+class PlogFileMgrUtest : public testing::Test {
61+protected:
62+ static void SetUpTestCase()
63+ {
64+ /* Point the log root to PATH_ROOT so PlogFileMgrInit() succeeds. */
65+ setenv("ASCEND_PROCESS_LOG_PATH", PATH_ROOT, 1);
66+ system("rm -rf " PATH_ROOT);
67+ system("mkdir -p " PATH_ROOT);
68+ system("echo [DBG][TEST] Start PlogFileMgr test suite");
69+ }
70+ 
71+ static void TearDownTestCase()
72+ {
73+ system("rm -rf " PATH_ROOT);
74+ unsetenv("ASCEND_PROCESS_LOG_PATH");
75+ system("echo [DBG][TEST] End PlogFileMgr test suite");
76+ }
77+ 
78+ virtual void SetUp()
79+ {
80+ system("rm -rf " PATH_ROOT "/*");
81+ ResetErrLog();
82+ }
83+ 
84+ virtual void TearDown()
85+ {
86+ /* Always exit file manager so g_plogFileList is freed between tests. */
87+ PlogFileMgrExit();
88+ GlobalMockObject::verify();
89+ }
90+};
91+ 
92+/* ══════════════════════════════════════════════════════════════════════════ *
93+ * TC-01: PlogReinitFileHeadsForChild() is safe when called before init, *
94+ * i.e., when g_plogFileList is NULL. Must not crash. *
95+ * ══════════════════════════════════════════════════════════════════════════ */
96+TEST_F(PlogFileMgrUtest, ReinitForChild_NullListSafe)
97+{
98+ /* g_plogFileList is NULL before PlogFileMgrInit() is called. */
99+ ASSERT_EQ(nullptr, PlogGetFileMgrInfo());
100+ /* Should return without crashing. */
101+ PlogReinitFileHeadsForChild();
102+ EXPECT_EQ(nullptr, PlogGetFileMgrInfo());
103+}
104+ 
105+/* ══════════════════════════════════════════════════════════════════════════ *
106+ * TC-02: After reinit, all hostLogList entries carry the child's PID in *
107+ * the pid field and in aucFileHead. *
108+ * ══════════════════════════════════════════════════════════════════════════ */
109+TEST_F(PlogFileMgrUtest, ReinitForChild_UpdatesAllHostLogPidAndFileHead)
110+{
111+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
112+ PlogFileMgrInfo *fileList = PlogGetFileMgrInfo();
113+ ASSERT_NE(nullptr, fileList);
114+ 
115+ const uint32_t parentPid = (uint32_t)getpid();
116+ /* Verify that after init the host log file heads contain the parent PID. */
117+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
118+ EXPECT_EQ(parentPid, fileList->hostLogList[i].pid)
119+ << "hostLogList[" << i << "].pid should be parent pid after init";
120+ EXPECT_TRUE(FileHeadContainsPid(fileList->hostLogList[i].aucFileHead, parentPid))
121+ << "hostLogList[" << i << "].aucFileHead should contain parent pid after init";
122+ }
123+ 
124+ /* Simulate the child's ToolGetPid() returning a different PID. */
125+ const uint32_t fakePid = parentPid + 12345U;
126+ MOCKER(ToolGetPid).stubs().will(returnValue((INT32)fakePid));
127+ 
128+ PlogReinitFileHeadsForChild();
129+ fileList = PlogGetFileMgrInfo();
130+ 
131+ /* All host log entries must now carry the child PID. */
132+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
133+ EXPECT_EQ(fakePid, fileList->hostLogList[i].pid)
134+ << "hostLogList[" << i << "].pid not updated after reinit";
135+ 
136+ const char *head = fileList->hostLogList[i].aucFileHead;
137+ /* Must start with "plog-" and contain the new PID. */
138+ EXPECT_EQ(0, strncmp(head, HOST_FILE_HEAD_PREFIX, strlen(HOST_FILE_HEAD_PREFIX)))
139+ << "hostLogList[" << i << "].aucFileHead prefix wrong: " << head;
140+ EXPECT_TRUE(FileHeadContainsPid(head, fakePid))
141+ << "hostLogList[" << i << "].aucFileHead should contain child pid";
142+ /* Must NOT contain the old parent PID. */
143+ EXPECT_FALSE(FileHeadContainsPid(head, parentPid))
144+ << "hostLogList[" << i << "].aucFileHead still contains parent pid";
145+ }
146+ 
147+ GlobalMockObject::verify();
148+}
149+ 
150+/* ══════════════════════════════════════════════════════════════════════════ *
151+ * TC-03: After reinit, all deviceLogList entries carry the child's PID in *
152+ * the pid field and in aucFileHead. *
153+ * ══════════════════════════════════════════════════════════════════════════ */
154+TEST_F(PlogFileMgrUtest, ReinitForChild_UpdatesAllDeviceLogPidAndFileHead)
155+{
156+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
157+ PlogFileMgrInfo *fileList = PlogGetFileMgrInfo();
158+ ASSERT_NE(nullptr, fileList);
159+ 
160+ const uint32_t parentPid = (uint32_t)getpid();
161+ const uint32_t fakePid = parentPid + 9999U;
162+ 
163+ MOCKER(ToolGetPid).stubs().will(returnValue((INT32)fakePid));
164+ 
165+ PlogReinitFileHeadsForChild();
166+ fileList = PlogGetFileMgrInfo();
167+ 
168+ for (uint32_t type = 0; type < (uint32_t)LOG_TYPE_NUM; type++) {
169+ ASSERT_NE(nullptr, fileList->deviceLogList[type]);
170+ for (uint32_t idx = 0; idx < fileList->deviceNum; idx++) {
171+ PlogFileList *list = &fileList->deviceLogList[type][idx];
172+ EXPECT_EQ(fakePid, list->pid)
173+ << "deviceLogList[" << type << "][" << idx << "].pid not updated";
174+ 
175+ const char *head = list->aucFileHead;
176+ EXPECT_EQ(0, strncmp(head, DEV_FILE_HEAD_PREFIX, strlen(DEV_FILE_HEAD_PREFIX)))
177+ << "deviceLogList[" << type << "][" << idx << "].aucFileHead prefix wrong";
178+ EXPECT_TRUE(FileHeadContainsPid(head, fakePid))
179+ << "deviceLogList[" << type << "][" << idx << "] should contain child pid";
180+ EXPECT_FALSE(FileHeadContainsPid(head, parentPid))
181+ << "deviceLogList[" << type << "][" << idx << "] still contains parent pid";
182+ }
183+ }
184+ 
185+ GlobalMockObject::verify();
186+}
187+ 
188+/* ══════════════════════════════════════════════════════════════════════════ *
189+ * TC-04: After reinit, the current filename slot is cleared so the child's *
190+ * next write opens a fresh file rather than sharing the parent's. *
191+ * ══════════════════════════════════════════════════════════════════════════ */
192+TEST_F(PlogFileMgrUtest, ReinitForChild_ClearsCurrentFileNameSlot)
193+{
194+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
195+ PlogFileMgrInfo *fileList = PlogGetFileMgrInfo();
196+ ASSERT_NE(nullptr, fileList);
197+ 
198+ /* Artificially set a non-empty filename so we can verify it gets cleared. */
199+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
200+ PlogFileList *list = &fileList->hostLogList[i];
201+ if ((list->aucFileName != nullptr) && (list->currIndex < list->maxFileNum)) {
202+ (void)strcpy_s(list->aucFileName[list->currIndex], MAX_FILENAME_LEN + 1U,
203+ "stale_parent_pid_name.log");
204+ }
205+ }
206+ 
207+ const uint32_t fakePid = (uint32_t)getpid() + 7777U;
208+ MOCKER(ToolGetPid).stubs().will(returnValue((INT32)fakePid));
209+ 
210+ PlogReinitFileHeadsForChild();
211+ fileList = PlogGetFileMgrInfo();
212+ 
213+ /* The current slot must be zeroed out so the child starts a new file. */
214+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
215+ PlogFileList *list = &fileList->hostLogList[i];
216+ if ((list->aucFileName != nullptr) && (list->currIndex < list->maxFileNum)) {
217+ EXPECT_EQ('\0', list->aucFileName[list->currIndex][0])
218+ << "hostLogList[" << i << "] current filename slot not cleared after reinit";
219+ }
220+ }
221+ 
222+ GlobalMockObject::verify();
223+}
224+ 
225+/* ══════════════════════════════════════════════════════════════════════════ *
226+ * TC-05: Multiple calls to PlogReinitFileHeadsForChild() are idempotent: *
227+ * the second call with the same (mocked) pid yields the same result. *
228+ * ══════════════════════════════════════════════════════════════════════════ */
229+TEST_F(PlogFileMgrUtest, ReinitForChild_IdempotentMultipleCalls)
230+{
231+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
232+ 
233+ const uint32_t fakePid = (uint32_t)getpid() + 5555U;
234+ MOCKER(ToolGetPid).stubs().will(returnValue((INT32)fakePid));
235+ 
236+ PlogReinitFileHeadsForChild();
237+ PlogReinitFileHeadsForChild(); /* second call must not corrupt state */
238+ 
239+ PlogFileMgrInfo *fileList = PlogGetFileMgrInfo();
240+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
241+ EXPECT_EQ(fakePid, fileList->hostLogList[i].pid);
242+ EXPECT_TRUE(FileHeadContainsPid(fileList->hostLogList[i].aucFileHead, fakePid));
243+ }
244+ 
245+ GlobalMockObject::verify();
246+}
247+ 
248+/* ══════════════════════════════════════════════════════════════════════════ *
249+ * TC-06: fork()-based integration test. *
250+ * Verifies that after a real fork() + PlogReinitFileHeadsForChild(), *
251+ * the child's g_plogFileList carries the child's own PID — not the *
252+ * parent's — in every host log list entry. *
253+ * *
254+ * exit(0) → g_plogFileList correctly updated with child PID (fix works) *
255+ * exit(1) → still contains parent PID (bug present) *
256+ * ══════════════════════════════════════════════════════════════════════════ */
257+TEST_F(PlogFileMgrUtest, ForkChild_ReinitGivesChildOwnPidInFileHead)
258+{
259+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
260+ ASSERT_NE(nullptr, PlogGetFileMgrInfo());
261+ 
262+ const pid_t parentPid = getpid();
263+ 
264+ pid_t child = fork();
265+ ASSERT_GE(child, 0) << "fork() failed: " << strerror(errno);
266+ 
267+ if (child == 0) {
268+ /* ── child process ─────────────────────────────────────────────── */
269+ const pid_t myPid = getpid();
270+ 
271+ /* Simulate what the real ATFORK_CHILD handler does. */
272+ PlogReinitFileHeadsForChild();
273+ 
274+ PlogFileMgrInfo *childList = PlogGetFileMgrInfo();
275+ bool ok = true;
276+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
277+ uint32_t storedPid = childList->hostLogList[i].pid;
278+ if (storedPid != (uint32_t)myPid) {
279+ ok = false;
280+ break;
281+ }
282+ const char *head = childList->hostLogList[i].aucFileHead;
283+ char expected[32] = {};
284+ (void)snprintf_s(expected, sizeof(expected), sizeof(expected) - 1, "%u", (uint32_t)myPid);
285+ if (strstr(head, expected) == nullptr) {
286+ ok = false;
287+ break;
288+ }
289+ }
290+ /* exit(0) = fix works, exit(1) = bug still present */
291+ _exit(ok ? 0 : 1);
292+ }
293+ 
294+ /* ── parent: wait for child and check exit code ────────────────────── */
295+ int wstatus = 0;
296+ waitpid(child, &wstatus, 0);
297+ ASSERT_TRUE(WIFEXITED(wstatus)) << "child did not exit normally";
298+ EXPECT_EQ(0, WEXITSTATUS(wstatus))
299+ << "Child process: PlogReinitFileHeadsForChild() did not update "
300+ "hostLogList with child PID — fork PID naming bug is still present";
301+ 
302+ /* Sanity-check: parent's list must still hold the parent's PID. */
303+ PlogFileMgrInfo *parentList = PlogGetFileMgrInfo();
304+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
305+ EXPECT_EQ((uint32_t)parentPid, parentList->hostLogList[i].pid)
306+ << "Parent's hostLogList[" << i << "].pid was modified by child "
307+ "(fork isolation violated?)";
308+ }
309+}
310+ 
311+/* ══════════════════════════════════════════════════════════════════════════ *
312+ * TC-07: Regression — without reinit, child would inherit the parent PID. *
313+ * This test documents the bug by showing what happens when reinit is *
314+ * NOT called, confirming the test environment is sound. *
315+ * ══════════════════════════════════════════════════════════════════════════ */
316+TEST_F(PlogFileMgrUtest, ForkChild_WithoutReinitInheritsParentPid_Regression)
317+{
318+ ASSERT_EQ(LOG_SUCCESS, PlogFileMgrInit());
319+ ASSERT_NE(nullptr, PlogGetFileMgrInfo());
320+ 
321+ const pid_t parentPid = getpid();
322+ 
323+ pid_t child = fork();
324+ ASSERT_GE(child, 0) << "fork() failed: " << strerror(errno);
325+ 
326+ if (child == 0) {
327+ /* ── child process: intentionally does NOT call reinit ─────────── */
328+ const pid_t myPid = getpid();
329+ PlogFileMgrInfo *childList = PlogGetFileMgrInfo();
330+ bool inherited = false;
331+ for (int i = (int)DEBUG_LOG; i < (int)LOG_TYPE_NUM; i++) {
332+ if (childList->hostLogList[i].pid == (uint32_t)parentPid &&
333+ (uint32_t)myPid != (uint32_t)parentPid) {
334+ inherited = true;
335+ break;
336+ }
337+ }
338+ /* exit(0) = bug present (expected for regression doc),
339+ * exit(1) = somehow not inherited (unexpected) */
340+ _exit(inherited ? 0 : 1);
341+ }
342+ 
343+ int wstatus = 0;
344+ waitpid(child, &wstatus, 0);
345+ ASSERT_TRUE(WIFEXITED(wstatus));
346+ /* The child exits 0 to signal the bug is present (no reinit → parent PID
347+ * inherited). If this expectation ever fails it means the OS or compiler
348+ * has somehow prevented the inheritance, which would be surprising. */
349+ EXPECT_EQ(0, WEXITSTATUS(wstatus))
350+ << "Regression: child should have inherited parent PID in g_plogFileList "
351+ "when PlogReinitFileHeadsForChild() is NOT called";
352+}