diff --git a/Cargo.lock b/Cargo.lock index d830457256..6bcacf21da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4311,6 +4311,7 @@ dependencies = [ "jsonrpc-http-server", "jsonrpc-pubsub", "jsonrpc-ws-server", + "libc", "log 0.4.11", "lru", "matches", @@ -4359,6 +4360,7 @@ dependencies = [ "solana-version", "solana-vote-program", "spl-token", + "symlink", "systemstat", "tempfile", "thiserror", diff --git a/core/Cargo.toml b/core/Cargo.toml index 313597edb7..0299899afa 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -34,6 +34,7 @@ jsonrpc-derive = "17.0.0" jsonrpc-http-server = "17.0.0" jsonrpc-pubsub = "17.0.0" jsonrpc-ws-server = "17.0.0" +libc = "0.2.81" log = "0.4.11" lru = "0.6.1" miow = "0.2.2" @@ -89,6 +90,7 @@ matches = "0.1.6" num_cpus = "1.13.0" reqwest = { version = "0.10.8", default-features = false, features = ["blocking", "rustls-tls", "json"] } serial_test = "0.4.0" +symlink = "0.1.0" systemstat = "0.1.5" [build-dependencies] diff --git a/core/src/rpc_service.rs b/core/src/rpc_service.rs index 898f713825..40d388e229 100644 --- a/core/src/rpc_service.rs +++ b/core/src/rpc_service.rs @@ -65,7 +65,7 @@ impl RpcRequestMiddleware { Self { ledger_path, snapshot_archive_path_regex: Regex::new( - r"/snapshot-\d+-[[:alnum:]]+\.(tar|tar\.bz2|tar\.zst|tar\.gz)$", + r"^/snapshot-\d+-[[:alnum:]]+\.(tar|tar\.bz2|tar\.zst|tar\.gz)$", ) .unwrap(), snapshot_config, @@ -110,6 +110,26 @@ impl RpcRequestMiddleware { } } + #[cfg(unix)] + async fn open_no_follow(path: impl AsRef) -> std::io::Result { + // Stuck on tokio 0.2 until the jsonrpc crates upgrade + use tokio_02::fs::os::unix::OpenOptionsExt; + tokio_02::fs::OpenOptions::new() + .read(true) + .write(false) + .create(false) + .custom_flags(libc::O_NOFOLLOW) + .open(path) + .await + } + + #[cfg(not(unix))] + async fn open_no_follow(path: impl AsRef) -> std::io::Result { + // TODO: Is there any way to achieve the same on Windows? + // Stuck on tokio 0.2 until the jsonrpc crates upgrade + tokio_02::fs::File::open(path).await + } + fn process_file_get(&self, path: &str) -> RequestMiddlewareAction { let stem = path.split_at(1).1; // Drop leading '/' from path let filename = { @@ -137,8 +157,7 @@ impl RpcRequestMiddleware { RequestMiddlewareAction::Respond { should_validate_hosts: true, response: Box::pin(async { - // Stuck on tokio 0.2 until the jsonrpc crates upgrade - match tokio_02::fs::File::open(filename).await { + match Self::open_no_follow(filename).await { Err(_) => Ok(Self::internal_server_error()), Ok(file) => { let stream = @@ -449,6 +468,7 @@ mod tests { }; use solana_runtime::{bank::Bank, bank_forks::ArchiveFormat, snapshot_utils::SnapshotVersion}; use solana_sdk::{genesis_config::ClusterType, signature::Signer}; + use std::io::Write; use std::net::{IpAddr, Ipv4Addr}; #[test] @@ -566,15 +586,78 @@ mod tests { assert!(rrm_with_snapshot_config .is_file_get_path("/snapshot-100-AvFf9oS8A8U78HdjT9YG2sTTThLHJZmhaMn2g8vkWYnr.tar")); - assert!(!rrm.is_file_get_path( + assert!(!rrm_with_snapshot_config.is_file_get_path( "/snapshot-notaslotnumber-AvFf9oS8A8U78HdjT9YG2sTTThLHJZmhaMn2g8vkWYnr.tar.bz2" )); + assert!(!rrm_with_snapshot_config.is_file_get_path("../../../test/snapshot-123-xxx.tar")); + assert!(!rrm.is_file_get_path("/")); assert!(!rrm.is_file_get_path("..")); assert!(!rrm.is_file_get_path("🎣")); } + #[test] + fn test_process_file_get() { + let mut runtime = tokio_02::runtime::Runtime::new().unwrap(); + + let ledger_path = get_tmp_ledger_path!(); + std::fs::create_dir(&ledger_path).unwrap(); + + let genesis_path = ledger_path.join("genesis.tar.bz2"); + let rrm = RpcRequestMiddleware::new( + ledger_path.clone(), + None, + create_bank_forks(), + RpcHealth::stub(), + ); + + // File does not exist => request should fail. + let action = rrm.process_file_get("/genesis.tar.bz2"); + if let RequestMiddlewareAction::Respond { response, .. } = action { + let response = runtime.block_on(response); + let response = response.unwrap(); + assert_ne!(response.status(), 200); + } else { + panic!("Unexpected RequestMiddlewareAction variant"); + } + + { + let mut file = std::fs::File::create(&genesis_path).unwrap(); + file.write_all(b"should be ok").unwrap(); + } + + // Normal file exist => request should succeed. + let action = rrm.process_file_get("/genesis.tar.bz2"); + if let RequestMiddlewareAction::Respond { response, .. } = action { + let response = runtime.block_on(response); + let response = response.unwrap(); + assert_eq!(response.status(), 200); + } else { + panic!("Unexpected RequestMiddlewareAction variant"); + } + + #[cfg(unix)] + { + std::fs::remove_file(&genesis_path).unwrap(); + { + let mut file = std::fs::File::create(ledger_path.join("wrong")).unwrap(); + file.write_all(b"wrong file").unwrap(); + } + symlink::symlink_file("wrong", &genesis_path).unwrap(); + + // File is a symbolic link => request should fail. + let action = rrm.process_file_get("/genesis.tar.bz2"); + if let RequestMiddlewareAction::Respond { response, .. } = action { + let response = runtime.block_on(response); + let response = response.unwrap(); + assert_ne!(response.status(), 200); + } else { + panic!("Unexpected RequestMiddlewareAction variant"); + } + } + } + #[test] fn test_health_check_with_no_trusted_validators() { let rm = RpcRequestMiddleware::new(