mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
Integrate reviewed executable update acceptance
Retain exact commit 18df4c1b46. Executable identity and bounded successful version probes replace metadata-only acceptance, retaining rollback and prior-active restoration. Source-uy8hwdan contains passing 24 focused methods and 191 subcases with vet; enclosing exit 1 from the incorrect control count and the independent broad installer failure remain adverse evidence, not cleared.
Change-source: pulse-maintainer
This commit is contained in:
@@ -53,6 +53,26 @@ editions cause no release read or installer action, while a recognised community
|
||||
binary still reaches both. These ordinary-user shell fixtures do not establish
|
||||
installed systemd, paid customer or native update acceptance.
|
||||
|
||||
### Unattended update acceptance requires executable version evidence
|
||||
|
||||
After the signed installer returns success, the unattended updater requires the
|
||||
selected primary (or, only when absent, legacy) executable to report a recognised
|
||||
community version on its first line and exit successfully within five seconds,
|
||||
with a one-second KILL backstop. Optional build details on later lines remain
|
||||
valid. An optional `v` prefix is normalised for comparison with the exact target.
|
||||
VERSION sidecars, failed or empty probes, other editions/brands and versions
|
||||
embedded in later lines cannot establish successful replacement. Unknown version
|
||||
identity or a mismatch enters the existing verified rollback transaction, never
|
||||
success. The same bounded read is used before release discovery.
|
||||
|
||||
`TestAutoUpdateBackupTransaction` executes real local signed installers that
|
||||
return success but leave each adverse executable beside a target VERSION file.
|
||||
It checks failure, restored original bytes/modes/metadata/configuration and
|
||||
only-prior-active service restoration, with valid build-detail and unprefixed
|
||||
community controls. `TestAutoUpdateVersionProbeHasHardDeadline` exercises a real
|
||||
TERM-resistant executable. These are source fixtures, not native systemd or
|
||||
installed customer recovery. Consent, signatures and rollback gates are unchanged.
|
||||
|
||||
### Unattended update consent comes from complete configuration
|
||||
|
||||
When `system.json` exists, the unattended updater requires one complete JSON
|
||||
|
||||
@@ -297,3 +297,87 @@ systemctl() { return 0; }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// VERSION is deliberately plausible in every fixture. Neither it nor a legacy
|
||||
// executable can rescue a present primary whose own version evidence is bad.
|
||||
func TestAutoUpdateVersionNeedsSuccessfulExecutable(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, body, legacy, want string
|
||||
nonExecutable, directory bool
|
||||
}{
|
||||
{name: "community", body: "echo 'Pulse v6.5.0'", want: "v6.5.0"},
|
||||
{name: "unprefixed", body: "echo 'Pulse 6.5.0'", want: "v6.5.0"},
|
||||
{name: "build_details", body: "printf 'Pulse v6.5.0\\nBuilt: yesterday\\n'", want: "v6.5.0"},
|
||||
{name: "preview", body: "echo 'Pulse v6.5.0-rc.1'", want: "v6.5.0-rc.1"},
|
||||
{name: "legacy", legacy: "echo 'Pulse v6.5.0'", want: "v6.5.0"},
|
||||
{name: "failed", body: "echo 'Pulse v6.5.0'; exit 1"},
|
||||
{name: "empty", body: ":"},
|
||||
{name: "pro", body: "echo 'Pulse Pro v6.5.0'"},
|
||||
{name: "other_brand", body: "echo 'Other v6.5.0 private-detail'"},
|
||||
{name: "later_line", body: "printf 'Other\\nPulse v6.5.0\\n'"},
|
||||
{name: "invalid_suffix", body: "echo 'Pulse v6.5.0-extra!'"},
|
||||
{name: "non_executable", body: "echo 'Pulse v6.5.0'", nonExecutable: true},
|
||||
{name: "missing"},
|
||||
{name: "bad_primary_over_legacy", body: "exit 1", legacy: "echo 'Pulse v6.5.0'"},
|
||||
{name: "primary_directory", directory: true, legacy: "echo 'Pulse v6.5.0'"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(dir, "bin"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
primary := filepath.Join(dir, "bin", "pulse")
|
||||
if tc.directory {
|
||||
if err := os.Mkdir(primary, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if tc.body != "" {
|
||||
mode := os.FileMode(0700)
|
||||
if tc.nonExecutable {
|
||||
mode = 0600
|
||||
}
|
||||
if err := os.WriteFile(primary, []byte("#!/bin/bash\n"+tc.body+"\n"), mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if tc.legacy != "" {
|
||||
if err := os.WriteFile(filepath.Join(dir, "pulse"), []byte("#!/bin/bash\n"+tc.legacy+"\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "VERSION"), []byte("v6.5.0\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command("bash", "-c", "set -euo pipefail\n"+extractAutoUpdateAdmissionFunction(t, "get_current_version")+"\nget_current_version\n")
|
||||
cmd.Env = append(os.Environ(), "INSTALL_DIR="+dir)
|
||||
out, err := cmd.CombinedOutput()
|
||||
want := tc.want
|
||||
if want == "" {
|
||||
want = "unknown"
|
||||
}
|
||||
if err != nil || strings.TrimSpace(string(out)) != want {
|
||||
t.Fatalf("version: %v, output %q; want %s", err, out, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoUpdateVersionProbeHasHardDeadline(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(dir, "bin"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "bin", "pulse"), []byte("#!/bin/bash\ntrap '' TERM\necho 'Pulse v6.5.0'\nexec sleep 60\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "VERSION"), []byte("v6.5.0\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command("bash", "-c", extractAutoUpdateAdmissionFunction(t, "get_current_version")+"\nget_current_version\n")
|
||||
cmd.Env = append(os.Environ(), "INSTALL_DIR="+dir)
|
||||
start := time.Now()
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil || !strings.HasSuffix(string(out), "unknown\n") || time.Since(start) > 9*time.Second {
|
||||
t.Fatalf("deadline: %v, duration %s, output %s", err, time.Since(start), out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,6 +639,16 @@ func TestAutoUpdateBackupTransaction(t *testing.T) {
|
||||
{name: "invalid_signature", fault: "invalid_signature", preflight: true},
|
||||
{name: "installer_failure", outcome: "failure"},
|
||||
{name: "wrong_version", outcome: "mismatch"},
|
||||
{name: "failed_version_probe", outcome: "probe_failure"},
|
||||
{name: "empty_version_probe", outcome: "probe_empty"},
|
||||
{name: "unrecognised_version_brand", outcome: "probe_unknown"},
|
||||
{name: "different_edition", outcome: "probe_pro"},
|
||||
{name: "nonexecutable_replacement", outcome: "probe_nonexec"},
|
||||
{name: "missing_replacement", outcome: "probe_missing"},
|
||||
{name: "version_on_later_line", outcome: "probe_later_line"},
|
||||
{name: "invalid_version_suffix", outcome: "probe_trailing"},
|
||||
{name: "valid_build_details", outcome: "probe_details", success: true},
|
||||
{name: "valid_unprefixed_version", outcome: "probe_unprefixed", success: true},
|
||||
{name: "original_version_absent", outcome: "failure", noVersion: true},
|
||||
{name: "extended_metadata", fault: "xattrs", outcome: "failure"},
|
||||
{name: "legacy_layout_migration", outcome: "failure", layout: "legacy"},
|
||||
@@ -745,6 +755,20 @@ chmod 700 "$PULSE_INSTALL_DIR/bin/pulse"
|
||||
if [[ -f "$PULSE_INSTALL_DIR/pulse" ]]; then printf 'changed legacy binary\n' > "$PULSE_INSTALL_DIR/pulse"; fi
|
||||
printf '%s\n' "$new_version" > "$PULSE_INSTALL_DIR/VERSION"
|
||||
chmod 600 "$PULSE_INSTALL_DIR/VERSION"
|
||||
# Keep the target VERSION sidecar even when the signed installer leaves an
|
||||
# unusable executable. The real post-install probe must trigger full rollback.
|
||||
case "$OUTCOME" in
|
||||
probe_failure) printf '#!/bin/bash\necho "Pulse v6.5.0"; exit 1\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_empty) printf '#!/bin/bash\nexit 0\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_unknown) printf '#!/bin/bash\necho "Other v6.5.0 private-detail"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_pro) printf '#!/bin/bash\necho "Pulse Pro v6.5.0"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_nonexec) chmod 600 "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_missing) rm "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_later_line) printf '#!/bin/bash\nprintf "Other\\nPulse v6.5.0\\n"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_trailing) printf '#!/bin/bash\necho "Pulse v6.5.0-other!"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_details) printf '#!/bin/bash\nprintf "Pulse v6.5.0\\nBuilt: yesterday\\n"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
probe_unprefixed) printf '#!/bin/bash\necho "Pulse 6.5.0"\n' > "$PULSE_INSTALL_DIR/bin/pulse" ;;
|
||||
esac
|
||||
if [[ "$OUTCOME" == symlink ]]; then
|
||||
rm "$PULSE_INSTALL_DIR/bin/pulse"
|
||||
ln -s "$FIXTURE_DIR/victim" "$PULSE_INSTALL_DIR/bin/pulse"
|
||||
|
||||
@@ -115,23 +115,27 @@ check_auto_updates_enabled() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Get current version
|
||||
# Read version identity from the executable, never installation metadata. This
|
||||
# runs both before discovery and after installer success: a VERSION sidecar can
|
||||
# survive a failed replacement and cannot prove the new binary is usable.
|
||||
get_current_version() {
|
||||
local version=""
|
||||
|
||||
# Try to get version from binary
|
||||
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
|
||||
version=$("$INSTALL_DIR/bin/pulse" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || true)
|
||||
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
|
||||
version=$("$INSTALL_DIR/pulse" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || true)
|
||||
local binary="" version_output="" first_line=""
|
||||
if [[ -e "$INSTALL_DIR/bin/pulse" || -L "$INSTALL_DIR/bin/pulse" ]]; then
|
||||
binary="$INSTALL_DIR/bin/pulse"
|
||||
elif [[ -e "$INSTALL_DIR/pulse" || -L "$INSTALL_DIR/pulse" ]]; then
|
||||
binary="$INSTALL_DIR/pulse"
|
||||
fi
|
||||
|
||||
# Fallback to VERSION file
|
||||
if [[ -z "$version" ]] && [[ -f "$INSTALL_DIR/VERSION" ]]; then
|
||||
version=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null | tr -d '\n' || true)
|
||||
if [[ -z "$binary" || ! -f "$binary" || ! -x "$binary" ]] ||
|
||||
! version_output=$(timeout --kill-after=1 5 "$binary" --version 2>/dev/null); then
|
||||
echo unknown
|
||||
return 0
|
||||
fi
|
||||
first_line=${version_output%%$'\n'*}
|
||||
if [[ "$first_line" =~ ^Pulse\ v?([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
|
||||
printf 'v%s\n' "${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo unknown
|
||||
fi
|
||||
|
||||
echo "${version:-unknown}"
|
||||
}
|
||||
|
||||
# The unattended updater installs public community builds. The separately
|
||||
@@ -492,7 +496,7 @@ perform_update() {
|
||||
# Verify new version
|
||||
local installed_version
|
||||
installed_version=$(get_current_version)
|
||||
if [[ "$installed_version" == "$new_version" ]]; then
|
||||
if [[ "$installed_version" != "unknown" && "${installed_version#v}" == "${new_version#v}" ]]; then
|
||||
log info "Version verified: $installed_version"
|
||||
|
||||
# If Pulse was running before the update, make sure it is running
|
||||
|
||||
Reference in New Issue
Block a user