Skip to content

Commit 9700401

Browse files
committed
feat(fuzzing): Fuzzer for ic_http_endpoints CallService
1 parent 1032c06 commit 9700401

File tree

11 files changed

+233
-10
lines changed

11 files changed

+233
-10
lines changed

.gitlab/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ go_deps.bzl @dfinity-lab/teams/idx
124124
/rs/gitlab-ci-config.yml @dfinity-lab/teams/idx
125125
/rs/ic_os/ @dfinity-lab/teams/node-team
126126
/rs/http_endpoints/ @dfinity-lab/teams/networking-team
127+
/rs/http_endpoints/fuzz/ @dfinity-lab/teams/prodsec
127128
/rs/http_utils/ @dfinity-lab/teams/consensus-owners
128129
/rs/https_outcalls/ @dfinity-lab/teams/networking-team
129130
/rs/https_outcalls/consensus/ @dfinity-lab/teams/consensus-owners

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/http_endpoints/fuzz/BUILD.bazel

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
load("//bazel:fuzz_testing.bzl", "rust_fuzz_test_binary")
2+
3+
package(default_visibility = ["//visibility:private"])
4+
5+
MACRO_DEPENDENCIES = []
6+
7+
CALLSERVICE_FUZZER_DEPENDENCIES = [
8+
"//rs/config",
9+
"//rs/http_endpoints/public",
10+
"//rs/interfaces",
11+
"//rs/interfaces/registry",
12+
"//rs/interfaces/registry/mocks",
13+
"//rs/monitoring/logger",
14+
"//rs/monitoring/metrics",
15+
"//rs/protobuf",
16+
"//rs/registry/keys",
17+
"//rs/registry/provisional_whitelist",
18+
"//rs/test_utilities",
19+
"//rs/types/error_types",
20+
"//rs/types/types",
21+
"@crate_index//:arbitrary",
22+
"@crate_index//:crossbeam",
23+
"@crate_index//:bytes",
24+
"@crate_index//:hyper",
25+
"@crate_index//:libfuzzer-sys",
26+
"@crate_index//:mockall",
27+
"@crate_index//:prost",
28+
"@crate_index//:tokio",
29+
"@crate_index//:tower",
30+
"@crate_index//:tower-test",
31+
]
32+
33+
# required to compile tests/common
34+
DEV_DEPENDENCIES = [
35+
"//rs/crypto/tree_hash",
36+
"//rs/interfaces/state_manager",
37+
"//rs/registry/subnet_type",
38+
"//rs/replicated_state",
39+
"//rs/monitoring/pprof",
40+
"//rs/certification/test-utils",
41+
"//rs/crypto/tls_interfaces/mocks",
42+
"//rs/interfaces/mocks",
43+
"//rs/interfaces/state_manager/mocks",
44+
"//rs/registry/routing_table",
45+
"@crate_index//:ic-agent",
46+
]
47+
48+
rust_fuzz_test_binary(
49+
name = "execute_call_service_libfuzzer",
50+
testonly = True,
51+
srcs = [
52+
"fuzz_targets/execute_call_service.rs",
53+
"//rs/http_endpoints/public:tests/common/mod.rs",
54+
],
55+
crate_root = "fuzz_targets/execute_call_service.rs",
56+
proc_macro_deps = MACRO_DEPENDENCIES,
57+
deps = CALLSERVICE_FUZZER_DEPENDENCIES + DEV_DEPENDENCIES,
58+
)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#![no_main]
2+
use bytes::Bytes;
3+
use hyper::{Body, Method, Request, Response};
4+
use ic_config::http_handler::Config;
5+
use ic_error_types::UserError;
6+
use ic_http_endpoints_public::call::CallService;
7+
use ic_http_endpoints_public::metrics::HttpHandlerMetrics;
8+
use ic_http_endpoints_public::validator_executor::ValidatorExecutor;
9+
use ic_interfaces::{
10+
execution_environment::QueryExecutionResponse, ingress_pool::IngressPoolThrottler,
11+
};
12+
use ic_interfaces_registry::RegistryClient;
13+
use ic_logger::replica_logger::no_op_logger;
14+
use ic_metrics::MetricsRegistry;
15+
use ic_registry_provisional_whitelist::ProvisionalWhitelist;
16+
use ic_test_utilities::{
17+
crypto::temp_crypto_component_with_fake_registry,
18+
types::ids::{node_test_id, subnet_test_id},
19+
};
20+
use ic_types::{
21+
malicious_flags::MaliciousFlags,
22+
messages::{CertificateDelegation, SignedIngressContent, UserQuery},
23+
PrincipalId,
24+
};
25+
use libfuzzer_sys::fuzz_target;
26+
use mockall::mock;
27+
use std::{
28+
convert::Infallible,
29+
net::SocketAddr,
30+
str::FromStr,
31+
sync::{Arc, RwLock},
32+
};
33+
use tokio::runtime::Runtime;
34+
use tower::{util::BoxCloneService, Service, ServiceExt};
35+
use tower_test::mock::Handle;
36+
37+
#[path = "../../public/tests/common/mod.rs"]
38+
pub mod common;
39+
use common::{basic_registry_client, get_free_localhost_socket_addr, setup_ingress_filter_mock};
40+
41+
pub type IngressFilterHandle =
42+
Handle<(ProvisionalWhitelist, SignedIngressContent), Result<(), UserError>>;
43+
pub type QueryExecutionHandle =
44+
Handle<(UserQuery, Option<CertificateDelegation>), QueryExecutionResponse>;
45+
46+
mock! {
47+
pub IngressPoolThrottler {}
48+
49+
impl IngressPoolThrottler for IngressPoolThrottler {
50+
fn exceeds_threshold(&self) -> bool;
51+
}
52+
}
53+
54+
// This fuzzer attempts to execute the CallService call method. The input to the call method
55+
// is an HTTP request. Currently only the HTTP request body is fuzzed. The HTTP requests
56+
// headers are fixed.
57+
//
58+
// The fuzz test is only compiled but not executed by CI.
59+
//
60+
// To execute the fuzzer run
61+
// bazel run --config=fuzzing //rs/http_endpoints/fuzz:execute_call_service_libfuzzer -- corpus/
62+
//
63+
// TODO (PSEC-1654): Implement Arbitrary for the request body. Details:
64+
// This initial version of the fuzzer is currently likely ineffective. This is because as soon as the data
65+
// can't be CBOR decoded, is incorrectly signed, or contains a mismatching effective canister id, `call`
66+
// will fail, and such a failure will happen for most mutations of `data`.
67+
// To address this, the next MR (PSEC-1654) will implement Arbitrary so that mutations of the data more
68+
// effectively explore interesting inputs.
69+
fuzz_target!(|data: &[u8]| {
70+
let rt = Runtime::new().unwrap();
71+
let addr = get_free_localhost_socket_addr();
72+
let effective_canister_id = "223xb-saaaa-aaaaf-arlqa-cai";
73+
74+
let mut call_service = new_call_service(addr);
75+
let mut req = Request::builder()
76+
.method(Method::POST)
77+
.uri(format!(
78+
"http://{}/api/v2/canister/{}/call",
79+
addr, effective_canister_id,
80+
))
81+
.header("Content-Type", "application/cbor")
82+
.body(Bytes::from(data.to_vec()))
83+
.expect("Failed to build the request");
84+
85+
// The effective_canister_id is added to the request during routing
86+
// and then removed from the request parts (see `remove_effective_principal_id`
87+
// in http_endponts/public/src/common.rs).
88+
// We simulate that behaviour in this line.
89+
req.extensions_mut()
90+
.insert(PrincipalId::from_str(effective_canister_id).unwrap());
91+
92+
rt.block_on(async move {
93+
call_service
94+
.ready()
95+
.await
96+
.expect("could not create call service")
97+
.call(req)
98+
.await
99+
.unwrap()
100+
});
101+
});
102+
103+
fn new_call_service(
104+
addr: SocketAddr,
105+
) -> BoxCloneService<Request<Bytes>, Response<Body>, Infallible> {
106+
let config = Config {
107+
listen_addr: addr,
108+
..Default::default()
109+
};
110+
let log = no_op_logger();
111+
let metrics_registry = MetricsRegistry::new();
112+
let mock_registry_client: Arc<dyn RegistryClient> = Arc::new(basic_registry_client());
113+
114+
let (ingress_filter, _ingress_filter_handle) = setup_ingress_filter_mock();
115+
let mut ingress_pool_throttler = MockIngressPoolThrottler::new();
116+
ingress_pool_throttler
117+
.expect_exceeds_threshold()
118+
.returning(|| false);
119+
120+
let ingress_throttler = Arc::new(RwLock::new(ingress_pool_throttler));
121+
122+
let (ingress_tx, _ingress_rx) = crossbeam::channel::unbounded();
123+
124+
let sig_verifier = Arc::new(temp_crypto_component_with_fake_registry(node_test_id(1)));
125+
126+
CallService::new_service(
127+
config,
128+
log.clone(),
129+
HttpHandlerMetrics::new(&metrics_registry),
130+
node_test_id(1),
131+
subnet_test_id(1),
132+
Arc::clone(&mock_registry_client),
133+
ValidatorExecutor::new(
134+
Arc::clone(&mock_registry_client),
135+
sig_verifier,
136+
&MaliciousFlags::default(),
137+
log,
138+
),
139+
ingress_filter,
140+
ingress_throttler,
141+
ingress_tx,
142+
)
143+
}

