fix: security audit F5–F7 + Pi CI races (bundle) #12

Merged
sirius merged 6 commits from feat/audit-fixes-bundle into main 2026-09-11 00:41:40 +00:00
6 changed files with 56 additions and 8 deletions
Showing only changes of commit 424002c0f5 - Show all commits

View file

@ -1,5 +1,6 @@
//! In-process Arti onion-service helpers (no C-tor). //! In-process Arti onion-service helpers (no C-tor).
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -8,7 +9,7 @@ use arti_client::{TorClient, TorClientConfig};
use futures::StreamExt; use futures::StreamExt;
use safelog::DisplayRedacted; use safelog::DisplayRedacted;
use tor_hsservice::status::State; use tor_hsservice::status::State;
use tor_hsservice::{HsNickname, OnionServiceConfig, RunningOnionService}; use tor_hsservice::{HsId, HsNickname, OnionServiceConfig, RunningOnionService};
use tor_rtcompat::PreferredRuntime; use tor_rtcompat::PreferredRuntime;
pub const HS_PORT: u16 = 80; pub const HS_PORT: u16 = 80;
@ -28,9 +29,24 @@ const PROBE_TIMEOUT: Duration = Duration::from_secs(12);
pub type Client = Arc<TorClient<PreferredRuntime>>; pub type Client = Arc<TorClient<PreferredRuntime>>;
fn mkdir_700(path: &std::path::Path) {
std::fs::create_dir_all(path).expect("mkdir");
let mut perms = std::fs::metadata(path).expect("metadata").permissions();
perms.set_mode(0o700);
std::fs::set_permissions(path, perms).expect("chmod 0700");
}
/// Status/probe log label: safelog-redacted v3 onion, never the locator.
pub fn log_label(onion: &str) -> String {
match onion.parse::<HsId>() {
Ok(id) => id.display_redacted().to_string(),
Err(_) => safelog::sensitive(onion).to_string(),
}
}
pub fn client_config(state_dir: &std::path::Path, cache_dir: &std::path::Path) -> TorClientConfig { pub fn client_config(state_dir: &std::path::Path, cache_dir: &std::path::Path) -> TorClientConfig {
std::fs::create_dir_all(state_dir).expect("state dir"); mkdir_700(state_dir);
std::fs::create_dir_all(cache_dir).expect("cache dir"); mkdir_700(cache_dir);
let mut builder = TorClientConfigBuilder::from_directories(state_dir, cache_dir); let mut builder = TorClientConfigBuilder::from_directories(state_dir, cache_dir);
builder.storage().permissions().dangerously_trust_everyone(); builder.storage().permissions().dangerously_trust_everyone();
builder builder
@ -84,8 +100,8 @@ pub async fn wait_until_published(
client: &Client, client: &Client,
svc: &RunningOnionService, svc: &RunningOnionService,
onion: &str, onion: &str,
label: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let label = log_label(onion);
let deadline = Instant::now() + PUBLISH_WAIT; let deadline = Instant::now() + PUBLISH_WAIT;
let mut events = svc.status_events(); let mut events = svc.status_events();
let mut next_probe = Instant::now() + PROBE_EVERY; let mut next_probe = Instant::now() + PROBE_EVERY;

View file

@ -92,7 +92,7 @@ impl Node {
_svc: Arc::clone(&svc), _svc: Arc::clone(&svc),
rend, rend,
}); });
hs::wait_until_published(&node.client, &svc, &onion, &onion).await?; hs::wait_until_published(&node.client, &svc, &onion).await?;
node.store node.store
.lock() .lock()
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
@ -444,7 +444,7 @@ impl Node {
_svc: Arc::clone(&svc), _svc: Arc::clone(&svc),
rend, rend,
}); });
hs::wait_until_published(&self.client, &svc, &onion, &onion).await?; hs::wait_until_published(&self.client, &svc, &onion).await?;
{ {
let store = self.store.lock().map_err(|e| e.to_string())?; let store = self.store.lock().map_err(|e| e.to_string())?;
store.set_onion(&onion).map_err(|e| e.to_string())?; store.set_onion(&onion).map_err(|e| e.to_string())?;

View file

@ -117,6 +117,7 @@ impl Store {
} }
mkdir_700(home)?; mkdir_700(home)?;
mkdir_700(&home.join("arti"))?; mkdir_700(&home.join("arti"))?;
mkdir_700(&home.join("cache"))?;
let db_path = home.join("onionwire.db"); let db_path = home.join("onionwire.db");
let conn = Connection::open(&db_path)?; let conn = Connection::open(&db_path)?;
let journal: String = conn.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?; let journal: String = conn.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?;

View file

@ -1,5 +1,6 @@
//! HS publish wait and CBT floor — 180s fail-closed cuts a working HsDir upload. //! HS publish wait and CBT floor — 180s fail-closed cuts a working HsDir upload.
use std::os::unix::fs::PermissionsExt;
use std::time::Duration; use std::time::Duration;
use onionwire::hs; use onionwire::hs;
@ -67,3 +68,30 @@ fn broken_or_shutdown_never_ready() {
assert!(!hs::hs_is_ready(State::Broken, true)); assert!(!hs::hs_is_ready(State::Broken, true));
assert!(!hs::hs_is_ready(State::Shutdown, true)); assert!(!hs::hs_is_ready(State::Shutdown, true));
} }
#[test]
fn hs_log_label_is_not_the_full_v3_onion() {
// Public v3 address; checksum is valid so HsId::from_str works.
let onion = "facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion";
let label = hs::log_label(onion);
assert_ne!(label, onion, "status/probe logs must not use the locator");
assert!(
!label.contains("facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd"),
"log label leaked the onion body: {label}"
);
assert!(
label.contains('…') || label.contains("[scrubbed]"),
"expected safelog redaction, got {label}"
);
}
#[test]
fn client_config_mkdirs_state_and_cache_0700() {
let root = tempfile::tempdir().expect("tempdir");
let state = root.path().join("arti");
let cache = root.path().join("cache");
let _cfg = hs::client_config(&state, &cache);
let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
assert_eq!(mode(&state), 0o700);
assert_eq!(mode(&cache), 0o700);
}

