use super::*;
use std::os::unix::fs::symlink;
use std::sync::atomic::AtomicUsize;
#[test]
fn owns_current_lock_is_never_trusted_through_a_racing_symlink_swap() {
const ITERATIONS: usize = 20_000;
let workspace = tempfile::tempdir().expect("tempdir");
let outside = tempfile::tempdir().expect("outside tempdir");
let lock = MigrationLock::acquire(workspace.path(), "run-A").expect("acquire");
let lock_path = workspace.path().join(LOCK_FILE);
let staging_symlink = workspace.path().join("staging-symlink");
let staging_mismatched = workspace.path().join("staging-mismatched");
let mismatched = LockRecord {
format_version: LOCK_FORMAT_VERSION,
held_by: "run-A".to_owned(),
token: "mismatched-token".to_owned(),
};
let mismatched_body = serde_json::to_vec(&mismatched).expect("serialize mismatched");
let secret = LockRecord {
format_version: LOCK_FORMAT_VERSION,
held_by: "attacker".to_owned(),
token: lock.token.clone(),
};
let secret_path = outside.path().join("secret.lock");
std::fs::write(
&secret_path,
serde_json::to_vec(&secret).expect("serialize secret"),
)
.expect("plant secret");
std::fs::write(&lock_path, &mismatched_body).expect("seed mismatched");
let trusted = AtomicUsize::new(0);
std::thread::scope(|scope| {
let writer = scope.spawn(|| {
for _ in 0..ITERATIONS {
symlink(&secret_path, &staging_symlink).expect("stage symlink");
std::fs::rename(&staging_symlink, &lock_path)
.expect("atomically swap in the symlink");
std::fs::write(&staging_mismatched, &mismatched_body)
.expect("stage mismatched record");
std::fs::rename(&staging_mismatched, &lock_path)
.expect("atomically swap in the mismatched record");
}
});
while !writer.is_finished() {
if lock.owns_current_lock() {
trusted.fetch_add(1, Ordering::Relaxed);
}
}
writer.join().expect("writer must not panic");
});
assert_eq!(
trusted.load(Ordering::Relaxed),
0,
"a racing symlink swap must never be trusted as this lock's own record"
);
}
#[test]
fn a_symlinked_lock_record_is_never_read_for_its_holder() {
let workspace = tempfile::tempdir().expect("tempdir");
let outside = tempfile::tempdir().expect("outside tempdir");
let secret = outside.path().join("secret");
std::fs::write(&secret, "TOP-SECRET-CONTENT").expect("write secret");
symlink(&secret, workspace.path().join(LOCK_FILE)).expect("plant symlink");
assert_eq!(MigrationLock::holder(workspace.path()), None);
let err = MigrationLock::acquire(workspace.path(), "run-B").expect_err("record remains");
assert!(!err.contains("TOP-SECRET-CONTENT"), "leaked: {err}");
}
#[test]
fn an_oversized_holder_is_refused_before_any_record_exists() {
let workspace = tempfile::tempdir().expect("tempdir");
let err = MigrationLock::acquire(workspace.path(), &"h".repeat(5000))
.expect_err("an oversized holder must be refused");
assert!(err.contains("holder name too long"), "{err}");
assert!(!workspace.path().join(LOCK_FILE).exists());
let lock = MigrationLock::acquire(workspace.path(), "run-B").expect("still acquirable");
lock.release().expect("release");
}
#[test]
fn a_hard_linked_foreign_file_is_never_echoed_as_its_holder() {
for foreign in ["TOP-SECRET-CONTENT\n", "held_by=x\nTOP-SECRET-CONTENT\n"] {
let workspace = tempfile::tempdir().expect("tempdir");
let outside = tempfile::tempdir().expect("outside tempdir");
let secret = outside.path().join("secret");
std::fs::write(&secret, foreign).expect("write secret");
std::fs::hard_link(&secret, workspace.path().join(LOCK_FILE)).expect("plant hard link");
let holder = MigrationLock::holder(workspace.path()).expect("the record exists");
assert!(!holder.contains("TOP-SECRET-CONTENT"), "leaked: {holder}");
let err = MigrationLock::acquire(workspace.path(), "run-B").expect_err("record remains");
assert!(err.contains("record remains"), "{err}");
assert!(!err.contains("TOP-SECRET-CONTENT"), "leaked: {err}");
}
}