rs/http_endpoints/public/BUILD.bazel

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
load("@rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script")
22
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
33
load("//bazel:defs.bzl", "rust_test_suite_with_extra_srcs")
4+
load("//bazel:fuzz_testing.bzl", "DEFAULT_RUSTC_FLAGS_FOR_FUZZING")
45

5-
package(default_visibility = ["//rs/replica:__subpackages__"])
6+
package(default_visibility = [
7+
"//rs/http_endpoints:__subpackages__",
8+
"//rs/replica:__subpackages__",
9+
])
610

711
DEPENDENCIES = [
812
"//rs/async_utils",
@@ -29,6 +33,7 @@ DEPENDENCIES = [
2933
"@crate_index//:askama",
3034
"@crate_index//:bytes",
3135
"@crate_index//:byte-unit",
36+
"@crate_index//:cfg-if",
3237
"@crate_index//:crossbeam",
3338
"@crate_index//:futures",
3439
"@crate_index//:futures-util",
@@ -101,10 +106,15 @@ rust_library(
101106
aliases = ALIASES,
102107
crate_features = select({
103108
"//bazel:malicious_code_enabled": ["malicious_code"],
109+
"//bazel:fuzzing_code_enabled": ["fuzzing_code"],
104110
"//conditions:default": [],
105111
}),
106112
crate_name = "ic_http_endpoints_public",
107113
proc_macro_deps = MACRO_DEPENDENCIES,
114+
rustc_flags = select({
115+
"//bazel:fuzzing_code_enabled": DEFAULT_RUSTC_FLAGS_FOR_FUZZING,
116+
"//conditions:default": [],
117+
}),
108118
version = "0.9.0",
109119
deps = DEPENDENCIES + [":build_script"],
110120
)

rs/http_endpoints/public/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ askama = { workspace = true }
1111
async-trait = "0.1.68"
1212
bytes = { workspace = true }
1313
byte-unit = "4.0.14"
14+
cfg-if = "1.0.0"
1415
crossbeam = "0.8.2"
1516
hex = "0.4.2"
1617
http = "0.2.5"

rs/http_endpoints/public/src/call.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use tower::{
3838
};
3939

4040
#[derive(Clone)]
41-
pub(crate) struct CallService {
41+
pub struct CallService {
4242
log: ReplicaLogger,
4343
metrics: HttpHandlerMetrics,
4444
node_id: NodeId,
@@ -52,7 +52,7 @@ pub(crate) struct CallService {
5252

5353
impl CallService {
5454
#[allow(clippy::too_many_arguments)]
55-
pub(crate) fn new_service(
55+
pub fn new_service(
5656
config: Config,
5757
log: ReplicaLogger,
5858
metrics: HttpHandlerMetrics,

rs/http_endpoints/public/src/lib.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,29 @@
66
//! naming used in the [Interface
77
//! Specification](https://sdk.dfinity.org/docs/interface-spec/index.html)
88
mod body;
9-
mod call;
109
mod catch_up_package;
1110
mod common;
1211
mod dashboard;
1312
mod health_status_refresher;
14-
mod metrics;
1513
mod pprof;
1614
mod query;
1715
mod read_state;
1816
mod state_reader_executor;
1917
mod status;
2018
mod threads;
2119
mod types;
22-
mod validator_executor;
20+
21+
cfg_if::cfg_if! {
22+
if #[cfg(feature = "fuzzing_code")] {
23+
pub mod validator_executor;
24+
pub mod metrics;
25+
pub mod call;
26+
} else {
27+
mod validator_executor;
28+
mod metrics;
29+
mod call;
30+
}
31+
}
2332

2433
use crate::{
2534
body::BodyReceiverLayer,
@@ -109,7 +118,7 @@ const HTTP_DASHBOARD_URL_PATH: &str = "/_/dashboard";
109118
const CONTENT_TYPE_CBOR: &str = "application/cbor";
110119

111120
#[derive(Debug, Clone, PartialEq)]
112-
pub(crate) struct HttpError {
121+
pub struct HttpError {
113122
pub status: StatusCode,
114123
pub message: String,
115124
}

rs/http_endpoints/public/src/metrics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub const REQUESTS_LABEL_NAMES: [&str; REQUESTS_NUM_LABELS] = [LABEL_REQUEST_TYP
2222
// Struct holding only Prometheus metric objects. Hence, it is thread-safe iff
2323
// the data members are thread-safe.
2424
#[derive(Clone)]
25-
pub(crate) struct HttpHandlerMetrics {
25+
pub struct HttpHandlerMetrics {
2626
pub requests: HistogramVec,
2727
pub request_body_size_bytes: HistogramVec,
2828
pub response_body_size_bytes: HistogramVec,

rs/http_endpoints/public/src/validator_executor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use tokio::sync::oneshot;
2020
const VALIDATOR_EXECUTOR_THREADS: usize = 1;
2121

2222
#[derive(Clone)]
23-
pub(crate) struct ValidatorExecutor<C> {
23+
pub struct ValidatorExecutor<C> {
2424
registry_client: Arc<dyn RegistryClient>,
2525
validator: Arc<dyn HttpRequestVerifier<C, RegistryRootOfTrustProvider>>,
2626
threadpool: ThreadPool,

rs/http_endpoints/public/tests/common/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ fn setup_query_execution_mock() -> (QueryExecutionService, QueryExecutionHandle)
104104
}
105105

106106
#[allow(clippy::type_complexity)]
107-
fn setup_ingress_filter_mock() -> (IngressFilterService, IngressFilterHandle) {
107+
pub fn setup_ingress_filter_mock() -> (IngressFilterService, IngressFilterHandle) {
108108
let (service, handle) = tower_test::mock::pair::<
109109
(ProvisionalWhitelist, SignedIngressContent),
110110
Result<(), UserError>,

0 commit comments

Comments
 (0)