@@ -3581,6 +3581,222 @@ TEST(daemon_ipc_posix_lifetime_reservation_rejects_fork_inheritance) {
35813581 PASS ();
35823582}
35833583
3584+ static bool ipc_test_lifetime_lock_path (char out [TEST_PATH_CAP ], const char * runtime_dir ,
3585+ const char * key ) {
3586+ int written = runtime_dir && key
3587+ ? snprintf (out , TEST_PATH_CAP , "%s/cbm-%s.lifetime.lock" , runtime_dir , key )
3588+ : -1 ;
3589+ return written > 0 && written < TEST_PATH_CAP ;
3590+ }
3591+
3592+ /* Child-process verdict on whether the lifetime lock at lock_path is held by
3593+ * someone else, going straight through open()/fcntl() rather than any of
3594+ * this codebase's APIs, so a genuinely separate process cannot inherit the
3595+ * parent's in-process claim registry. Distinguishes three outcomes instead
3596+ * of collapsing "held" and "could not even check" into the same value:
3597+ * IPC_TEST_LOCK_HELD only when open() succeeded and F_SETLK failed with
3598+ * EACCES or EAGAIN (the lock is held by another process, as expected);
3599+ * IPC_TEST_LOCK_TAKEN when F_SETLK succeeded (the lock was free, a bug);
3600+ * IPC_TEST_LOCK_ERROR for anything else (open() failed, e.g. a wrong path,
3601+ * or F_SETLK failed with an errno that says nothing about the lock's
3602+ * state), which must never be mistaken for IPC_TEST_LOCK_HELD. */
3603+ enum {
3604+ IPC_TEST_LOCK_HELD = 0 ,
3605+ IPC_TEST_LOCK_TAKEN = 1 ,
3606+ IPC_TEST_LOCK_ERROR = 3 ,
3607+ };
3608+
3609+ static uint8_t ipc_test_lock_child_verdict (const char * lock_path ) {
3610+ int fd = open (lock_path , O_RDWR );
3611+ if (fd < 0 ) {
3612+ return IPC_TEST_LOCK_ERROR ;
3613+ }
3614+ struct flock record_lock = {
3615+ .l_type = F_WRLCK ,
3616+ .l_whence = SEEK_SET ,
3617+ .l_start = 0 ,
3618+ .l_len = 0 ,
3619+ };
3620+ int lock_result ;
3621+ do {
3622+ lock_result = fcntl (fd , F_SETLK , & record_lock );
3623+ } while (lock_result != 0 && errno == EINTR );
3624+ int lock_errno = errno ;
3625+ (void )close (fd );
3626+ if (lock_result == 0 ) {
3627+ return IPC_TEST_LOCK_TAKEN ;
3628+ }
3629+ return lock_errno == EACCES || lock_errno == EAGAIN ? IPC_TEST_LOCK_HELD : IPC_TEST_LOCK_ERROR ;
3630+ }
3631+
3632+ TEST (daemon_ipc_posix_lifetime_reservation_probe_does_not_drop_held_lock ) {
3633+ static const char key [] = "c1c1d2d2e3e3f4f4" ;
3634+ char parent [TEST_PATH_CAP ] = {0 };
3635+ char runtime_dir [TEST_PATH_CAP ] = {0 };
3636+ char lock_path [TEST_PATH_CAP ] = {0 };
3637+ cbm_daemon_ipc_endpoint_t * endpoint = NULL ;
3638+ cbm_daemon_ipc_lifetime_reservation_t * reservation = NULL ;
3639+ int result_pipe [2 ] = {-1 , -1 };
3640+ pid_t child = -1 ;
3641+ uint8_t child_result = 0 ;
3642+ int child_status = -1 ;
3643+ int acquired = -1 ;
3644+ int held_after_first_probe = -1 ;
3645+ int held_after_second_probe = -1 ;
3646+ int free_after_release = -1 ;
3647+ bool lock_path_ok = false;
3648+
3649+ if (ipc_test_parent_new (parent , "probe-holds-lock" )) {
3650+ endpoint = cbm_daemon_ipc_endpoint_new (key , parent );
3651+ }
3652+ if (endpoint ) {
3653+ ipc_test_copy_path (runtime_dir , cbm_daemon_ipc_endpoint_runtime_dir (endpoint ));
3654+ lock_path_ok = ipc_test_lifetime_lock_path (lock_path , runtime_dir , key );
3655+ acquired = cbm_daemon_ipc_lifetime_reservation_try_acquire (endpoint , & reservation );
3656+ }
3657+ if (reservation ) {
3658+ /* Two probes, matching how a real daemon polls its own reservation
3659+ * repeatedly while running. Each probe of an already-held lock used
3660+ * to open a throwaway fd on the lock file and close it. Since
3661+ * fcntl(2) record locks are scoped to (process, inode), that close
3662+ * silently released the real lock every time, regardless of the
3663+ * still-open fd the reservation itself is holding. */
3664+ held_after_first_probe = cbm_daemon_ipc_lifetime_reservation_probe (endpoint );
3665+ held_after_second_probe = cbm_daemon_ipc_lifetime_reservation_probe (endpoint );
3666+ }
3667+ if (reservation && lock_path_ok && pipe (result_pipe ) == 0 ) {
3668+ child = fork ();
3669+ }
3670+ if (child == 0 ) {
3671+ /* A genuinely independent process, not going through any of this
3672+ * codebase's APIs so it cannot inherit the parent's in-process
3673+ * claim registry: try to take the real OS-level lock directly. If
3674+ * the parent's fcntl lock survived the probes above, this fails. */
3675+ (void )close (result_pipe [0 ]);
3676+ child_result = ipc_test_lock_child_verdict (lock_path );
3677+ bool reported = ipc_test_fd_write_all (result_pipe [1 ], & child_result , sizeof (child_result ));
3678+ (void )close (result_pipe [1 ]);
3679+ _exit (reported ? 0 : 1 );
3680+ }
3681+ if (child > 0 ) {
3682+ (void )close (result_pipe [1 ]);
3683+ result_pipe [1 ] = -1 ;
3684+ bool received = ipc_test_fd_read_all (result_pipe [0 ], & child_result , sizeof (child_result ));
3685+ (void )close (result_pipe [0 ]);
3686+ result_pipe [0 ] = -1 ;
3687+ if (!received ) {
3688+ child_result = 2 ;
3689+ }
3690+ while (waitpid (child , & child_status , 0 ) < 0 && errno == EINTR ) {}
3691+ }
3692+ cbm_daemon_ipc_lifetime_reservation_release (reservation );
3693+ if (endpoint ) {
3694+ free_after_release = cbm_daemon_ipc_lifetime_reservation_probe (endpoint );
3695+ }
3696+ for (size_t index = 0 ; index < 2 ; index ++ ) {
3697+ if (result_pipe [index ] >= 0 ) {
3698+ (void )close (result_pipe [index ]);
3699+ }
3700+ }
3701+ cbm_daemon_ipc_endpoint_free (endpoint );
3702+ ipc_test_remove_tree (runtime_dir , parent );
3703+
3704+ ASSERT_EQ (acquired , 1 );
3705+ ASSERT_TRUE (lock_path_ok );
3706+ ASSERT_EQ (held_after_first_probe , 1 );
3707+ ASSERT_EQ (held_after_second_probe , 1 );
3708+ ASSERT_GT (child , 0 );
3709+ ASSERT_TRUE (WIFEXITED (child_status ));
3710+ ASSERT_EQ (WEXITSTATUS (child_status ), 0 );
3711+ ASSERT_EQ (child_result , IPC_TEST_LOCK_HELD );
3712+ ASSERT_EQ (free_after_release , 0 );
3713+ PASS ();
3714+ }
3715+
3716+ TEST (daemon_ipc_posix_lifetime_reservation_try_acquire_again_does_not_drop_held_lock ) {
3717+ static const char key [] = "a1a1b2b2c3c3d4d4" ;
3718+ char parent [TEST_PATH_CAP ] = {0 };
3719+ char runtime_dir [TEST_PATH_CAP ] = {0 };
3720+ char lock_path [TEST_PATH_CAP ] = {0 };
3721+ cbm_daemon_ipc_endpoint_t * endpoint = NULL ;
3722+ cbm_daemon_ipc_lifetime_reservation_t * reservation = NULL ;
3723+ cbm_daemon_ipc_lifetime_reservation_t * second_reservation =
3724+ (cbm_daemon_ipc_lifetime_reservation_t * )(uintptr_t )1 ;
3725+ int result_pipe [2 ] = {-1 , -1 };
3726+ pid_t child = -1 ;
3727+ uint8_t child_result = 0 ;
3728+ int child_status = -1 ;
3729+ int acquired = -1 ;
3730+ int reacquired = -1 ;
3731+ int free_after_release = -1 ;
3732+ bool lock_path_ok = false;
3733+
3734+ if (ipc_test_parent_new (parent , "try-acquire-again-holds-lock" )) {
3735+ endpoint = cbm_daemon_ipc_endpoint_new (key , parent );
3736+ }
3737+ if (endpoint ) {
3738+ ipc_test_copy_path (runtime_dir , cbm_daemon_ipc_endpoint_runtime_dir (endpoint ));
3739+ lock_path_ok = ipc_test_lifetime_lock_path (lock_path , runtime_dir , key );
3740+ acquired = cbm_daemon_ipc_lifetime_reservation_try_acquire (endpoint , & reservation );
3741+ }
3742+ if (reservation ) {
3743+ /* This process already holds the lifetime lock. Calling try_acquire
3744+ * again, still in this process, must trust the in-process registry
3745+ * alone and return 0 (already held, nothing new to hand back)
3746+ * without touching the lock file: opening and closing a second fd
3747+ * on it would drop the fcntl(2) record lock this process already
3748+ * holds, exactly the way the probe's own fast path used to. */
3749+ reacquired = cbm_daemon_ipc_lifetime_reservation_try_acquire (endpoint , & second_reservation );
3750+ }
3751+ if (reservation && lock_path_ok && pipe (result_pipe ) == 0 ) {
3752+ child = fork ();
3753+ }
3754+ if (child == 0 ) {
3755+ /* A genuinely independent process, not going through any of this
3756+ * codebase's APIs so it cannot inherit the parent's in-process
3757+ * claim registry: try to take the real OS-level lock directly. If
3758+ * the repeated try_acquire above survived, this fails. */
3759+ (void )close (result_pipe [0 ]);
3760+ child_result = ipc_test_lock_child_verdict (lock_path );
3761+ bool reported = ipc_test_fd_write_all (result_pipe [1 ], & child_result , sizeof (child_result ));
3762+ (void )close (result_pipe [1 ]);
3763+ _exit (reported ? 0 : 1 );
3764+ }
3765+ if (child > 0 ) {
3766+ (void )close (result_pipe [1 ]);
3767+ result_pipe [1 ] = -1 ;
3768+ bool received = ipc_test_fd_read_all (result_pipe [0 ], & child_result , sizeof (child_result ));
3769+ (void )close (result_pipe [0 ]);
3770+ result_pipe [0 ] = -1 ;
3771+ if (!received ) {
3772+ child_result = 2 ;
3773+ }
3774+ while (waitpid (child , & child_status , 0 ) < 0 && errno == EINTR ) {}
3775+ }
3776+ cbm_daemon_ipc_lifetime_reservation_release (reservation );
3777+ if (endpoint ) {
3778+ free_after_release = cbm_daemon_ipc_lifetime_reservation_probe (endpoint );
3779+ }
3780+ for (size_t index = 0 ; index < 2 ; index ++ ) {
3781+ if (result_pipe [index ] >= 0 ) {
3782+ (void )close (result_pipe [index ]);
3783+ }
3784+ }
3785+ cbm_daemon_ipc_endpoint_free (endpoint );
3786+ ipc_test_remove_tree (runtime_dir , parent );
3787+
3788+ ASSERT_EQ (acquired , 1 );
3789+ ASSERT_TRUE (lock_path_ok );
3790+ ASSERT_EQ (reacquired , 0 );
3791+ ASSERT_TRUE (second_reservation == NULL );
3792+ ASSERT_GT (child , 0 );
3793+ ASSERT_TRUE (WIFEXITED (child_status ));
3794+ ASSERT_EQ (WEXITSTATUS (child_status ), 0 );
3795+ ASSERT_EQ (child_result , IPC_TEST_LOCK_HELD );
3796+ ASSERT_EQ (free_after_release , 0 );
3797+ PASS ();
3798+ }
3799+
35843800TEST (daemon_ipc_posix_child_participant_handoff_retains_legacy_bridge ) {
35853801#ifdef _WIN32
35863802 PASS ();
@@ -5999,6 +6215,8 @@ SUITE(daemon_ipc) {
59996215#endif
60006216 RUN_TEST (daemon_ipc_posix_startup_lock_is_cross_process );
60016217 RUN_TEST (daemon_ipc_posix_lifetime_reservation_rejects_fork_inheritance );
6218+ RUN_TEST (daemon_ipc_posix_lifetime_reservation_probe_does_not_drop_held_lock );
6219+ RUN_TEST (daemon_ipc_posix_lifetime_reservation_try_acquire_again_does_not_drop_held_lock );
60026220 RUN_TEST (daemon_ipc_posix_child_participant_handoff_retains_legacy_bridge );
60036221 RUN_TEST (daemon_ipc_posix_publication_boundaries_recover_from_crash );
60046222 RUN_TEST (daemon_ipc_posix_record_publication_windows_recover_from_crash );
0 commit comments