mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Keep DSM config restoration atomically non-clobbering
A pathname absence check followed by mv could overwrite a concurrent root-created job. Use exact-target non-replacement links for override creation and config restoration; directory races also refuse without creating nested artifacts. Confined concurrent-file/directory controls are mandatory. Partial activation is never restarted and conflicting staged recovery is never discarded. Change-source: pulse-maintainer
This commit is contained in:
@@ -33,13 +33,13 @@ configuration inode is staged outside Upstart's `.conf` namespace until checked
|
||||
reload, exact unknown-job status and independent process absence establish
|
||||
retirement. On unconfirmed retirement it is restored only if the original path
|
||||
is still absent, leaving the manual override and all local recovery state;
|
||||
a concurrently created config is never overwritten and the staged recovery copy
|
||||
is retained. Only confirmed retirement removes the staged config and manual
|
||||
atomic non-replacement links refuse concurrently created files or directories,
|
||||
including during restore; the staged recovery copy is retained on conflict. Only confirmed retirement removes the staged config and manual
|
||||
before credential/state erasure. No credentials, arguments or config contents
|
||||
are returned by these observations. Existing same-instance URL, exact record,
|
||||
TLS, systemd/OpenRC/rc.d/SysV and state-directory ownership remain unchanged.
|
||||
|
||||
The registered `scripts/installtests/install_sh_test.go` requires all fourteen
|
||||
The registered `scripts/installtests/install_sh_test.go` requires all fifteen
|
||||
`agent_upstart_teardown.py` production-function/caller controls, including exact
|
||||
file restoration, effective activation, late refusal, stopped/absent/repeated
|
||||
removal and real timeout-child termination. These are confined ordinary-user
|
||||
|
||||
@@ -34,13 +34,13 @@ configuration inode is staged outside Upstart's `.conf` namespace until checked
|
||||
reload, exact unknown-job status and independent process absence establish
|
||||
retirement. On unconfirmed retirement it is restored only if the original path
|
||||
is still absent, leaving the manual override and all local recovery state;
|
||||
a concurrently created config is never overwritten and the staged recovery copy
|
||||
is retained. Only confirmed retirement removes the staged config and manual
|
||||
atomic non-replacement links refuse concurrently created files or directories,
|
||||
including during restore; the staged recovery copy is retained on conflict. Only confirmed retirement removes the staged config and manual
|
||||
before credential/state erasure. No credentials, arguments or config contents
|
||||
are returned by these observations. Existing same-instance URL, exact record,
|
||||
TLS, systemd/OpenRC/rc.d/SysV and state-directory ownership remain unchanged.
|
||||
|
||||
The registered `scripts/installtests/install_sh_test.go` requires all fourteen
|
||||
The registered `scripts/installtests/install_sh_test.go` requires all fifteen
|
||||
`agent_upstart_teardown.py` production-function/caller controls, including exact
|
||||
file restoration, effective activation, late refusal, stopped/absent/repeated
|
||||
removal and real timeout-child termination. These are confined ordinary-user
|
||||
|
||||
+7
-2
@@ -1622,7 +1622,7 @@ quiesce_upstart_agent_for_removal() {
|
||||
pending=$(mktemp "${override}.XXXXXX") || return 1
|
||||
# Link without replacement: an override created concurrently is not
|
||||
# ours to overwrite. The temporary file contains no credential.
|
||||
if ! { printf 'manual\n' > "$pending" && chmod 644 "$pending" && ln -- "$pending" "$override"; }; then
|
||||
if ! { printf 'manual\n' > "$pending" && chmod 644 "$pending" && ln -T -- "$pending" "$override"; }; then
|
||||
rm -f -- "$pending"
|
||||
return 1
|
||||
fi
|
||||
@@ -1656,7 +1656,12 @@ teardown_upstart_agent_service() {
|
||||
confirmed=true
|
||||
fi
|
||||
if [[ "$confirmed" != true ]]; then
|
||||
if [[ ! -e "$conf" && ! -L "$conf" ]]; then mv -- "$retired" "$conf" || return 1; fi
|
||||
if [[ ! -e "$conf" && ! -L "$conf" ]]; then
|
||||
# Atomic no-replacement restore, including a target that
|
||||
# becomes a directory after the preceding pathname check.
|
||||
ln -T -- "$retired" "$conf" || return 1
|
||||
rm -f -- "$retired" || return 1
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
rm -f -- "$retired" || return 1
|
||||
|
||||
@@ -188,6 +188,18 @@ rm() {
|
||||
printf 'file:remove\n' >> "$EVENTS"
|
||||
builtin command rm "$@"
|
||||
}
|
||||
ln() {
|
||||
local arg
|
||||
for arg in "$@"; do [[ "$arg" == -T || "$arg" == -- || "$arg" == "$FIXTURE/"* ]] || return 97; done
|
||||
[[ "$1" == -T && "$2" == -- ]] || return 98
|
||||
if [[ "$FAULT" == concurrent-override-file && "$4" == "$FIXTURE/init/pulse-agent.override" ]]; then
|
||||
printf 'concurrent override retained\n' > "$4"
|
||||
elif [[ "$FAULT" == concurrent-override-directory && "$4" == "$FIXTURE/init/pulse-agent.override" ]]; then
|
||||
mkdir "$4"
|
||||
fi
|
||||
printf 'file:link\n' >> "$EVENTS"
|
||||
builtin command ln "$@"
|
||||
}
|
||||
mv() {
|
||||
local arg
|
||||
for arg in "$@"; do [[ "$arg" == -- || "$arg" == "$FIXTURE/"* ]] || return 97; done
|
||||
@@ -283,6 +295,18 @@ remove_safe_profile_state_dir() { builtin command rm -r -- "$SAFE_PROFILE_STATE_
|
||||
self.assertEqual(after, before)
|
||||
self.assertEqual(events, [])
|
||||
|
||||
def test_concurrent_override_file_or_directory_is_not_replaced_or_written_inside(self):
|
||||
for fault in ("concurrent-override-file", "concurrent-override-directory"):
|
||||
with self.subTest(fault=fault):
|
||||
r, events, before, after, _ = self.exercise(fault=fault)
|
||||
self.assertNotEqual(r.returncode, 0, r.stderr)
|
||||
self.assertNotIn("remote:remove", events)
|
||||
self.assertNotIn("manager:reload", events)
|
||||
for name in before: self.assertEqual(after[name], before[name])
|
||||
self.assertEqual(set(after) - set(before), {"init/pulse-agent.override"})
|
||||
payload = b"concurrent override retained\n" if fault.endswith("file") else None
|
||||
self.assertEqual(after["init/pulse-agent.override"][2], payload)
|
||||
|
||||
def test_missing_tools_refuse_without_remote_or_erasure(self):
|
||||
for fault in ("missing-initctl", "missing-timeout"):
|
||||
with self.subTest(fault=fault):
|
||||
|
||||
@@ -8481,6 +8481,7 @@ func TestInstallSHUpstartTeardownRequiresObservedQuiescence(t *testing.T) {
|
||||
"test_effective_disabled_config_is_required_after_override_reload",
|
||||
"test_independent_process_inventory_rejects_orphan_or_unavailable_read",
|
||||
"test_untrusted_config_override_and_prior_retirement_refuse_without_changes",
|
||||
"test_concurrent_override_file_or_directory_is_not_replaced_or_written_inside",
|
||||
"test_missing_tools_refuse_without_remote_or_erasure",
|
||||
"test_remote_refusal_keeps_stopped_disabled_recovery_without_restart",
|
||||
"test_late_manager_refusal_retains_recovery_after_remote_confirmation",
|
||||
@@ -8495,7 +8496,7 @@ func TestInstallSHUpstartTeardownRequiresObservedQuiescence(t *testing.T) {
|
||||
t.Fatalf("DSM Upstart control %s did not pass:\n%s", name, output)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(output), "\nRan 14 tests in ") || !strings.Contains(string(output), "\nOK\n") {
|
||||
if !strings.Contains(string(output), "\nRan 15 tests in ") || !strings.Contains(string(output), "\nOK\n") {
|
||||
t.Fatalf("incomplete DSM Upstart verdict:\n%s", output)
|
||||
}
|
||||
t.Logf("%s", output)
|
||||
|
||||
Reference in New Issue
Block a user