diff --git a/score/launch_manager/src/daemon/src/configuration/component_config.hpp b/score/launch_manager/src/daemon/src/configuration/component_config.hpp index 21322f819..9aea85b20 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -74,7 +74,7 @@ using ReadyCondition = std::variant; struct ComponentProperties { std::string binary_name; - ApplicationProfile application_profile; + ApplicationProfile application_profile{}; std::vector depends_on; std::vector process_arguments; @@ -104,15 +104,15 @@ struct DeploymentConfig std::optional ready_recovery_action; // Currently only SwitchRunTargetAction is supported here, RestartAction to be added in the future std::optional recovery_action; - Sandbox sandbox; + Sandbox sandbox{}; }; struct ComponentConfig { std::string name; std::string description; - ComponentProperties component_properties; - DeploymentConfig deployment_config; + ComponentProperties component_properties{}; + DeploymentConfig deployment_config{}; }; } // namespace score::mw::lifecycle::internal::configuration diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index b5a576bf7..e1a27e41d 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -25,7 +25,6 @@ cc_library( name = "semaphore", srcs = ["details/posix/semaphore.cpp"], hdrs = [ - "return_types.hpp", "semaphore.hpp", ], include_prefix = "score/mw/launch_manager/osal", @@ -38,7 +37,6 @@ cc_library( name = "wait_for_file", srcs = ["details/posix/wait_for_file.cpp"], hdrs = [ - "return_types.hpp", "wait_for_file.hpp", ], include_prefix = "score/mw/launch_manager/osal", @@ -73,7 +71,6 @@ cc_library( name = "ifile_waiter", hdrs = [ "ifile_waiter.hpp", - "return_types.hpp", ], include_prefix = "score/mw/launch_manager/osal", strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", @@ -165,8 +162,6 @@ cc_library( name = "ipc_comms", hdrs = [ "ipc_comms.hpp", - "return_types.hpp", - "semaphore.hpp", ], include_prefix = "score/mw/launch_manager/osal", strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", diff --git a/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp index e0bafa096..5d8fc1ad9 100644 --- a/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp +++ b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp @@ -20,7 +20,7 @@ #include "score/mw/launch_manager/configuration/component_config.hpp" #include -#include "return_types.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" namespace score::mw::lifecycle::internal::osal { diff --git a/score/launch_manager/src/daemon/src/osal/ipc_comms.hpp b/score/launch_manager/src/daemon/src/osal/ipc_comms.hpp index a3a054171..530f16900 100644 --- a/score/launch_manager/src/daemon/src/osal/ipc_comms.hpp +++ b/score/launch_manager/src/daemon/src/osal/ipc_comms.hpp @@ -18,7 +18,7 @@ #include #include "score/mw/launch_manager/common/log.hpp" -#include "semaphore.hpp" +#include "score/mw/launch_manager/osal/semaphore.hpp" namespace score::mw::lifecycle::internal::osal { @@ -99,6 +99,53 @@ struct IpcCommsSync final return ret; } + /// @brief Initializes semaphores within a given shared memory block. + /// @param[in] block Pointer to the shared memory block where semaphores will be initialized. + /// @return True if semaphore initialization is successful, false otherwise. + /// @details This is static instead of a member function because, even though not needed currently, any vtable + /// lookups would be UB. + static bool initializeSemaphores(IpcCommsP shared_block) + { + bool result = true; + + if (osal::OsalReturnType::kFail == shared_block->send_sync_.init(0U, true) || + osal::OsalReturnType::kFail == shared_block->reply_sync_.init(0U, true)) + { + result = false; + LM_LOG_ERROR() << "Semaphore init failed: Unable to initialize send_sync or reply_sync semaphore."; + } + + return result; + } + + /// @brief Deinitializes semaphores within a given shared memory block. + /// @param[in] block Pointer to the shared memory block. + /// @details This is static instead of a member function because, even though not needed currently, any vtable + /// lookups would be UB. + static void deinit(IpcCommsP shared_block) + { + // We are not interested in the result of msync, just whether it worked or not. + // If it did not work, the child process has probably crashed and corrupted the shared memory + // so we should not try to deinitialize the semaphores. + // mincore would be more appropriate here, but is not available on QNX + if (msync(shared_block.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) + { + if (shared_block->send_sync_.deinit() != OsalReturnType::kSuccess) + { + LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; + } + if (shared_block->reply_sync_.deinit() != OsalReturnType::kSuccess) + { + LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; + } + } + else + { + LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid:" + << errno_message(errno); + } + } + private: /// @brief Deleter to release IpcCommsSync object /// This is passed to the constructor of a shared pointer diff --git a/score/launch_manager/src/daemon/src/osal/semaphore.hpp b/score/launch_manager/src/daemon/src/osal/semaphore.hpp index 52203dde4..e9d7ea3b9 100644 --- a/score/launch_manager/src/daemon/src/osal/semaphore.hpp +++ b/score/launch_manager/src/daemon/src/osal/semaphore.hpp @@ -17,7 +17,7 @@ #include #include -#include "return_types.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" namespace score::mw::lifecycle::internal::osal { diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp index 497913b02..b39710bf3 100644 --- a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -23,7 +23,7 @@ #include #include -#include "return_types.hpp" +#include "score/mw/launch_manager/osal/return_types.hpp" namespace score::mw::lifecycle::internal::osal { diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD index 08b1f22db..914c0c70e 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD @@ -384,6 +384,54 @@ cc_library( ], ) +cc_library( + name = "mock_proc_launch_syscalls", + testonly = True, + hdrs = ["mock_proc_launch_syscalls.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager/details", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", + visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], + deps = [ + "@googletest//:gtest_main", + ], +) + +lm_cc_test( + name = "process_launcher_UT", + srcs = ["process_launcher_UT.cpp"], + linkopts = [ + "-Wl,--wrap=fork", + "-Wl,--wrap=execve", + "-Wl,--wrap=kill", + "-Wl,--wrap=wait", + "-Wl,--wrap=access", + "-Wl,--wrap=shm_open", + "-Wl,--wrap=ftruncate", + "-Wl,--wrap=shm_unlink", + "-Wl,--wrap=mmap", + "-Wl,--wrap=munmap", + "-Wl,--wrap=setpgid", + "-Wl,--wrap=setgid", + "-Wl,--wrap=setuid", + "-Wl,--wrap=sched_setscheduler", + "-Wl,--wrap=chdir", + "-Wl,--wrap=setrlimit", + "-Wl,--wrap=getpid", + "-Wl,--wrap=fcntl", + "-Wl,--wrap=sem_init", + "-Wl,--wrap=sem_destroy", + "-Wl,--wrap=sem_trywait", + "-Wl,--wrap=sem_post", + "-Wl,--wrap=msync", + ], + linkstatic = True, + deps = [ + ":mock_proc_launch_syscalls", + ":process_launcher", + "@googletest//:gtest_main", + ], +) + cc_library( name = "dependency_graph", hdrs = [ diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/mock_proc_launch_syscalls.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/mock_proc_launch_syscalls.hpp new file mode 100644 index 000000000..404c4b135 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/mock_proc_launch_syscalls.hpp @@ -0,0 +1,399 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef MOCK_PROC_LAUNCH_SYSCALLS +#define MOCK_PROC_LAUNCH_SYSCALLS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// NOLINTBEGIN - clang-tidy does not like syscalls + +class SyscallMock +{ + public: + MOCK_METHOD(pid_t, fork, (), ()); + MOCK_METHOD(int, execve, (const char*, char* const[], char* const[]), ()); + MOCK_METHOD(int, kill, (pid_t, int), ()); + MOCK_METHOD(pid_t, wait, (int*), ()); + MOCK_METHOD(int, shm_open, (const char*, int, mode_t), ()); + MOCK_METHOD(int, shm_unlink, (const char*), ()); + MOCK_METHOD(int, ftruncate, (int, off_t), ()); + MOCK_METHOD(int, access, (const char*, int), ()); + MOCK_METHOD(void*, mmap, (void* __addr, size_t __len, int __prot, int __flags, int __fd, off_t __offset), ()); + MOCK_METHOD(int, munmap, (void* __addr, size_t __len), ()); + MOCK_METHOD(void, sysexit, (int status), ()); + MOCK_METHOD(int, setpgid, (pid_t __pid, pid_t __pgid), ()); + MOCK_METHOD(int, setgid, (gid_t __gid), ()); + MOCK_METHOD(int, setuid, (uid_t __uid), ()); + MOCK_METHOD(int, sched_setscheduler, (pid_t __pid, int __policy, const struct sched_param*), ()); + MOCK_METHOD(int, chdir, (const char* __path), ()); + MOCK_METHOD(int, setrlimit, (int __resource, const struct rlimit* __rlimits), ()); + MOCK_METHOD(int, setSecurityPolicy, (const char* policy), ()); + MOCK_METHOD(pid_t, getpid, (), ()); + MOCK_METHOD(int, fcntl, (int __fd, int __cmd, void* arg), ()); + MOCK_METHOD(int, setgroups, (size_t n, const gid_t* groups), ()); + MOCK_METHOD(int, sem_init, (sem_t * __sem, int __pshared, unsigned int __value), ()); + MOCK_METHOD(int, sem_destroy, (sem_t * __sem), ()); + MOCK_METHOD(int, sem_trywait, (sem_t * __sem), ()); + MOCK_METHOD(int, sem_post, (sem_t * __sem), ()); + MOCK_METHOD(int, msync, (void* __addr, size_t __len, int __flags), ()); +}; + +std::unique_ptr g_syscall_mock = nullptr; + +extern "C" { +// wrap for fork +extern pid_t __real_fork(void); + +pid_t __wrap_fork(void) +{ + if (g_syscall_mock) + { + return g_syscall_mock->fork(); + } + + return __real_fork(); +} + +// wrap for execve +extern int __real_execve(const char*, char* const[], char* const[]); + +int __wrap_execve(const char* filename, char* const argv[], char* const envp[]) +{ + if (g_syscall_mock) + { + return g_syscall_mock->execve(filename, argv, envp); + } + + return __real_execve(filename, argv, envp); +} + +// wrap for kill +extern int __real_kill(pid_t, int); + +int __wrap_kill(pid_t pid, int sig) +{ + if (g_syscall_mock) + { + return g_syscall_mock->kill(pid, sig); + } + + return __real_kill(pid, sig); +} + +// wrap for wait +extern pid_t __real_wait(int*); + +pid_t __wrap_wait(int* status) +{ + if (g_syscall_mock) + { + return g_syscall_mock->wait(status); + } + + return __real_wait(status); +} + +// wrap for shm_open +extern int __real_shm_open(const char*, int, mode_t); + +int __wrap_shm_open(const char* name, int oflag, mode_t mode) +{ + if (g_syscall_mock) + { + return g_syscall_mock->shm_open(name, oflag, mode); + } + + return __real_shm_open(name, oflag, mode); +} + +// wrap for shm_unlink +extern int __real_shm_unlink(const char*); + +int __wrap_shm_unlink(const char* name) +{ + if (g_syscall_mock) + { + return g_syscall_mock->shm_unlink(name); + } + + return __real_shm_unlink(name); +} + +// wrap for ftruncate +extern int __real_ftruncate(int fildes, off_t length); + +int __wrap_ftruncate(int fildes, off_t length) +{ + if (g_syscall_mock) + { + return g_syscall_mock->ftruncate(fildes, length); + } + + return __real_ftruncate(fildes, length); +} + +// wrap for access +extern int __real_access(const char* name, int type); + +int __wrap_access(const char* name, int type) +{ + if (g_syscall_mock) + { + return g_syscall_mock->access(name, type); + } + + return __real_access(name, type); +} + +// wrap for mmap +extern void* __real_mmap(void* __addr, size_t __len, int __prot, int __flags, int __fd, off_t __offset); + +void* __wrap_mmap(void* __addr, size_t __len, int __prot, int __flags, int __fd, off_t __offset) +{ + if (g_syscall_mock) + { + return g_syscall_mock->mmap(__addr, __len, __prot, __flags, __fd, __offset); + } + + return __real_mmap(__addr, __len, __prot, __flags, __fd, __offset); +} + +// wrap for munmap +extern int __real_munmap(void* __addr, size_t __len); + +int __wrap_munmap(void* __addr, size_t __len) +{ + if (g_syscall_mock) + { + return g_syscall_mock->munmap(__addr, __len); + } + + return __real_munmap(__addr, __len); +} + +// wrap for setpgid +extern int __real_setpgid(pid_t __pid, pid_t __pgid); + +int __wrap_setpgid(pid_t __pid, pid_t __pgid) +{ + if (g_syscall_mock) + { + return g_syscall_mock->setpgid(__pid, __pgid); + } + + return __real_setpgid(__pid, __pgid); +} + +// wrap for setgid +extern int __real_setgid(gid_t __gid); + +int __wrap_setgid(gid_t __gid) +{ + if (g_syscall_mock) + { + return g_syscall_mock->setgid(__gid); + } + + return __real_setgid(__gid); +} + +// wrap for setuid +extern int __real_setuid(uid_t __uid); + +int __wrap_setuid(uid_t __uid) +{ + if (g_syscall_mock) + { + return g_syscall_mock->setuid(__uid); + } + + return __real_setuid(__uid); +} + +// wrap for sched_setscheduler +extern int __real_sched_setscheduler(pid_t __pid, int __policy, const struct sched_param* __param); + +int __wrap_sched_setscheduler(pid_t __pid, int __policy, const struct sched_param* __param) +{ + if (g_syscall_mock) + { + return g_syscall_mock->sched_setscheduler(__pid, __policy, __param); + } + + return __real_sched_setscheduler(__pid, __policy, __param); +} + +// wrap for chdir +extern int __real_chdir(const char* __path); + +int __wrap_chdir(const char* __path) +{ + if (g_syscall_mock) + { + return g_syscall_mock->chdir(__path); + } + + return __real_chdir(__path); +} + +// wrap for setrlimit +extern int __real_setrlimit(int __resource, const struct rlimit* __rlimits); + +int __wrap_setrlimit(int __resource, const struct rlimit* __rlimits) +{ + if (g_syscall_mock) + { + return g_syscall_mock->setrlimit(__resource, __rlimits); + } + + return __real_setrlimit(__resource, __rlimits); +} + +// wrap for getpid +extern int __real_getpid(); + +pid_t __wrap_getpid() +{ + if (g_syscall_mock) + { + return g_syscall_mock->getpid(); + } + + return __real_getpid(); +} + +// wrap for fcntl +extern int __real_fcntl(int __fd, int __cmd, ...); + +int __wrap_fcntl(int __fd, int __cmd, ...) +{ + void* arg = nullptr; + va_list args; + + va_start(args, __cmd); + arg = va_arg(args, void*); + va_end(args); + + if (g_syscall_mock) + { + return g_syscall_mock->fcntl(__fd, __cmd, arg); + } + + return __real_fcntl(__fd, __cmd, arg); +} + +// wrap for sem_init +extern int __real_sem_init(sem_t* __sem, int __pshared, unsigned int __value); + +int __wrap_sem_init(sem_t* __sem, int __pshared, unsigned int __value) +{ + if (g_syscall_mock) + { + return g_syscall_mock->sem_init(__sem, __pshared, __value); + } + + return __real_sem_init(__sem, __pshared, __value); +} + +// wrap for sem_destroy +extern int __real_sem_destroy(sem_t* __sem); + +int __wrap_sem_destroy(sem_t* __sem) +{ + if (g_syscall_mock) + { + return g_syscall_mock->sem_destroy(__sem); + } + + return __real_sem_destroy(__sem); +} + +// wrap for sem_trywait +extern int __real_sem_trywait(sem_t* __sem); + +int __wrap_sem_trywait(sem_t* __sem) +{ + if (g_syscall_mock) + { + return g_syscall_mock->sem_trywait(__sem); + } + + return __real_sem_trywait(__sem); +} + +// wrap for sem_post +extern int __real_sem_post(sem_t* __sem); + +int __wrap_sem_post(sem_t* __sem) +{ + if (g_syscall_mock) + { + return g_syscall_mock->sem_post(__sem); + } + + return __real_sem_post(__sem); +} + +// wrap for msync +extern int __real_msync(void* __addr, size_t __len, int __flags); + +int __wrap_msync(void* __addr, size_t __len, int __flags) +{ + if (g_syscall_mock) + { + return g_syscall_mock->msync(__addr, __len, __flags); + } + + return __real_msync(__addr, __len, __flags); +} +} + +namespace score::mw::lifecycle::internal::osal +{ +void sysexit(int status) +{ + g_syscall_mock->sysexit(status); +} + +int setSecurityPolicy(const char* policy) +{ + return g_syscall_mock->setSecurityPolicy(policy); +} + +int setgroups(size_t n, const gid_t* groups) +{ + return g_syscall_mock->setgroups(n, groups); +} + +} // namespace score::mw::lifecycle::internal::osal + +// NOLINTEND + +#endif diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 7b704bfa3..08afd9aa4 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -370,7 +370,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr { // currently we do not support multiple ready conditions so we need // to ignore the krunning signal. - auto wait_res = process_handling_.process_interface_->ignoreRunning(sync_); + auto wait_res = process_handling_.process_interface_->waitForkRunning(sync_, std::nullopt); static_cast(wait_res); } diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp index 371006b71..aa2b80584 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp @@ -797,7 +797,7 @@ TEST_F(ProcessInfoNodeFileStateTest, ConditionAlreadyMet_ReturnsSuccess) std::chrono::milliseconds{50}, std::chrono::milliseconds{5}); expectSuccessfulProcessLaunch(); - EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_processIf_, waitForkRunning(_, Eq(std::nullopt))).WillOnce(Return(osal::OsalReturnType::kSuccess)); EXPECT_CALL( mock_file_waiter_, waitForFile( @@ -824,7 +824,7 @@ TEST_F(ProcessInfoNodeFileStateTest, NotExistingCondition_ReturnsSuccess) auto node = createFileStateProcessInfoNode("/var/run/gone", configuration::FileExistenceState::NotExisting); expectSuccessfulProcessLaunch(); - EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_processIf_, waitForkRunning(_, Eq(std::nullopt))).WillOnce(Return(osal::OsalReturnType::kSuccess)); EXPECT_CALL(mock_file_waiter_, waitForFile(_, Eq(configuration::FileExistenceState::NotExisting), _, _, _)) .WillOnce(Return(osal::OsalReturnType::kSuccess)); @@ -837,7 +837,7 @@ TEST_F(ProcessInfoNodeFileStateTest, NotExistingCondition_ReturnsSuccess) TEST_F(ProcessInfoNodeFileStateTest, NativeApplication_DoesNotIgnoreRunning_ReturnsSuccess) { - RecordProperty("Description", "A FileState ready condition with a native process hall not call ignoreRunning."); + RecordProperty("Description", "A FileState ready condition with a native process hall not call waitForkRunning."); auto node = createFileStateProcessInfoNode( "/var/run/ready", configuration::FileExistenceState::Exists, configuration::ApplicationType::Native); @@ -859,7 +859,7 @@ TEST_F(ProcessInfoNodeFileStateTest, WaitForFileTimesOut_ReturnsActivationTimedO auto node = createFileStateProcessInfoNode("/var/run/ready", configuration::FileExistenceState::Exists); expectSuccessfulProcessLaunch(); - EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_processIf_, waitForkRunning(_, Eq(std::nullopt))).WillOnce(Return(osal::OsalReturnType::kSuccess)); EXPECT_CALL(mock_file_waiter_, waitForFile(_, _, _, _, _)).WillOnce(Return(osal::OsalReturnType::kTimeout)); // Simulate the OS handler reporting the killed process's exit once termination is requested. expectOsAcknowledgesTermination(node.get()); diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 34cdbae9c..5a36e0163 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -33,6 +33,7 @@ #include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include #include +#include #include #include #include @@ -67,7 +68,7 @@ void applyLimitOrDie(const int resource, const rlimit& limit, const std::string_ /// @brief Sets the limit if given a non-zero value, otherwise skips. /// @details The implementation should be async signal safe. /// @warning This will sysexit if the set is not succesful. -void setLimit(const int resource, const std::size_t amount, const std::string_view rlimit_name) noexcept +void setLimit(const int resource, const std::size_t amount, const std::string_view rlimit_name) noexcept(false) { if (amount == 0U) { @@ -82,8 +83,6 @@ void setLimit(const int resource, const std::size_t amount, const std::string_vi /// @details The implementation should be async signal safe. void handleComms(score::mw::lifecycle::internal::osal::ChildProcessConfig& param) { - // kNoComms !fd3 & !fd4 - // kReporting fd3 & !fd4 if (!param.shared_block) { // kNoComms, fds are CLOEXEC @@ -96,26 +95,18 @@ void handleComms(score::mw::lifecycle::internal::osal::ChildProcessConfig& param // It must be ensured that sync_fd (f3) remains open depending on // the communication type. Flag FD_CLOEXEC is cleared conditionally to ensure that the // respective file descriptor remains open after the execve call. - switch (param.shared_block->comms_type_) + assert( + param.shared_block->comms_type_ != CommsType::kNoComms && + "Invalid comms type, kNoComms is expected to be handled above"); + + assert( + param.shared_block->comms_type_ == CommsType::kReporting && + "Invalid comms type, a communicating process must be kReporting"); + + if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) { - case CommsType::kNoComms: - // in the current implementation this case means param.shared_block == nullptr and is handled above - break; - case CommsType::kReporting: - if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) - { - static_cast(signal_safe_log_errno(errno, "fcntl at line ", __LINE__, " failed")); - sysexit(EXIT_FAILURE); - } - break; - default: - static_cast(signal_safe_log( - "at line ", - __LINE__, - " unknown CommsType ", - static_cast(param.shared_block->comms_type_))); - sysexit(EXIT_FAILURE); - break; + static_cast(signal_safe_log_errno(errno, "fcntl at line ", __LINE__, " failed")); + sysexit(EXIT_FAILURE); } } @@ -172,10 +163,8 @@ void changeSecurityPolicy(const score::mw::lifecycle::internal::configuration::S namespace score::mw::lifecycle::internal::osal { -OsalReturnType ProcessLauncher::startProcess( - ProcessID& pid, - IpcCommsP& block, - const score::mw::lifecycle::internal::configuration::ComponentConfig& config) +OsalReturnType +ProcessLauncher::startProcess(ProcessID& pid, IpcCommsP& block, const configuration::ComponentConfig& config) { OsalReturnType result = OsalReturnType::kFail; @@ -193,8 +182,8 @@ OsalReturnType ProcessLauncher::startProcess( block = nullptr; bool comms_result = true; - auto app_type = config.component_properties.application_profile.application_type; - if (app_type != score::mw::lifecycle::internal::configuration::ApplicationType::Native) + const auto& app_type = config.component_properties.application_profile.application_type; + if (app_type != configuration::ApplicationType::Native) { comms_result = setupComms(block, fd, config); } @@ -250,8 +239,6 @@ OsalReturnType ProcessLauncher::startProcess( bool ProcessLauncher::setupComms(IpcCommsP& block, int& fd, const configuration::ComponentConfig& config) { - const auto app_type = config.component_properties.application_profile.application_type; - size_t length = sizeof(IpcCommsSync); constexpr std::string_view kShmNamePrefix{"/ipc_shared_mem"}; @@ -296,19 +283,13 @@ bool ProcessLauncher::setupComms(IpcCommsP& block, int& fd, const configuration: } // Map application type to CommsType for backward compatibility - switch (app_type) - { - case configuration::ApplicationType::Native: - block->comms_type_ = CommsType::kNoComms; - break; - case configuration::ApplicationType::Reporting: - case configuration::ApplicationType::ReportingAndSupervised: - default: - block->comms_type_ = CommsType::kReporting; - break; - } + SCORE_LANGUAGE_FUTURECPP_ASSERT_DBG_MESSAGE( + config.component_properties.application_profile.application_type != configuration::ApplicationType::Native, + "We should not set up the comms object if the application type is native. This used to be the kNoComms case"); + + block->comms_type_ = CommsType::kReporting; - if (!initializeSemaphores(block)) + if (!IpcCommsSync::initializeSemaphores(block)) { LM_LOG_ERROR() << "Semaphore init failed:" << config.name << "Unable to initialize send_sync or reply_sync semaphore."; @@ -318,20 +299,6 @@ bool ProcessLauncher::setupComms(IpcCommsP& block, int& fd, const configuration: return true; } -bool ProcessLauncher::initializeSemaphores(IpcCommsP shared_block) -{ - bool result = true; - - if (osal::OsalReturnType::kFail == shared_block->send_sync_.init(0U, true) || - osal::OsalReturnType::kFail == shared_block->reply_sync_.init(0U, true)) - { - result = false; - LM_LOG_ERROR() << "Semaphore init failed: Unable to initialize send_sync or reply_sync semaphore."; - } - - return result; -} - /// @details The implementation should be async signal safe. OsalReturnType ProcessLauncher::setSchedulingAndSecurity(const configuration::Sandbox& config) { @@ -526,67 +493,40 @@ OsalReturnType ProcessLauncher::waitForTermination(osal::ProcessID& pid, int32_t return result; } -OsalReturnType ProcessLauncher::ignoreRunning(IpcCommsP sync) +OsalReturnType ProcessLauncher::waitForkRunning(IpcCommsP sync, std::optional timeout) { + OsalReturnType result = OsalReturnType::kSuccess; + if (!sync) { LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; return OsalReturnType::kFail; } - const auto post_res = sync->reply_sync_.post(); - if (post_res == OsalReturnType::kFail) + if (timeout.has_value()) { - LM_LOG_ERROR() << "Semaphore post failed"; - return OsalReturnType::kFail; + result = sync->send_sync_.timedWait(timeout.value()); } - return OsalReturnType::kSuccess; -} -OsalReturnType ProcessLauncher::waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) -{ - OsalReturnType result = OsalReturnType::kSuccess; + const auto post_res = sync->reply_sync_.post(); - if (!sync) + if (post_res == OsalReturnType::kFail) { - LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; - return OsalReturnType::kFail; + LM_LOG_ERROR() << "Semaphore post failed"; + result = OsalReturnType::kFail; } - const auto time_res = sync->send_sync_.timedWait(timeout); - const auto post_res = sync->reply_sync_.post(); - - if ((time_res == OsalReturnType::kFail) || (post_res == OsalReturnType::kFail)) + if (result == OsalReturnType::kFail) { LM_LOG_ERROR() << "Semaphore timedWait or post failed: Unable to wait or post on semaphores within the " "specified timeout."; - result = OsalReturnType::kFail; } - else + else if (timeout.has_value()) { result = sync->send_sync_.timedWait(std::chrono::milliseconds(100)); } - // We are not interested in the result of msync, just whether it worked or not. - // If it did not work, the child process has probably crashed and corrupted the shared memory - // so we should not try to deinitialize the semaphores. - // mincore would be more appropriate here, but is not available on QNX - if (msync(sync.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) - { - if (sync->send_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; - } - if (sync->reply_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; - } - } - else - { - LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid:" - << errno_message(errno); - } + IpcCommsSync::deinit(sync); return result; } diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp index 2756ecf2e..26c3c83c8 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp @@ -40,10 +40,7 @@ class ProcessLauncher final : public IProcess OsalReturnType waitForTermination(ProcessID& pid, int32_t& status) override; /// @see IProcess::waitForkRunning() for details - OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) override; - - /// @see IProcess::waitForkRunning() for details - OsalReturnType ignoreRunning(IpcCommsP sync) override; + OsalReturnType waitForkRunning(IpcCommsP sync, std::optional timeout) override; private: /// @brief Creates shared memory for communication between processes. @@ -56,11 +53,6 @@ class ProcessLauncher final : public IProcess bool setupComms(IpcCommsP& sync, int& fd, const score::mw::lifecycle::internal::configuration::ComponentConfig& config); - /// @brief Initializes semaphores within a given shared memory block. - /// @param[in] block Pointer to the shared memory block where semaphores will be initialized. - /// @return True if semaphore initialization is successful, false otherwise. - bool initializeSemaphores(IpcCommsP block); - /// @brief Handles the execution of the child process after forking. /// @param[in] param Reference to child process configuration. void handleChildProcess(ChildProcessConfig& param); diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher_UT.cpp new file mode 100644 index 000000000..7cc568f2e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher_UT.cpp @@ -0,0 +1,765 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "score/mw/launch_manager/process_group_manager/details/mock_proc_launch_syscalls.hpp" +#include "score/mw/launch_manager/process_group_manager/details/process_launcher.hpp" + +using namespace testing; + +using namespace score::mw::lifecycle::internal::osal; +using namespace score::mw::lifecycle::internal; + +class ProcessLauncherTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + g_syscall_mock = std::make_unique(); + process_launcher = std::make_unique(); + + // Used by logging framework + ON_CALL(*g_syscall_mock, access).WillByDefault(Invoke(__real_access)); + ON_CALL(*g_syscall_mock, fcntl).WillByDefault(Invoke(__real_fcntl)); + EXPECT_CALL(*g_syscall_mock, access).Times(AnyNumber()); + EXPECT_CALL(*g_syscall_mock, fcntl).Times(AnyNumber()); + } + + void TearDown() override + { + process_launcher.reset(); + g_syscall_mock.reset(); + } + + std::shared_ptr GetInitialisedIpc() + { + std::memset(test_buffer.data(), 0, test_buffer.size()); + auto* sync = reinterpret_cast(test_buffer.data()); + + sync->comms_type_ = score::mw::lifecycle::internal::osal::CommsType::kNoComms; + sync->pid_ = 0; + EXPECT_EQ(sync->reply_sync_.init(0, false), OsalReturnType::kSuccess); + EXPECT_EQ(sync->send_sync_.init(0, false), OsalReturnType::kSuccess); + // Real msync calls would treat this fake shared memory as invalid because it isn't page aligned. + ON_CALL(*g_syscall_mock, msync(sync, _, _)).WillByDefault(Return(0)); + EXPECT_CALL(*g_syscall_mock, msync).Times(AnyNumber()); + + std::shared_ptr shared{sync, [](IpcCommsSync* ptr) { + }}; + + return shared; + } + + void ExpectSemaphoreLifecycle() + { + EXPECT_CALL(*g_syscall_mock, sem_init).Times(AtLeast(1)).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, sem_destroy).Times(AtLeast(1)).WillRepeatedly(Return(0)); + } + + std::unique_ptr process_launcher; + + alignas(IpcCommsSync) std::array test_buffer; +}; + +TEST_F(ProcessLauncherTest, waitForTerminationSuccess) +{ + RecordProperty("Description", "Test that waitForTermination calls `wait` and sets the correct values"); + + const pid_t pid = 7; + const uint32_t status = 9; + + EXPECT_CALL(*g_syscall_mock, wait).WillOnce(DoAll(SetArgPointee<0>(status), Return(pid))); + + ProcessID out_pid; + int32_t out_status; + const auto res = process_launcher->waitForTermination(out_pid, out_status); + + EXPECT_EQ(out_pid, pid); + EXPECT_EQ(out_status, status); + EXPECT_EQ(res, OsalReturnType::kSuccess); +} + +TEST_F(ProcessLauncherTest, waitForTerminationFails) +{ + RecordProperty("Description", "Test that waitForTermination reacts correctly to a failed `wait` syscall"); + + EXPECT_CALL(*g_syscall_mock, wait).WillOnce(SetErrnoAndReturn(WNOHANG, -1)); + + ProcessID out_pid; + int32_t out_status; + const auto res = process_launcher->waitForTermination(out_pid, out_status); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +class TerminationTest : public ProcessLauncherTest +{ +}; + +TEST_F(TerminationTest, requestTerminationSuccess) +{ + RecordProperty( + "Description", + "Test that requestTermination invokes `kill` with the provided pid and SIGTERM and returns the correct osal " + "result"); + const pid_t pid = 7; + + EXPECT_CALL(*g_syscall_mock, kill(pid, SIGTERM)).WillOnce(Return(0)); + + const OsalReturnType res = process_launcher->requestTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kSuccess); +} + +TEST_F(TerminationTest, requestTerminationFailure) +{ + RecordProperty( + "Description", "Test that requestTermination returns the correct osal result when the `kill` syscall fails"); + const pid_t pid = 7; + + EXPECT_CALL(*g_syscall_mock, kill).WillOnce(SetErrnoAndReturn(ESRCH, -1)); + + const OsalReturnType res = process_launcher->requestTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +TEST_F(TerminationTest, requestTerminationInvalid) +{ + RecordProperty("Description", "Test that requestTermination rejects invalid PIDs"); + + const pid_t pid = -1; + + EXPECT_CALL(*g_syscall_mock, kill).Times(0); + + const OsalReturnType res = process_launcher->requestTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +TEST_F(TerminationTest, forceTerminationSuccess) +{ + RecordProperty( + "Description", + "Test that forceTermination invokes `kill` with the provided pid and SIGKILL and returns the correct osal " + "result"); + const pid_t pid = 7; + + EXPECT_CALL(*g_syscall_mock, kill(pid, SIGKILL)).WillOnce(Return(0)); + + const OsalReturnType res = process_launcher->forceTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kSuccess); +} + +TEST_F(TerminationTest, forceTerminationSearchFailure) +{ + RecordProperty( + "Description", + "Test that forceTermination returns the correct osal result when the `kill` syscall fails due to a missing " + "process"); + const pid_t pid = 7; + + EXPECT_CALL(*g_syscall_mock, kill).WillOnce(SetErrnoAndReturn(ESRCH, -1)); + + const OsalReturnType res = process_launcher->forceTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +TEST_F(TerminationTest, forceTerminationPermFailure) +{ + RecordProperty( + "Description", + "Test that forceTermination returns the correct osal result when the `kill` syscall fails due to a permission " + "error"); + const pid_t pid = 7; + + EXPECT_CALL(*g_syscall_mock, kill).WillOnce(SetErrnoAndReturn(EPERM, -1)); + + const OsalReturnType res = process_launcher->forceTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +TEST_F(TerminationTest, forceTerminationInvalid) +{ + RecordProperty("Description", "Test that forceTermination rejects invalid PIDs"); + + const pid_t pid = -1; + + EXPECT_CALL(*g_syscall_mock, kill).Times(0); + + const OsalReturnType res = process_launcher->forceTermination(pid); + + EXPECT_EQ(res, OsalReturnType::kFail); +} + +// The sysexit mock can throw this to interrupt execution +struct SysExitException +{ +}; + +class StartProcessTest : public ProcessLauncherTest +{ + protected: + void SetUp() override + { + ProcessLauncherTest::SetUp(); + + EXPECT_CALL(*g_syscall_mock, getpid).Times(AnyNumber()).WillRepeatedly(Return(forked_pid)); + + config_.name = "TestComponent"; + config_.component_properties.binary_name = "TestProcess"; + config_.component_properties.application_profile.application_type = configuration::ApplicationType::Native; + config_.deployment_config.executable_path = "/bin/TestProcess"; + config_.deployment_config.working_dir = "/tmp"; + config_.deployment_config.sandbox.max_memory_usage = std::nullopt; + config_.deployment_config.sandbox.max_cpu_usage = std::nullopt; + config_.deployment_config.sandbox.max_memory_usage = std::nullopt; + config_.deployment_config.sandbox.security_policy = std::nullopt; + config_.deployment_config.sandbox.scheduling_priority = 1; + config_.deployment_config.sandbox.scheduling_policy = SCHED_RR; + config_.deployment_config.sandbox.gid = 1; + config_.deployment_config.sandbox.uid = 1; + config_.deployment_config.sandbox.supplementary_group_ids = {}; + } + + void TearDown() override + { + sync_.reset(); + + ProcessLauncherTest::TearDown(); + } + + void ExpectChildProcessStarts() + { + EXPECT_CALL(*g_syscall_mock, access).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, fork).WillOnce(Return(0)); + } + + void ExpectSuccessfulSchedulingAndSecurity() + { + EXPECT_CALL(*g_syscall_mock, setpgid).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).Times(AnyNumber()).WillRepeatedly(Return(0)); + } + + void ExpectSuccessfulChdir() + { + EXPECT_CALL(*g_syscall_mock, chdir).Times(AnyNumber()).WillRepeatedly(Return(0)); + } + + IpcCommsSync* StubShmObject() + { + const int fd = 123; + void* data = static_cast(test_buffer.data()); + EXPECT_CALL(*g_syscall_mock, shm_open).WillOnce(Return(fd)); + EXPECT_CALL(*g_syscall_mock, shm_unlink).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, mmap(_, _, _, _, fd, _)).WillOnce(Return(data)); + EXPECT_CALL(*g_syscall_mock, ftruncate(fd, _)).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, munmap(data, sizeof(IpcCommsSync))).WillOnce(Return(0)); + + return static_cast(data); + } + + void ExpectSetupComms() + { + StubShmObject(); + + EXPECT_CALL(*g_syscall_mock, sem_init).Times(AnyNumber()).WillRepeatedly(Return(0)); + } + + const pid_t forked_pid = 23; + + configuration::ComponentConfig config_ = {}; + ProcessID pid_; + IpcCommsP sync_; +}; + +TEST_F(StartProcessTest, startProcessNoFile) +{ + RecordProperty("Description", "Test that startProcess returns a failure if the provided path does not exist"); + + EXPECT_CALL(*g_syscall_mock, access(_, _)).WillOnce(Return(-1)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(StartProcessTest, startProcessNoPath) +{ + RecordProperty("Description", "Test that startProcess returns a failure if the provided path is empty"); + + config_.component_properties.binary_name = ""; + EXPECT_CALL(*g_syscall_mock, access).Times(0); // Access should not be called on an empty path + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(StartProcessTest, startProcessForkFailed) +{ + RecordProperty("Description", "Test that startProcess returns a failure if the provided path is empty"); + + EXPECT_CALL(*g_syscall_mock, access(_, _)).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, fork).WillOnce(Return(-1)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(StartProcessTest, handleCommsFailsFnctl) +{ + RecordProperty( + "Description", "Verify that the forked process exits if setting up comms fails due to a syscall failure"); + + config_.component_properties.application_profile.application_type = configuration::ApplicationType::Reporting; + ExpectChildProcessStarts(); + ExpectSetupComms(); + EXPECT_CALL(*g_syscall_mock, fcntl).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +class SetupCommsTest : public StartProcessTest +{ + void SetUp() override + { + StartProcessTest::SetUp(); + + config_.component_properties.application_profile.application_type = configuration::ApplicationType::Reporting; + + EXPECT_CALL(*g_syscall_mock, access).WillOnce(Return(0)); + } +}; + +TEST_F(SetupCommsTest, shmOpenFails) +{ + RecordProperty("Description", "Verify that if opening shared memory fails, startProcess fails"); + + EXPECT_CALL(*g_syscall_mock, shm_open).WillOnce(Return(-1)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(SetupCommsTest, ftruncateFails) +{ + RecordProperty("Description", "Verify that if truncating shared memory fails, startProcess fails"); + + EXPECT_CALL(*g_syscall_mock, shm_open).WillOnce(Return(123)); + EXPECT_CALL(*g_syscall_mock, shm_unlink).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, ftruncate).WillOnce(Return(-1)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(SetupCommsTest, getCommsFails) +{ + RecordProperty("Description", "Verify that if truncating shared memory fails, startProcess fails"); + + EXPECT_CALL(*g_syscall_mock, shm_open).WillOnce(Return(123)); + EXPECT_CALL(*g_syscall_mock, shm_unlink).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, ftruncate).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, mmap).WillOnce(Return(MAP_FAILED)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +TEST_F(SetupCommsTest, initSemaphoresFails) +{ + RecordProperty("Description", "Verify that if setting up the semaphores fails, startProcess fails"); + + StubShmObject(); + EXPECT_CALL(*g_syscall_mock, sem_init).WillRepeatedly(Return(-1)); + + EXPECT_EQ(process_launcher->startProcess(pid_, sync_, config_), OsalReturnType::kFail); +} + +class SetSchedulingAndSecurityTest : public StartProcessTest +{ + protected: + void SetUp() override + { + StartProcessTest::SetUp(); + + ExpectChildProcessStarts(); + ExpectSuccessfulChdir(); + EXPECT_CALL(*g_syscall_mock, execve).Times(AtMost(1)); + } +}; + +TEST_F(SetSchedulingAndSecurityTest, setpgidFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting pgid fails"); + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(-1)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(SetSchedulingAndSecurityTest, setgidFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting gid fails"); + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(-1)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(SetSchedulingAndSecurityTest, setuidFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting uid fails"); + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(SetSchedulingAndSecurityTest, setschedFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting the scheduler fails"); + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(-1)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(SetSchedulingAndSecurityTest, setschedClampedUpper) +{ + RecordProperty( + "Description", "Verify that if the configured priority is higher than the OS supports, it is clamped"); + + config_.deployment_config.sandbox.scheduling_policy = SCHED_FIFO; + const int too_high = 1000; + config_.deployment_config.sandbox.scheduling_priority = too_high; + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sched_setscheduler(_, _, Field(&sched_param::sched_priority, Lt(too_high)))) + .WillOnce(Return(0)); + + static_cast(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process +} + +TEST_F(SetSchedulingAndSecurityTest, setschedClampedLower) +{ + RecordProperty( + "Description", "Verify that if the configured priority is lower than the OS supports, it is clamped"); + + config_.deployment_config.sandbox.scheduling_policy = SCHED_FIFO; + const int too_low = -10; + config_.deployment_config.sandbox.scheduling_priority = too_low; + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sched_setscheduler(_, _, Field(&sched_param::sched_priority, Gt(too_low)))) + .WillOnce(Return(0)); + + static_cast(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process +} + +TEST_F(SetSchedulingAndSecurityTest, setgroupsFails) +{ + RecordProperty("Description", "Verify that if setting supplementary gids fails, the forked process exits"); + + config_.deployment_config.sandbox.supplementary_group_ids = {1, 2, 3}; + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgroups).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(StartProcessTest, chdirFails) +{ + RecordProperty("Description", "Verify that the forked process exits if changing the working dir fails"); + + ExpectChildProcessStarts(); + ExpectSuccessfulSchedulingAndSecurity(); + EXPECT_CALL(*g_syscall_mock, chdir).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(StartProcessTest, changeSecurityPolicyFails) +{ + RecordProperty("Description", "Verify that the forked process exits if changing the security policy fails"); + + config_.deployment_config.sandbox.security_policy = "security"; + + ExpectChildProcessStarts(); + ExpectSuccessfulSchedulingAndSecurity(); + ExpectSuccessfulChdir(); + EXPECT_CALL(*g_syscall_mock, setSecurityPolicy).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(StartProcessTest, setRLimitFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting a limit fails"); + + config_.deployment_config.sandbox.max_cpu_usage = 500; + + ExpectChildProcessStarts(); + ExpectSuccessfulSchedulingAndSecurity(); + ExpectSuccessfulChdir(); + EXPECT_CALL(*g_syscall_mock, setrlimit).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +TEST_F(StartProcessTest, setRLimitIgnore) +{ + RecordProperty("Description", "Verify that rlimits are not set if the configured value is 0"); + + config_.deployment_config.sandbox.max_cpu_usage = 0; + config_.deployment_config.sandbox.max_memory_usage = 0; + + ExpectChildProcessStarts(); + ExpectSuccessfulSchedulingAndSecurity(); + ExpectSuccessfulChdir(); + EXPECT_CALL(*g_syscall_mock, setrlimit).Times(0); + EXPECT_CALL(*g_syscall_mock, execve).Times(AtMost(1)); + + static_cast(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process +} + +TEST_F(StartProcessTest, execveFails) +{ + RecordProperty("Description", "Verify that the forked process exits if execve fails"); + + ExpectChildProcessStarts(); + ExpectSuccessfulSchedulingAndSecurity(); + ExpectSuccessfulChdir(); + EXPECT_CALL(*g_syscall_mock, execve).WillOnce(Return(-1)); + + EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{})); + + EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); +} + +// Define a matcher that checks a null-terminated char** against a vector of strings +MATCHER_P(ArgvMatches, expected_args, "") +{ + for (size_t i = 0; i < expected_args.size(); ++i) + { + // Check if the argument is null prematurely, or if the string doesn't match + if (arg[i] == nullptr || std::string(arg[i]) != expected_args[i]) + { + *result_listener << "mismatch at index " << i << " (expected: \"" << expected_args[i] << "\", got: \"" + << (arg[i] ? arg[i] : "NULL") << "\")"; + return false; + } + } + // execve argv must be null-terminated; ensure the last expected element is followed by NULL + return arg[expected_args.size()] == nullptr; +} + +MATCHER_P(CArrayMatches, expected, "") +{ + for (size_t i = 0; i < expected.size(); ++i) + { + if (arg[i] != expected[i]) + { + *result_listener << "mismatch at index " << i << " (expected: " << expected[i] << ", got: " << arg[i] + << ")"; + return false; + } + } + return true; +} + +TEST_F(StartProcessTest, startProcessChildSuccess) +{ + RecordProperty("Description", "Verify that when startProcess succeeds, the forked process is configured correctly"); + + const int scheduler = SCHED_FIFO; + const int uid = 11; + const int gid = 13; + const std::vector sgids = {1, 2, 3}; + const std::uint64_t mem_limit = 4096; + const std::uint32_t cpu_limit = 500; + const std::string security_policy = "security"; + const std::vector args_in = {"-c 2", "--argument yes"}; + const std::vector expected_launch_args = { + config_.deployment_config.executable_path, args_in[0], args_in[1]}; + + config_.component_properties.application_profile.application_type = configuration::ApplicationType::Reporting; + config_.deployment_config.sandbox.scheduling_policy = scheduler; + config_.deployment_config.sandbox.uid = uid; + config_.deployment_config.sandbox.gid = gid; + config_.deployment_config.sandbox.supplementary_group_ids = sgids; + config_.deployment_config.sandbox.max_memory_usage = mem_limit; + config_.deployment_config.sandbox.max_cpu_usage = cpu_limit; + config_.deployment_config.sandbox.security_policy = security_policy; + config_.deployment_config.environmental_variables.add("environment", "yes"); + config_.deployment_config.environmental_variables.add("errors", "no"); + config_.component_properties.process_arguments = args_in; + + IpcCommsSync* block = StubShmObject(); + + EXPECT_CALL(*g_syscall_mock, access).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, fork).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, getpid).WillRepeatedly(Return(forked_pid)); + EXPECT_CALL(*g_syscall_mock, fcntl).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sem_init).Times(2).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, setpgid(0, forked_pid)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler(0, scheduler, _)).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setuid(uid)).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgid(gid)).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setgroups(sgids.size(), CArrayMatches(sgids))).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, chdir(StrEq("/tmp"))).WillOnce(Return(0)); + EXPECT_CALL( + *g_syscall_mock, + setrlimit(RLIMIT_DATA, AllOf(Field(&rlimit::rlim_cur, mem_limit), Field(&rlimit::rlim_max, mem_limit)))); + EXPECT_CALL( + *g_syscall_mock, + setrlimit(RLIMIT_AS, AllOf(Field(&rlimit::rlim_cur, mem_limit), Field(&rlimit::rlim_max, mem_limit)))); + EXPECT_CALL( + *g_syscall_mock, + setrlimit(RLIMIT_CPU, AllOf(Field(&rlimit::rlim_cur, cpu_limit), Field(&rlimit::rlim_max, cpu_limit)))); + EXPECT_CALL(*g_syscall_mock, setSecurityPolicy(StrEq(security_policy.data()))).WillOnce(Return(0)); + EXPECT_CALL( + *g_syscall_mock, + execve( + StrEq(expected_launch_args[0]), + ArgvMatches(expected_launch_args), + config_.deployment_config.environmental_variables.envp())) + .WillOnce(Return(0)); + + EXPECT_CALL(*g_syscall_mock, sysexit).Times(0); // No failures + + static_cast(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process + + EXPECT_EQ(block->pid_, forked_pid); + EXPECT_EQ(block->comms_type_, CommsType::kReporting); +} + +class ClientMethodsTest : public ProcessLauncherTest +{ +}; + +using namespace std::chrono_literals; + +TEST_F(ProcessLauncherTest, ignoreRunningNoPost) +{ + RecordProperty("Description", "Verify that ignoreRunning correctly handles a failed semaphore post"); + + EXPECT_CALL(*g_syscall_mock, sem_init).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_syscall_mock, sem_destroy).WillRepeatedly(Return(0)); + std::shared_ptr sync = GetInitialisedIpc(); + EXPECT_CALL(*g_syscall_mock, sem_post).WillOnce(SetErrnoAndReturn(EINVAL, -1)); + + EXPECT_EQ(process_launcher->waitForkRunning(sync, std::nullopt), OsalReturnType::kFail); +} + +TEST_F(ProcessLauncherTest, ignoreRunningSuccess) +{ + RecordProperty( + "Description", + "Verify that waitForkRunning without a timeout posts on the reply semaphore without waiting and returns a " + "success"); + + ExpectSemaphoreLifecycle(); + std::shared_ptr sync = GetInitialisedIpc(); + EXPECT_CALL(*g_syscall_mock, sem_post).Times(1); + + EXPECT_EQ(process_launcher->waitForkRunning(sync, std::nullopt), OsalReturnType::kSuccess); +} + +TEST_F(ProcessLauncherTest, kRunningNoSync) +{ + RecordProperty("Description", "Verify that waitForkRunning correctly handles a null pointer"); + + std::shared_ptr sync; + + EXPECT_EQ(process_launcher->waitForkRunning(sync, 1ms), OsalReturnType::kFail); +} + +TEST_F(ProcessLauncherTest, kRunningSuccess) +{ + RecordProperty( + "Description", + "Verify that waitForkRunning waits for a notification, posts a reply, and then waits for another notification " + "before proceeding"); + + ExpectSemaphoreLifecycle(); + std::shared_ptr sync = GetInitialisedIpc(); + + Expectation waits = EXPECT_CALL(*g_syscall_mock, sem_trywait).WillOnce(Return(0)); + Expectation posts = EXPECT_CALL(*g_syscall_mock, sem_post).After(waits).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sem_trywait).After(posts).WillOnce(Return(0)); + + EXPECT_EQ(process_launcher->waitForkRunning(sync, 1ms), OsalReturnType::kSuccess); +} + +TEST_F(ProcessLauncherTest, kRunningTimeout) +{ + RecordProperty( + "Description", + "Verify that waitForkRunning returns a timeout failure if no notification is received within the timeout"); + + ExpectSemaphoreLifecycle(); + + std::shared_ptr sync = GetInitialisedIpc(); + EXPECT_CALL(*g_syscall_mock, sem_trywait).WillRepeatedly(SetErrnoAndReturn(EAGAIN, -1)); + EXPECT_CALL(*g_syscall_mock, sem_post).Times(1); // Posts even if the wait fails... + + EXPECT_EQ(process_launcher->waitForkRunning(sync, 1ms), OsalReturnType::kTimeout); +} diff --git a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp index 2a80ee4dc..a9ab5d1bd 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp @@ -110,16 +110,9 @@ class IProcess /// @brief This method wait for kRunning to be received from the process that was started /// @param sync The valid pointer returned from startProcess. Must not be NULL - /// @param timeout How long to wait for kRunning + /// @param timeout How long to wait for kRunning. If nullopt is provided, kRunning notifications are ignored /// @return kFail if sync is NULL or a timeout occurs, kSuccess otherwise - - virtual OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; - - /// @brief Ignores a kRunning signal. - /// @param sync The pointer returned from startProcess. - virtual OsalReturnType ignoreRunning(IpcCommsP sync) = 0; - - // virtual OsalReturnType respondToRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; + virtual OsalReturnType waitForkRunning(IpcCommsP sync, std::optional timeout) = 0; }; } // namespace score::mw::lifecycle::internal::osal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp index d8864ee82..c65926752 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp @@ -32,8 +32,11 @@ class MockIProcess : public IProcess MOCK_METHOD(OsalReturnType, requestTermination, (ProcessID pid), (override)); MOCK_METHOD(OsalReturnType, forceTermination, (ProcessID pid), (override)); MOCK_METHOD(OsalReturnType, waitForTermination, (ProcessID & pid, int32_t& status), (override)); - MOCK_METHOD(OsalReturnType, waitForkRunning, (IpcCommsP sync, std::chrono::milliseconds timeout), (override)); - MOCK_METHOD(OsalReturnType, ignoreRunning, (IpcCommsP sync), (override)); + MOCK_METHOD( + OsalReturnType, + waitForkRunning, + (IpcCommsP sync, std::optional timeout), + (override)); }; } // namespace score::mw::lifecycle::internal::osal