mirror of
https://github.com/telemt/telemt.git
synced 2026-10-10 11:25:57 +03:00
TOCTOU and lifecycle races across runtime boundaries fixes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
#[cfg(all(test, unix))]
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
@@ -33,7 +34,7 @@ use nix::fcntl::{Flock, FlockArg, OFlag, openat};
|
||||
#[cfg(unix)]
|
||||
use nix::sys::stat::Mode;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(all(test, unix))]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
// Direct relay lifecycle and conntrack publication.
|
||||
@@ -178,10 +179,7 @@ fn open_unknown_dc_log_append_anchored(
|
||||
) -> std::io::Result<std::fs::File> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let parent = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
|
||||
.open(&path.allowed_parent)?;
|
||||
let parent = crate::util::secure_fs::open_dir_nofollow(&path.allowed_parent)?;
|
||||
|
||||
let oflags = OFlag::O_CREAT
|
||||
| OFlag::O_APPEND
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Records one deterministic authentication failure against an isolated shared state.
|
||||
pub(crate) fn auth_probe_record_failure_for_testing(
|
||||
shared: &ProxySharedState,
|
||||
peer_ip: IpAddr,
|
||||
@@ -8,6 +9,7 @@ pub(crate) fn auth_probe_record_failure_for_testing(
|
||||
auth_probe_record_failure_in(shared, peer_ip, now);
|
||||
}
|
||||
|
||||
/// Returns the normalized peer failure streak from an isolated shared state.
|
||||
pub(crate) fn auth_probe_fail_streak_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
peer_ip: IpAddr,
|
||||
@@ -20,6 +22,7 @@ pub(crate) fn auth_probe_fail_streak_for_testing_in_shared(
|
||||
.map(|entry| entry.fail_streak)
|
||||
}
|
||||
|
||||
/// Clears probe entries, exact capacity accounting, and saturation state together.
|
||||
pub(crate) fn clear_auth_probe_state_for_testing_in_shared(shared: &ProxySharedState) {
|
||||
let removed = shared.handshake.auth_probe.len();
|
||||
assert_eq!(shared.handshake.auth_probe_slots.used(), removed);
|
||||
@@ -37,6 +40,7 @@ pub(crate) fn clear_auth_probe_state_for_testing_in_shared(shared: &ProxySharedS
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts one fixture entry while preserving exact registry capacity accounting.
|
||||
pub(crate) fn insert_auth_probe_state_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
peer_ip: IpAddr,
|
||||
@@ -59,22 +63,26 @@ pub(crate) fn insert_auth_probe_state_for_testing_in_shared(
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposes the isolated probe registry to adversarial tests.
|
||||
pub(crate) fn auth_probe_state_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
) -> &DashMap<IpAddr, AuthProbeState> {
|
||||
&shared.handshake.auth_probe
|
||||
}
|
||||
|
||||
/// Returns exact committed probe slots for capacity assertions.
|
||||
pub(crate) fn auth_probe_slots_for_testing_in_shared(shared: &ProxySharedState) -> usize {
|
||||
shared.handshake.auth_probe_slots.used()
|
||||
}
|
||||
|
||||
/// Exposes the isolated saturation state mutex to tests.
|
||||
pub(crate) fn auth_probe_saturation_state_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
) -> &Mutex<Option<AuthProbeSaturationState>> {
|
||||
&shared.handshake.auth_probe_saturation
|
||||
}
|
||||
|
||||
/// Locks isolated saturation state while recovering poisoned test fixtures.
|
||||
pub(crate) fn auth_probe_saturation_state_lock_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
) -> std::sync::MutexGuard<'_, Option<AuthProbeSaturationState>> {
|
||||
@@ -85,6 +93,7 @@ pub(crate) fn auth_probe_saturation_state_lock_for_testing_in_shared(
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
/// Resets the isolated unknown-SNI warning rate limiter.
|
||||
pub(crate) fn clear_unknown_sni_warn_state_for_testing_in_shared(shared: &ProxySharedState) {
|
||||
let mut guard = shared
|
||||
.handshake
|
||||
@@ -94,6 +103,7 @@ pub(crate) fn clear_unknown_sni_warn_state_for_testing_in_shared(shared: &ProxyS
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
/// Evaluates unknown-SNI warning admission at a deterministic instant.
|
||||
pub(crate) fn should_emit_unknown_sni_warn_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
now: Instant,
|
||||
@@ -101,18 +111,21 @@ pub(crate) fn should_emit_unknown_sni_warn_for_testing_in_shared(
|
||||
should_emit_unknown_sni_warn_in(shared, now)
|
||||
}
|
||||
|
||||
/// Clears the isolated invalid-secret warning deduplication set.
|
||||
pub(crate) fn clear_warned_secrets_for_testing_in_shared(shared: &ProxySharedState) {
|
||||
if let Ok(mut guard) = shared.handshake.invalid_secret_warned.lock() {
|
||||
guard.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposes the isolated invalid-secret warning set to tests.
|
||||
pub(crate) fn warned_secrets_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
) -> &Mutex<HashSet<(String, String)>> {
|
||||
&shared.handshake.invalid_secret_warned
|
||||
}
|
||||
|
||||
/// Evaluates peer throttling against the current test clock.
|
||||
pub(crate) fn auth_probe_is_throttled_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
peer_ip: IpAddr,
|
||||
@@ -120,12 +133,14 @@ pub(crate) fn auth_probe_is_throttled_for_testing_in_shared(
|
||||
auth_probe_is_throttled_in(shared, peer_ip, Instant::now())
|
||||
}
|
||||
|
||||
/// Evaluates global saturation throttling against the current test clock.
|
||||
pub(crate) fn auth_probe_saturation_is_throttled_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
) -> bool {
|
||||
auth_probe_saturation_is_throttled_in(shared, Instant::now())
|
||||
}
|
||||
|
||||
/// Evaluates global saturation throttling at a deterministic instant.
|
||||
pub(crate) fn auth_probe_saturation_is_throttled_at_for_testing_in_shared(
|
||||
shared: &ProxySharedState,
|
||||
now: Instant,
|
||||
|
||||
@@ -56,17 +56,25 @@ pub(crate) enum ConntrackClosePolicy {
|
||||
|
||||
pub(crate) struct HandshakeSharedState {
|
||||
pub(crate) auth_probe: DashMap<IpAddr, AuthProbeState>,
|
||||
/// Exact capacity authority for the authentication probe registry.
|
||||
pub(crate) auth_probe_slots: SlotBudget,
|
||||
pub(crate) auth_probe_saturation: Mutex<Option<AuthProbeSaturationState>>,
|
||||
pub(crate) auth_probe_eviction_hasher: RandomState,
|
||||
pub(crate) invalid_secret_warned: Mutex<HashSet<(String, String)>>,
|
||||
pub(crate) unknown_sni_warn_next_allowed: Mutex<Option<Instant>>,
|
||||
/// Stable credential hints keyed by exact peer IP.
|
||||
pub(crate) sticky_user_by_ip: DashMap<IpAddr, u64>,
|
||||
/// Exact capacity authority for peer-IP credential hints.
|
||||
pub(crate) sticky_user_by_ip_slots: SlotBudget,
|
||||
/// Stable credential hints keyed by bounded peer network prefix.
|
||||
pub(crate) sticky_user_by_ip_prefix: DashMap<u64, u64>,
|
||||
/// Exact capacity authority for peer-prefix credential hints.
|
||||
pub(crate) sticky_user_by_ip_prefix_slots: SlotBudget,
|
||||
/// Stable credential hints keyed by normalized SNI hash.
|
||||
pub(crate) sticky_user_by_sni_hash: DashMap<u64, u64>,
|
||||
/// Exact capacity authority for SNI credential hints.
|
||||
pub(crate) sticky_user_by_sni_hash_slots: SlotBudget,
|
||||
/// Bounded recent credential-hint ring used as an authentication fallback.
|
||||
pub(crate) recent_user_ring: Box<[AtomicU64]>,
|
||||
pub(crate) recent_user_ring_seq: AtomicU64,
|
||||
pub(crate) auth_expensive_checks_total: AtomicU64,
|
||||
|
||||
@@ -72,6 +72,51 @@ fn adversarial_parent_swap_after_check_is_blocked_by_anchored_open() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn adversarial_intermediate_parent_swap_is_blocked_by_component_walk() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let directory = tempfile::tempdir().expect("temporary directory must be creatable");
|
||||
let parent = directory.path().join("parent");
|
||||
let moved = directory.path().join("moved");
|
||||
let outside = directory.path().join("outside");
|
||||
fs::create_dir_all(parent.join("nested"))
|
||||
.expect("original nested directory must be creatable");
|
||||
fs::create_dir_all(outside.join("nested"))
|
||||
.expect("outside nested directory must be creatable");
|
||||
|
||||
let candidate = parent.join("nested/unknown-dc.log");
|
||||
let sanitized = sanitize_unknown_dc_log_path(
|
||||
candidate
|
||||
.to_str()
|
||||
.expect("temporary path must be valid UTF-8"),
|
||||
)
|
||||
.expect("candidate must sanitize before intermediate parent swap");
|
||||
assert!(
|
||||
unknown_dc_log_path_is_still_safe(&sanitized),
|
||||
"precondition: target should initially pass revalidation"
|
||||
);
|
||||
|
||||
fs::rename(&parent, &moved).expect("intermediate parent must be movable");
|
||||
symlink(&outside, &parent).expect("intermediate parent symlink must be creatable");
|
||||
|
||||
let err = open_unknown_dc_log_append_anchored(&sanitized)
|
||||
.expect_err("anchored open must reject a swapped intermediate component");
|
||||
let raw = err.raw_os_error();
|
||||
assert!(
|
||||
matches!(
|
||||
raw,
|
||||
Some(libc::ELOOP) | Some(libc::ENOTDIR) | Some(libc::ENOENT)
|
||||
),
|
||||
"component walk must fail closed on intermediate swap, got raw_os_error={raw:?}"
|
||||
);
|
||||
assert!(
|
||||
!outside.join("nested/unknown-dc.log").exists(),
|
||||
"component walk must not create a log through a swapped intermediate directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn anchored_open_nix_path_writes_expected_lines() {
|
||||
|
||||
@@ -1115,7 +1115,8 @@ async fn tls_unknown_sni_reject_handshake_policy_emits_unrecognized_name_alert()
|
||||
// Drain what the server wrote. We expect exactly one TLS alert record:
|
||||
// 0x15 0x03 0x03 0x00 0x02 0x02 0x70
|
||||
// (ContentType.alert, TLS 1.2, length=2, fatal, unrecognized_name)
|
||||
drop(result); // drops the server-side writer so peer_side sees EOF
|
||||
// Drop the server-side writer so `peer_side` observes EOF.
|
||||
drop(result);
|
||||
let mut buf = Vec::new();
|
||||
peer_side.read_to_end(&mut buf).await.unwrap();
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user