Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more tests to FDA check #7338

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions talpid-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,7 @@ features = [

[build-dependencies]
tonic-build = { workspace = true, default-features = false, features = ["transport", "prost"] }


[dev-dependencies]
tokio = { workspace = true, features = [ "io-util", "test-util", "time" ] }
102 changes: 64 additions & 38 deletions talpid-core/src/split_tunnel/macos/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ use libc::pid_t;
use serde::Deserialize;
use std::{
collections::{HashMap, HashSet},
future::Future,
io,
path::PathBuf,
process::{ExitStatus, Stdio},
process::Stdio,
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
Expand Down Expand Up @@ -110,8 +109,7 @@ pub async fn has_full_disk_access() -> bool {
let stderr = proc.stderr.take().unwrap();
drop(proc.stdin.take());

let has_full_disk_access =
parse_logger_status(proc.wait(), stdout, stderr).await == NeedFda::No;
let has_full_disk_access = parse_logger_status(stdout, stderr).await == NeedFda::No;
Ok::<bool, Error>(has_full_disk_access)
})
.await
Expand All @@ -127,7 +125,6 @@ enum NeedFda {
/// Return whether `proc` reports that full-disk access is unavailable based on its output
/// If it cannot be determined that access is available, it is assumed to be available
async fn parse_logger_status(
proc: impl Future<Output = io::Result<ExitStatus>>,
stdout: impl AsyncRead + Unpin + Send + 'static,
stderr: impl AsyncRead + Unpin + Send + 'static,
) -> NeedFda {
Expand Down Expand Up @@ -155,21 +152,15 @@ async fn parse_logger_status(
}
});

let proc = tokio::time::timeout(EARLY_FAIL_TIMEOUT, proc);
let deadline = tokio::time::sleep(EARLY_FAIL_TIMEOUT);

tokio::select! {
// Received standard err/out
biased; need_full_disk_access = &mut need_full_disk_access => {
need_full_disk_access.unwrap_or(NeedFda::No)
}
proc_result = proc => {
if let Ok(Ok(_exit_status)) = proc_result {
// Process exited
return need_full_disk_access.await.unwrap_or(NeedFda::No);
}
// Timeout or `Child::wait`` returned an error
NeedFda::No
}
// Timed out while checking for full-disk access
_ = deadline => NeedFda::No,
}
}

Expand Down Expand Up @@ -562,12 +553,28 @@ fn check_os_version_support_inner(version: MacosVersion) -> Result<(), Error> {

#[cfg(test)]
mod test {
use super::{
check_os_version_support_inner, parse_logger_status, NeedFda, EARLY_FAIL_TIMEOUT,
MIN_OS_VERSION,
};
use std::{process::ExitStatus, time::Duration};
use super::*;

use std::time::Duration;
use talpid_platform_metadata::MacosVersion;
use tokio::io::{simplex, AsyncWriteExt};

/// A mock-version of std{out,err}. [tokio::io::SimplexStream] implements [AsyncRead], so it can be used to test
/// [parse_logger_status].
fn output(msg: &'static str, lag: Duration) -> impl AsyncRead + Unpin + Send + 'static {
// Ensure that 'msg' contains a newline to prevent user errors
assert!(
msg.contains('\n'),
"Message does not contain a newline!! Make sure to add a newline to '{msg}'"
);
let (stdout_read, mut stdout_write) = simplex(msg.as_bytes().len());
// "print" to "stdout" after `duration`.
tokio::spawn(async move {
tokio::time::sleep(lag).await;
stdout_write.write_all(msg.as_bytes()).await.unwrap();
});
stdout_read
}

#[test]
fn test_min_os_version() {
Expand All @@ -590,7 +597,6 @@ mod test {
#[tokio::test]
async fn test_parse_logger_status_missing_access() {
let need_fda = parse_logger_status(
async { Ok(ExitStatus::default()) },
&[][..],
b"ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n".as_slice(),
)
Expand All @@ -603,15 +609,11 @@ mod test {
);
}

/// If the check times out and 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED' is not in stderr, assume
/// full-disk access is available.
/// If process exits without 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED', assume full-disk access
/// is available.
#[tokio::test]
async fn test_parse_logger_status_timeout() {
async fn test_parse_logger_status_immediate_exit() {
let need_fda = parse_logger_status(
async {
tokio::time::sleep(EARLY_FAIL_TIMEOUT + Duration::from_secs(10)).await;
Ok(ExitStatus::default())
},
b"nothing to see here\n".as_slice(),
b"nothing to see here\n".as_slice(),
)
Expand All @@ -620,25 +622,49 @@ mod test {
assert_eq!(
need_fda,
NeedFda::No,
"expected 'NeedFda::No' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED wasn't present"
"expected 'NeedFda::No' on immediate exit",
);
}

/// If process exits without 'ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED', assume full-disk access
/// is available.
/// Check that [parse_logger_status] returns within a reasonable timeframe.
/// "Reasonable" being within [EARLY_FAIL_TIMEOUT].
#[tokio::test(start_paused = true)]
async fn test_parse_logger_status_responsive() {
let start = tokio::time::Instant::now();
let stdout = output("This will never be printed\n", Duration::MAX);
let stderr = output(
"ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED\n",
EARLY_FAIL_TIMEOUT / 2,
);
tokio::time::resume();

let need_fda = parse_logger_status(stdout, stderr).await;

tokio::time::pause();

assert_eq!(
need_fda,
NeedFda::Yes,
"expected 'NeedFda::Yes' when ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED was eventually printed to stderr"
);

// Assert that we did not spend more time waiting than we should
assert!(start.elapsed() < EARLY_FAIL_TIMEOUT);
}

/// Check that [parse_logger_status] doesn't get stuck because nothing is ever output
/// to std{out,err}. It should time out with the assumption that full-disk access is available.
#[tokio::test]
async fn test_parse_logger_status_immediate_exit() {
let need_fda = parse_logger_status(
async { Ok(ExitStatus::default()) },
b"nothing to see here\n".as_slice(),
b"nothing to see here\n".as_slice(),
)
.await;
async fn test_parse_logger_status_timeout() {
let stdout = output("This will never be printed\n", Duration::MAX);
let stderr = output("This will never be printed\n", Duration::MAX);

let need_fda = parse_logger_status(stdout, stderr).await;

assert_eq!(
need_fda,
NeedFda::No,
"expected 'NeedFda::No' on immediate exit",
"expected 'NeedFda::No' when nothing was ever printed to stdout or stderr"
);
}
}
Loading