diff --git a/enroll/harvest.py b/enroll/harvest.py index 3e915cb..664d1ae 100644 --- a/enroll/harvest.py +++ b/enroll/harvest.py @@ -1121,8 +1121,14 @@ _SYSCTL_GENERATED_SRC_REL = "sysctl/99-enroll.conf" # config. This avoids generating a file that tries to replay one-shot triggers or # host identity that should be managed elsewhere (e.g. /etc/hostname). _SYSCTL_VOLATILE_KEYS = { + "fs.binfmt_misc.status", "kernel.domainname", "kernel.hostname", + "kernel.kexec_load_disabled", + "kernel.kexec_load_limit_panic", + "kernel.kexec_load_limit_reboot", + "kernel.max_rcu_stall_to_panic", + "kernel.modules_disabled", "kernel.ns_last_pid", "net.ipv4.route.flush", "net.ipv6.route.flush", @@ -1131,6 +1137,21 @@ _SYSCTL_VOLATILE_KEYS = { "vm.stat_refresh", } +_SYSCTL_VOLATILE_PREFIXES = ( + "fs.binfmt_misc.", + "kernel.sched_domain.", +) + +# These are paired with ratio/byte counterparts. The inactive side appears as 0 +# when read; replaying that 0 through sysctl -p is noisy and can be rejected by +# kernels that enforce minimum values. +_SYSCTL_SKIP_ZERO_VALUE_KEYS = { + "vm.dirty_background_bytes", + "vm.dirty_background_ratio", + "vm.dirty_bytes", + "vm.dirty_ratio", +} + def _sysctl_proc_path(key: str) -> str: return "/proc/sys/" + key.replace(".", "/") @@ -1139,7 +1160,9 @@ def _sysctl_proc_path(key: str) -> str: def _sysctl_key_is_persistable(key: str) -> tuple[bool, str]: if not key or not _SYSCTL_KEY_RE.fullmatch(key): return False, "invalid key" - if key in _SYSCTL_VOLATILE_KEYS: + if key in _SYSCTL_VOLATILE_KEYS or any( + key.startswith(prefix) for prefix in _SYSCTL_VOLATILE_PREFIXES + ): return False, "volatile/action key" proc_path = _sysctl_proc_path(key) @@ -1155,6 +1178,17 @@ def _sysctl_key_is_persistable(key: str) -> tuple[bool, str]: return True, "" +def _sysctl_entry_is_persistable(key: str, value: str) -> tuple[bool, str]: + ok, reason = _sysctl_key_is_persistable(key) + if not ok: + return ok, reason + + if key in _SYSCTL_SKIP_ZERO_VALUE_KEYS and str(value).strip() == "0": + return False, "inactive mutually-exclusive zero value" + + return True, "" + + def _parse_sysctl_a_output( text: str, *, @@ -1199,7 +1233,7 @@ def _parse_sysctl_a_output( skipped["duplicate"] += 1 continue if require_persistable: - ok, _reason = _sysctl_key_is_persistable(key) + ok, _reason = _sysctl_entry_is_persistable(key, value) if not ok: skipped["non_persistable"] += 1 continue diff --git a/tests/test_harvest_helpers.py b/tests/test_harvest_helpers.py index 03ec9ed..5131809 100644 --- a/tests/test_harvest_helpers.py +++ b/tests/test_harvest_helpers.py @@ -327,6 +327,52 @@ def test_parse_sysctl_a_output_keeps_persistable_values(monkeypatch): assert skipped["duplicate"] == 1 +def test_sysctl_filter_skips_non_replayable_runtime_keys(monkeypatch): + for key in ( + "fs.binfmt_misc.status", + "fs.binfmt_misc.register", + "kernel.kexec_load_disabled", + "kernel.kexec_load_limit_panic", + "kernel.kexec_load_limit_reboot", + "kernel.max_rcu_stall_to_panic", + "kernel.modules_disabled", + "kernel.sched_domain.cpu0.domain0.flags", + ): + ok, reason = h._sysctl_key_is_persistable(key) + assert ok is False + assert reason == "volatile/action key" + + monkeypatch.setattr(h, "_sysctl_key_is_persistable", lambda key: (True, "")) + for key in ( + "vm.dirty_background_bytes", + "vm.dirty_background_ratio", + "vm.dirty_bytes", + "vm.dirty_ratio", + ): + ok, reason = h._sysctl_entry_is_persistable(key, "0") + assert ok is False + assert reason == "inactive mutually-exclusive zero value" + assert h._sysctl_entry_is_persistable(key, "10")[0] is True + + +def test_parse_sysctl_a_output_skips_non_replayable_values(monkeypatch): + monkeypatch.setattr( + h, + "_sysctl_key_is_persistable", + lambda key: (key != "kernel.modules_disabled", "volatile/action key"), + ) + + params, skipped = h._parse_sysctl_a_output( + "kernel.modules_disabled = 0\n" + "vm.dirty_background_bytes = 0\n" + "vm.dirty_ratio = 20\n" + "net.ipv4.ip_forward = 1\n" + ) + + assert params == {"net.ipv4.ip_forward": "1", "vm.dirty_ratio": "20"} + assert skipped["non_persistable"] == 2 + + def test_collect_sysctl_snapshot_writes_generated_artifact(monkeypatch, tmp_path: Path): monkeypatch.setattr( h,