View file

@ -55,11 +55,14 @@ fn first_run_creates_0700_dirs_and_self_row() {
let store = Store::open().expect("open"); let store = Store::open().expect("open");
let arti = home.path().join("arti"); let arti = home.path().join("arti");
let cache = home.path().join("cache");
let db = home.path().join("onionwire.db"); let db = home.path().join("onionwire.db");
assert!(arti.is_dir(), "arti dir"); assert!(arti.is_dir(), "arti dir");
assert!(cache.is_dir(), "cache dir");
assert!(db.is_file(), "onionwire.db"); assert!(db.is_file(), "onionwire.db");
assert_eq!(mode(home.path()), 0o700); assert_eq!(mode(home.path()), 0o700);
assert_eq!(mode(&arti), 0o700); assert_eq!(mode(&arti), 0o700);
assert_eq!(mode(&cache), 0o700);
let me = store.self_identity().expect("self"); let me = store.self_identity().expect("self");
assert_eq!(me.identity_pk.len(), 32); assert_eq!(me.identity_pk.len(), 32);

View file

@ -124,10 +124,10 @@ async fn two_node_byte_pipe_restart_and_dormant() {
eprintln!("bob onion={bob_onion}"); eprintln!("bob onion={bob_onion}");
assert_ne!(alice_onion, bob_onion, "separate HS identities"); assert_ne!(alice_onion, bob_onion, "separate HS identities");
hs::wait_until_published(&alice, &alice_svc, &alice_onion, "alice") hs::wait_until_published(&alice, &alice_svc, &alice_onion)
.await .await
.expect("alice publish"); .expect("alice publish");
hs::wait_until_published(&bob, &bob_svc, &bob_onion, "bob") hs::wait_until_published(&bob, &bob_svc, &bob_onion)
.await .await
.expect("bob publish"); .expect("bob publish");