From d97ec596c6e0ef29376f3ab2c08c6e001cdedf06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Wed, 30 Sep 2026 11:24:53 +0200 Subject: [PATCH 1/3] Add support for runmask --- .../product_documentation/configuration.rst | 4 ++ .../src/configuration/component_config.hpp | 1 + .../config_schema/launch_manager.schema.json | 5 ++ .../details/flatbuffer_config_loader_UT.cpp | 38 +++++++++- .../details/flatbuffer_type_converters.cpp | 1 + .../details/flatbuffer_type_converters_UT.cpp | 18 ++++- .../src/configuration/details/lm_flatcfg.fbs | 2 + .../details/process_launcher.cpp | 15 ++-- scripts/config_mapping/lifecycle_config.py | 26 +++++++ .../expected_output/lm_config_gen.json | 12 ++-- .../full_config_test/input/lm_config.json | 3 +- scripts/config_mapping/unit_tests.py | 70 ++++++++++++++++++- .../sandbox_options/sandbox_options.json | 12 +++- .../sandbox_options/sandbox_options.py | 3 +- .../sandbox_options/verify_sandbox.hpp | 9 +-- 15 files changed, 193 insertions(+), 26 deletions(-) diff --git a/score/launch_manager/docs/product_documentation/configuration.rst b/score/launch_manager/docs/product_documentation/configuration.rst index 7ba96cd06f..52e04a77de 100644 --- a/score/launch_manager/docs/product_documentation/configuration.rst +++ b/score/launch_manager/docs/product_documentation/configuration.rst @@ -322,6 +322,10 @@ sandbox.max_cpu_usage (integer, optional) Description: Specifies the maximum CPU usage limit for the component, expressed as a percentage (%) of total CPU capacity. Constraint: Must be greater than 0. +sandbox.affinity_mask (string, optional) + Description: Specifies the affinity mask defining on which CPU cores the component may be scheduled, as a hexadecimal string prefixed with ``0x`` (e.g. ``"0x3"``). Bit 0 corresponds to core 0, bit 1 to core 1, and so on; up to 64 cores are supported. For example, ``"0x5"`` selects cores 0 and 2. If not configured, the component inherits the CPU affinity of the **Launch Manager** itself, i.e. no affinity system call is made on its behalf. + Constraint: Must be a hexadecimal string with the ``0x``/``0X`` prefix, at most 16 hex digits (64 bits), and at least one bit set. + .. _lm_conf_launch_manager_root_properties: Launch manager root properties 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 9aea85b208..3849c2fa2e 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -91,6 +91,7 @@ struct Sandbox std::int32_t scheduling_priority{}; std::optional max_memory_usage; std::optional max_cpu_usage; + std::optional affinity_mask; }; struct DeploymentConfig diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 2fcac9a4ef..81a5ee2bec 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -319,6 +319,11 @@ "type": "integer", "exclusiveMinimum": 0, "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage (%) of total CPU capacity." + }, + "affinity_mask": { + "type": "string", + "pattern": "^0[xX](?!0{1,16}$)[0-9a-fA-F]{1,16}$", + "description": "Specifies the affinity mask defining on which CPU cores the process may be scheduled, as a hexadecimal string (e.g. \"0x3\" for cores 0 and 1). Bit 0 corresponds to core 0; up to 64 cores are supported. At least one bit must be set. If not configured, the component inherits the CPU affinity of the **Launch Manager** itself, i.e. no affinity system call is made on its behalf." } }, "additionalProperties": false diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 9903dc17f5..7458898231 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -519,7 +519,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSandbox) fb::SchedulingPolicy::FIFO, 50 /*scheduling_priority*/, 4096 /*max_memory_usage*/, - 80 /*max_cpu_usage*/); + 80 /*max_cpu_usage*/, + 3 /*affinity_mask*/); auto bin_dir = fbb.CreateString("/opt"); auto work_dir = fbb.CreateString("/tmp"); @@ -554,6 +555,41 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSandbox) EXPECT_THAT(sb.max_memory_usage.value(), Eq(4096U)); ASSERT_THAT(sb.max_cpu_usage.has_value(), IsTrue()); EXPECT_THAT(sb.max_cpu_usage.value(), Eq(80U)); + ASSERT_THAT(sb.affinity_mask.has_value(), IsTrue()); + EXPECT_THAT(sb.affinity_mask.value(), Eq(3U)); +} + +TEST_F(FlatbufferConfigLoaderTest, LoadSandboxWithoutAffinityMaskHasNoValue) +{ + RecordProperty("Description", "Sandbox without affinity_mask configured leaves it as std::nullopt."); + + ::flatbuffers::FlatBufferBuilder fbb; + + auto sandbox = buildDefaultSandbox(fbb); + + auto bin_dir = fbb.CreateString("/opt"); + auto work_dir = fbb.CreateString("/tmp"); + auto deploy = fb::CreateDeploymentConfig( + fbb, + 500 /*ready_timeout_ms*/, + 500 /*shutdown_timeout_ms*/, + 0 /*environmental_variables*/, + bin_dir, + work_dir, + 0 /*ready_recovery_action*/, + 0 /*recovery_action*/, + sandbox); + + auto comp = buildDefaultComponent(fbb, "unsandboxed_comp", buildDefaultComponentProperties(fbb), deploy); + auto comps = fbb.CreateVector(std::vector<::flatbuffers::Offset>{comp}); + + auto result = loadBuffer(buildConfigWithComponents(fbb, comps)); + + ASSERT_THAT(result.has_value(), IsTrue()); + ASSERT_THAT(result->components().size(), Eq(1U)); + + const auto& sb = result->components()[0].deployment_config.sandbox; + EXPECT_THAT(sb.affinity_mask.has_value(), IsFalse()); } TEST_F(FlatbufferConfigLoaderTest, LoadComponentAliveSupervision) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 854385b092..ed2868c795 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -415,6 +415,7 @@ score::cpp::expected convertSandbox(const fb::San result.scheduling_priority = *scheduling_priority; result.max_memory_usage = optionalScalarValue(fb_sb->max_memory_usage()); result.max_cpu_usage = optionalScalarValue(fb_sb->max_cpu_usage()); + result.affinity_mask = optionalScalarValue(fb_sb->affinity_mask()); return result; } diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 916f6172e9..084945494a 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -661,7 +661,8 @@ TEST_F(ConverterTest, ConvertSandboxValid) fb::SchedulingPolicy::FIFO, 50 /*scheduling_priority*/, 4096 /*max_memory_usage*/, - 80 /*max_cpu_usage*/); + 80 /*max_cpu_usage*/, + 3 /*affinity_mask*/); fbb.Finish(sandbox); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); @@ -678,6 +679,21 @@ TEST_F(ConverterTest, ConvertSandboxValid) EXPECT_THAT(result->max_memory_usage.value(), Eq(4096U)); ASSERT_THAT(result->max_cpu_usage.has_value(), IsTrue()); EXPECT_THAT(result->max_cpu_usage.value(), Eq(80U)); + ASSERT_THAT(result->affinity_mask.has_value(), IsTrue()); + EXPECT_THAT(result->affinity_mask.value(), Eq(3U)); +} + +TEST_F(ConverterTest, ConvertSandboxWithoutAffinityMaskHasNoValue) +{ + RecordProperty("Description", "Sandbox without affinity_mask configured leaves it as std::nullopt."); + ::flatbuffers::FlatBufferBuilder fbb; + auto sandbox = buildDefaultSandbox(fbb); + fbb.Finish(sandbox); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = convertSandbox(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->affinity_mask.has_value(), IsFalse()); } TEST_F(ConverterTest, ConvertSandboxMissingUidReturnsError) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 9e0a1e0ef8..8da36cb7e4 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -128,6 +128,8 @@ table Sandbox { max_memory_usage:uint64 = null; // optional // Maximum CPU usage as a percentage of total CPU capacity. max_cpu_usage:uint32 = null; // optional + // Affinity mask defining on which CPU cores the process may be scheduled. + affinity_mask:uint64 = null; // optional } // Deployment configuration for a component. 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 5a36e01637..380a5813f0 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 @@ -342,12 +342,15 @@ OsalReturnType ProcessLauncher::setSchedulingAndSecurity(const configuration::Sa retval = OsalReturnType::kFail; } - // Set core affinity using OS specific functionality in osal - not in new config, skip - // if (-1 == osal::setaffinity(0)) - // { - // static_cast(signal_safe_log_errno(errno, "setaffinity failed")); - // retval = OsalReturnType::kFail; - // } + // Set core affinity using OS specific functionality in osal + if (config.affinity_mask.has_value()) + { + if (-1 == osal::setaffinity(config.affinity_mask.value())) + { + static_cast(signal_safe_log_errno(errno, "setaffinity failed")); + retval = OsalReturnType::kFail; + } + } // Set group ID if (-1 == setgid(config.gid)) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 308164e3db..9c63800526 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -260,6 +260,28 @@ def is_supervised(application_type): } +def parse_affinity_mask(value, component_name): + """Convert the user-facing hex string (e.g. "0x3") into the uint64 the flatbuffer expects.""" + if not isinstance(value, str) or not value.lower().startswith("0x"): + raise ValueError( + f"Component '{component_name}': affinity_mask must be a hexadecimal string " + f'such as "0x3" with the "0x" prefix, but got {value!r}.' + ) + try: + mask = int(value, 16) + except ValueError: + raise ValueError( + f"Component '{component_name}': affinity_mask must be a hexadecimal string " + f'such as "0x3", but got {value!r}.' + ) + if not 1 <= mask <= 0xFFFFFFFFFFFFFFFF: + raise ValueError( + f"Component '{component_name}': affinity_mask must select at least one core " + f"and at most 64 cores, but got {value!r}." + ) + return mask + + def output_filename(input_filename): stem = Path(input_filename).stem return f"{stem}_gen.json" @@ -336,6 +358,10 @@ def gen_config(output_dir, config, input_filename): sandbox_out["max_memory_usage"] = sandbox["max_memory_usage"] if "max_cpu_usage" in sandbox: sandbox_out["max_cpu_usage"] = sandbox["max_cpu_usage"] + if "affinity_mask" in sandbox: + sandbox_out["affinity_mask"] = parse_affinity_mask( + sandbox["affinity_mask"], component_name + ) deployment = { "ready_timeout_ms": depl_cfg["ready_timeout_ms"], diff --git a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json index 3a2d0efb04..9490356c18 100644 --- a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json +++ b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json @@ -42,7 +42,8 @@ "scheduling_policy": "RR", "scheduling_priority": 75, "max_memory_usage": 268435456, - "max_cpu_usage": 50 + "max_cpu_usage": 50, + "affinity_mask": 3 }, "environmental_variables": [ { @@ -108,7 +109,8 @@ "scheduling_policy": "FIFO", "scheduling_priority": 50, "max_memory_usage": 536870912, - "max_cpu_usage": 80 + "max_cpu_usage": 80, + "affinity_mask": 3 }, "environmental_variables": [ { @@ -171,7 +173,8 @@ "scheduling_policy": "FIFO", "scheduling_priority": 50, "max_memory_usage": 536870912, - "max_cpu_usage": 80 + "max_cpu_usage": 80, + "affinity_mask": 3 }, "environmental_variables": [ { @@ -234,7 +237,8 @@ "scheduling_policy": "FIFO", "scheduling_priority": 50, "max_memory_usage": 536870912, - "max_cpu_usage": 80 + "max_cpu_usage": 80, + "affinity_mask": 3 }, "environmental_variables": [ { diff --git a/scripts/config_mapping/tests/full_config_test/input/lm_config.json b/scripts/config_mapping/tests/full_config_test/input/lm_config.json index af41f5f58e..e9fc5b3c0e 100644 --- a/scripts/config_mapping/tests/full_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/full_config_test/input/lm_config.json @@ -30,7 +30,8 @@ "scheduling_policy": "SCHED_FIFO", "scheduling_priority": 50, "max_memory_usage": 536870912, - "max_cpu_usage": 80 + "max_cpu_usage": 80, + "affinity_mask": "0x3" } }, "component_properties": { diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 556ed726b3..584dc51639 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -31,6 +31,7 @@ is_supervised, load_json_file, output_filename, + parse_affinity_mask, preprocess_defaults, schema_validation, score_defaults, @@ -945,7 +946,7 @@ def test_gen_config_watchdog_partial_fields_omitted(tmp_path): def test_gen_config_with_sandbox_limits(tmp_path): - """sandbox max_memory_usage and max_cpu_usage should appear in output when present.""" + """sandbox max_memory_usage, max_cpu_usage and affinity_mask should appear in output when present.""" config = { "schema_version": 1, "components": { @@ -962,6 +963,7 @@ def test_gen_config_with_sandbox_limits(tmp_path): "gid": 1000, "max_memory_usage": 1024, "max_cpu_usage": 50, + "affinity_mask": "0x3", }, }, } @@ -982,6 +984,72 @@ def test_gen_config_with_sandbox_limits(tmp_path): sandbox = output["components"][0]["deployment_config"]["sandbox"] assert sandbox["max_memory_usage"] == 1024 assert sandbox["max_cpu_usage"] == 50 + assert sandbox["affinity_mask"] == 3 + + +def test_gen_config_without_affinity_mask(tmp_path): + """affinity_mask should be absent from the output when not configured.""" + config = { + "schema_version": 1, + "components": { + "app1": { + "component_properties": { + "application_profile": {"application_type": "REPORTING"} + }, + "deployment_config": { + "ready_timeout_ms": 1000, + "shutdown_timeout_ms": 2000, + "bin_dir": "/opt", + "sandbox": { + "uid": 1000, + "gid": 1000, + }, + }, + } + }, + "run_targets": {"Startup": {}}, + "initial_run_target": "Startup", + "fallback_run_target": {}, + "alive_supervision": {}, + "watchdog": {}, + } + gen_config(str(tmp_path), config, "test_input.json") + + with open(tmp_path / "test_input_gen.json") as f: + output = json.load(f) + + sandbox = output["components"][0]["deployment_config"]["sandbox"] + assert "affinity_mask" not in sandbox + + +@pytest.mark.parametrize( + "value,expected", + [ + ("0x3", 3), + ("0X3", 3), + ("0xff", 255), + ("0xFF", 255), + ("0x00000001", 1), + ("0xFFFFFFFFFFFFFFFF", 0xFFFFFFFFFFFFFFFF), + ], +) +def test_parse_affinity_mask_accepts_valid_values(value, expected): + assert parse_affinity_mask(value, "app1") == expected + + +@pytest.mark.parametrize( + "value", + [ + "0x0", + "3", + "0xZZ", + 3, + None, + ], +) +def test_parse_affinity_mask_rejects_invalid_values(value): + with pytest.raises(ValueError): + parse_affinity_mask(value, "app1") def test_gen_config_output_filename_matches_spec(tmp_path): diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 1b527f2a99..f3e963b198 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -91,7 +91,8 @@ "--uid=0", "--gid=0", "--scheduling-policy=SCHED_RR", - "--scheduling-priority=10" + "--scheduling-priority=10", + "--affinity=0x1" ], "ready_condition": { "process_state": "Terminated" @@ -106,7 +107,8 @@ "gid": 0, "supplementary_group_ids": [], "scheduling_policy": "SCHED_RR", - "scheduling_priority": 10 + "scheduling_priority": 10, + "affinity_mask": "0x1" } } }, @@ -120,7 +122,8 @@ "process_arguments": [ "--uid=0", "--gid=0", - "--working-dir=/tmp/tests/sandbox_options" + "--working-dir=/tmp/tests/sandbox_options", + "--affinity=0x2" ], "ready_condition": { "process_state": "Terminated" @@ -129,6 +132,9 @@ "deployment_config": { "environmental_variables": { "PROCESSIDENTIFIER": "DefaultPG_app2" + }, + "sandbox": { + "affinity_mask": "0x2" } } }, diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 3d680fdcb2..d868f09329 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -123,6 +123,7 @@ def require_realtime_scheduling(request, target): "comp_req__launch_man__scheduling_policy", "comp_req__launch_man__cwd_support", "comp_req__launch_man__supplementary_groups", + "comp_req__launch_man__runmask_support", ], partially_verifies=[], test_type="requirements-based", @@ -130,7 +131,7 @@ def require_realtime_scheduling(request, target): ) def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): """ - Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, and scheduling policy. + Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, scheduling policy, and CPU affinity mask. The launch manager starts with an initial run target. The control daemon activates the "Running" run target (starting the managed process with the sandbox options applied), then transitions back to "Startup", and finally activates "Off". Expected Behaviour: All run target transitions complete successfully and all processes report running. diff --git a/tests/integration/sandbox_options/verify_sandbox.hpp b/tests/integration/sandbox_options/verify_sandbox.hpp index ee9a5b0c94..ca9ffa44a1 100644 --- a/tests/integration/sandbox_options/verify_sandbox.hpp +++ b/tests/integration/sandbox_options/verify_sandbox.hpp @@ -175,9 +175,6 @@ inline ::testing::AssertionResult verifyAffinity(const std::uint64_t expected_af constexpr int kCores = 64; #if defined(__QNX__) - // ThreadCtl only offers a combined get-and-set: request a run mask covering every CPU (a - // no-op given the process is already confined by its actual mask) and read back the mask - // that was in effect beforehand, which is the state configured by the launch manager. constexpr int kSize = RMSK_SIZE(kCores); struct { @@ -185,12 +182,8 @@ inline ::testing::AssertionResult verifyAffinity(const std::uint64_t expected_af unsigned runmask[kSize]; unsigned inherit_mask[kSize]; } tm{kSize, {}, {}}; - for (int cpu = 0; cpu < kCores; ++cpu) - { - RMSK_SET(cpu, tm.runmask); - RMSK_SET(cpu, tm.inherit_mask); - } + // Calling ThreadCtl with an empty mask will only retrieve the current run mask without changing it. if (ThreadCtl(_NTO_TCTL_RUNMASK_GET_AND_SET_INHERIT, &tm) != 0) { failures << "Failed to get CPU affinity\n"; From 438e8a25a8d763c2375ac73f77866d379b2d4581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Thu, 1 Oct 2026 09:35:50 +0200 Subject: [PATCH 2/3] Rework boundary check --- scripts/config_mapping/lifecycle_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 9c63800526..95e6519737 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -274,7 +274,8 @@ def parse_affinity_mask(value, component_name): f"Component '{component_name}': affinity_mask must be a hexadecimal string " f'such as "0x3", but got {value!r}.' ) - if not 1 <= mask <= 0xFFFFFFFFFFFFFFFF: + + if mask < 1 or mask > 0xFFFFFFFFFFFFFFFF: raise ValueError( f"Component '{component_name}': affinity_mask must select at least one core " f"and at most 64 cores, but got {value!r}." From 101c4418ada53d022f38667f4ac23475f5a52ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fu=C3=9Fberger?= Date: Mon, 5 Oct 2026 15:11:58 +0200 Subject: [PATCH 3/3] Extend unit test for processlauncher --- .../details/mock_proc_launch_syscalls.hpp | 7 ++++ .../details/process_launcher_UT.cpp | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) 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 index 404c4b135a..3d714db01a 100644 --- 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 @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ class SyscallMock 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(std::int32_t, setaffinity, (std::uint64_t cpumask), ()); 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), ()); @@ -392,6 +394,11 @@ int setgroups(size_t n, const gid_t* groups) return g_syscall_mock->setgroups(n, groups); } +std::int32_t setaffinity(std::uint64_t cpumask) noexcept(true) +{ + return g_syscall_mock->setaffinity(cpumask); +} + } // namespace score::mw::lifecycle::internal::osal // NOLINTEND 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 index 7cc568f2eb..dec29f0dfa 100644 --- 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 @@ -519,6 +519,38 @@ TEST_F(SetSchedulingAndSecurityTest, setgroupsFails) EXPECT_THROW(static_cast(process_launcher->startProcess(pid_, sync_, config_)), SysExitException); } +TEST_F(SetSchedulingAndSecurityTest, setaffinityFails) +{ + RecordProperty("Description", "Verify that the forked process exits if setting the CPU affinity fails"); + + config_.deployment_config.sandbox.affinity_mask = 0x1U; + + EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0)); + EXPECT_CALL(*g_syscall_mock, setaffinity(0x1U)).WillOnce(SetErrnoAndReturn(EINVAL, -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, setaffinityIgnore) +{ + RecordProperty("Description", "Verify that the CPU affinity is not changed if no affinity mask is configured"); + + config_.deployment_config.sandbox.affinity_mask = std::nullopt; + + 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(0)); + EXPECT_CALL(*g_syscall_mock, setaffinity).Times(0); + + static_cast(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process +} + TEST_F(StartProcessTest, chdirFails) { RecordProperty("Description", "Verify that the forked process exits if changing the working dir fails"); @@ -635,6 +667,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess) const std::vector sgids = {1, 2, 3}; const std::uint64_t mem_limit = 4096; const std::uint32_t cpu_limit = 500; + const std::uint64_t affinity_mask = 0x8000000000000005U; const std::string security_policy = "security"; const std::vector args_in = {"-c 2", "--argument yes"}; const std::vector expected_launch_args = { @@ -647,6 +680,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess) 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.affinity_mask = affinity_mask; config_.deployment_config.sandbox.security_policy = security_policy; config_.deployment_config.environmental_variables.add("environment", "yes"); config_.deployment_config.environmental_variables.add("errors", "no"); @@ -661,6 +695,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess) 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, setaffinity(affinity_mask)).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));