bakery: fix correctness, reliability, and security issues from audit
Track switches now always take effect on `update --all` instead of silently no-op'ing or permanently refusing on strict semver comparison. `remove` no longer aborts cleanup on the first failed binary removal, orphaning the systemd unit. State reads/writes are now lock-protected and go through fsync'd atomic writes (also fixes a temp-path collision in binary installs). The index loader falls back to a stale-but-signed cache instead of hard-failing offline. systemd units now re-fetch on every update instead of freezing after first install. `doctor` now flags missing recorded binaries. Security hardening: path-traversal guard on all index-controlled filenames, archive extraction now rejects symlink/traversal entries before tar touches disk, archive temp files use secure unique paths, post_install hooks are gated behind --no-hooks/confirmation, response buffering is capped, empty-checksum downloads get a clear error, and both stable-track CI workflows now hard-fail on a missing signing key (matching the existing dev/rc guard) instead of silently publishing an index next to a stale signature. gen-index.sh now publishes the index and its signature atomically. Also: bakery install on an already-installed package no longer silently reinstalls/downgrades, cmd_update exits non-zero for unknown packages, and the unused toml dependency is removed.
This commit is contained in:
parent
620c5a1317
commit
d45fc422f2
10 changed files with 624 additions and 137 deletions
|
|
@ -1,5 +1,6 @@
|
|||
use crate::track::Track;
|
||||
use anyhow::{Context, Result};
|
||||
use fs4::FileExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -11,6 +12,11 @@ pub struct InstalledPackage {
|
|||
pub binaries: Vec<String>,
|
||||
pub services: Vec<String>,
|
||||
pub installed_at: String,
|
||||
// `#[serde(default)]` so an installed.json written before per-package
|
||||
// track tracking existed still deserializes — defaults to Stable, same
|
||||
// convention as `State.track` above.
|
||||
#[serde(default)]
|
||||
pub track: Track,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
|
|
@ -35,16 +41,37 @@ impl State {
|
|||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let path = state_path();
|
||||
if let Some(dir) = path.parent() {
|
||||
let text = serde_json::to_string_pretty(self)?;
|
||||
bread_utils::atomic::write_atomic(&path, &text, None)
|
||||
.context("writing installed.json")
|
||||
}
|
||||
|
||||
/// Runs `f` against a freshly-loaded `State` while holding an exclusive
|
||||
/// lock on a sibling `installed.json.lock` file, saving the result if `f`
|
||||
/// succeeds. Without this, two concurrent `bakery` invocations each
|
||||
/// load-mutate-save `installed.json` independently and the second save
|
||||
/// silently drops the first's change — the lock serializes the whole
|
||||
/// read-modify-write instead of just the final write.
|
||||
pub fn with_lock<T>(f: impl FnOnce(&mut State) -> Result<T>) -> Result<T> {
|
||||
let lock_path = PathBuf::from(format!("{}.lock", state_path().display()));
|
||||
if let Some(dir) = lock_path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
let text = serde_json::to_string_pretty(self)?;
|
||||
// Write to a temp file then rename for atomicity — avoids a torn write
|
||||
// if the process is killed mid-save.
|
||||
let tmp = path.with_extension("tmp");
|
||||
std::fs::write(&tmp, &text).context("writing installed.json.tmp")?;
|
||||
std::fs::rename(&tmp, &path).context("atomically replacing installed.json")?;
|
||||
Ok(())
|
||||
let lock_file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.context("opening installed.json.lock")?;
|
||||
lock_file
|
||||
.lock_exclusive()
|
||||
.context("locking installed.json.lock")?;
|
||||
|
||||
let mut state = Self::load()?;
|
||||
let result = f(&mut state)?;
|
||||
state.save()?;
|
||||
// Lock releases when `lock_file` drops at end of scope.
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn is_installed(&self, name: &str) -> bool {
|
||||
|
|
@ -85,6 +112,7 @@ mod tests {
|
|||
binaries: vec![],
|
||||
services: vec![],
|
||||
installed_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
track: Track::Stable,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,11 +165,41 @@ mod tests {
|
|||
binaries: vec!["bar".to_string()],
|
||||
services: vec!["bar.service".to_string()],
|
||||
installed_at: "2026-06-01T00:00:00Z".to_string(),
|
||||
track: Track::Beta,
|
||||
});
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let restored: State = serde_json::from_str(&json).unwrap();
|
||||
assert!(restored.is_installed("bar"));
|
||||
assert_eq!(restored.packages["bar"].version, "2.0.0");
|
||||
assert_eq!(restored.packages["bar"].services, ["bar.service"]);
|
||||
assert_eq!(restored.packages["bar"].track, Track::Beta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_package_track_defaults_to_stable_on_old_shape_json() {
|
||||
// Simulates an installed.json entry written before per-package track
|
||||
// tracking existed.
|
||||
let old_shape = r#"{"name":"foo","version":"1.0.0","binaries":[],"services":[],"installed_at":"2026-01-01T00:00:00Z"}"#;
|
||||
let installed: InstalledPackage = serde_json::from_str(old_shape).unwrap();
|
||||
assert_eq!(installed.track, Track::Stable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lock_persists_mutation_across_reload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// SAFETY (test-only): temporarily redirects the state dir env var so
|
||||
// this test doesn't touch the real ~/.local/state/bakery/installed.json.
|
||||
std::env::set_var("XDG_STATE_HOME", dir.path());
|
||||
|
||||
State::with_lock(|state| {
|
||||
state.record(pkg("foo", "1.0.0"));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let reloaded = State::load().unwrap();
|
||||
assert!(reloaded.is_installed("foo"));
|
||||
|
||||
std::env::remove_var("XDG_STATE_HOME");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue