Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crates/daemon/tests/daemon_foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,40 @@ async fn project_config_watcher_enqueues_project_reconciliation() -> Result<()>
Ok(())
}

#[tokio::test]
async fn project_config_watcher_survives_disappearing_database_side_files() -> Result<()> {
let tempdir = tempdir()?;
let paths = PvPaths::for_home(tempdir.path().join("home"));
drop(Database::open(&paths)?);

for name in ["pv.db-wal", "pv.db-shm"] {
let auxiliary_path = paths.root().join(name);
state::fs::write_sensitive_file(&auxiliary_path, "")?;
assert!(state::fs::path_exists(&auxiliary_path));
state::fs::remove_file(&auxiliary_path)?;
}

let daemon = daemon::RunningDaemon::start(paths.clone()).await?;
sleep(Duration::from_millis(250)).await;

let health_lines = request_lines(
&paths,
json!({
"protocol_version": daemon::PROTOCOL_VERSION,
"command": "health",
}),
)
.await?;

daemon.shutdown().await?;

assert_eq!(health_lines.len(), 1);
assert_eq!(health_lines[0]["status"], json!("ok"));
assert!(Database::open(&paths)?.recent_jobs()?.is_empty());

Ok(())
}

#[tokio::test]
async fn dns_resolver_answers_udp_a_and_aaaa_for_test_hostnames() -> Result<()> {
let tempdir = tempdir()?;
Expand Down
88 changes: 81 additions & 7 deletions crates/state/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,25 @@ pub(crate) fn database_exists(paths: &PvPaths) -> bool {
}

pub(crate) fn secure_database_files(paths: &PvPaths) -> Result<(), StateError> {
for (_, path) in database_files(paths) {
if !path_exists(&path) {
continue;
}
secure_sensitive_file(paths.db())?;

secure_sensitive_file(&path)?;
for path in database_auxiliary_files(paths) {
secure_database_auxiliary_file(&path)?;
}

Ok(())
}

fn secure_database_auxiliary_file(path: &Utf8Path) -> Result<(), StateError> {
match secure_sensitive_file(path) {
Ok(()) => Ok(()),
Err(StateError::Filesystem { source, .. }) if source.kind() == io::ErrorKind::NotFound => {
Ok(())
}
Err(error) => Err(error),
}
}

pub(crate) fn secure_sensitive_file(path: &Utf8Path) -> Result<(), StateError> {
require_owner_only_filesystem()?;
set_file_mode(path, SENSITIVE_FILE_MODE)?;
Expand All @@ -193,6 +201,13 @@ fn database_files(paths: &PvPaths) -> [(&'static str, Utf8PathBuf); 3] {
]
}

fn database_auxiliary_files(paths: &PvPaths) -> [Utf8PathBuf; 2] {
[
paths.root().join("pv.db-wal"),
paths.root().join("pv.db-shm"),
]
}

pub fn ensure_user_dir(path: &Utf8Path) -> Result<(), StateError> {
require_owner_only_filesystem()?;
create_dir_all(path)?;
Expand Down Expand Up @@ -604,14 +619,73 @@ fn require_owner_only_filesystem() -> Result<(), StateError> {
#[cfg(test)]
mod tests {
use camino::Utf8Path;
#[cfg(windows)]
use camino_tempfile::tempdir;

use super::temporary_path_for;
#[cfg(windows)]
use super::{ensure_user_dir, path_exists};
#[cfg(unix)]
use super::{
path_exists, remove_file, secure_database_auxiliary_file, secure_database_files,
write_sensitive_file,
};
#[cfg(unix)]
use crate::PvPaths;
#[cfg(windows)]
use crate::{StateCapability, StateError};
use crate::StateCapability;
use crate::StateError;

#[cfg(unix)]
#[test]
fn missing_persistent_database_file_is_not_ignored() -> anyhow::Result<()> {
let tempdir = tempdir()?;
let paths = PvPaths::for_home(tempdir.path().join("home"));

let result = secure_database_files(&paths);

assert!(matches!(
result,
Err(StateError::Filesystem { path, source })
if path == paths.db() && source.kind() == std::io::ErrorKind::NotFound
));

Ok(())
}

#[cfg(unix)]
#[test]
fn disappearing_database_auxiliary_file_is_ignored() -> anyhow::Result<()> {
let tempdir = tempdir()?;
let paths = PvPaths::for_home(tempdir.path().join("home"));
let auxiliary_path = paths.root().join("pv.db-wal");
write_sensitive_file(&auxiliary_path, "")?;
assert!(path_exists(&auxiliary_path));
remove_file(&auxiliary_path)?;

secure_database_auxiliary_file(&auxiliary_path)?;

Ok(())
}

#[cfg(unix)]
#[test]
fn database_auxiliary_file_security_errors_are_preserved() -> anyhow::Result<()> {
let tempdir = tempdir()?;
let paths = PvPaths::for_home(tempdir.path().join("home"));
let blocking_file = paths.root().join("blocking-file");
write_sensitive_file(&blocking_file, "")?;
let auxiliary_path = blocking_file.join("pv.db-wal");

let result = secure_database_auxiliary_file(&auxiliary_path);

assert!(matches!(
result,
Err(StateError::Filesystem { path, source })
if path == auxiliary_path && source.kind() == std::io::ErrorKind::NotADirectory
));

Ok(())
}

#[test]
fn temporary_paths_keep_the_target_extension_in_the_derived_name() {
Expand Down
Loading