Skip to content
Merged
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
12 changes: 11 additions & 1 deletion core/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,19 @@ arbitrary = { version = "1.4.2", features = ["derive"] }
libfuzzer-sys = "0.4"
log = { workspace = true }
logforth = { workspace = true }
opendal = { path = "..", features = ["tests"] }
opendal = { path = "..", features = ["tests", "services-fs"] }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find! Do we need a fallback since we could always exit 1 and warn the user in fuzzer test? Because a test failure will force us to check CI status.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @erickguan! Good question. The fallback is deliberately scoped to the unconfigured case only:

  • init_test_service() returns Ok(None) when OPENDAL_TEST is unset, Ok(Some(op)) when a service is selected and inits, and Err(..) when a selected service fails to init.
  • So init_test_service().expect("operator init must succeed") still panics loudly if someone explicitly sets e.g. OPENDAL_TEST=s3 and it fails to init — a genuine misconfiguration does fail the run and shows up in CI, exactly as you want. The fs fallback only kicks in for the Ok(None) (nothing selected) case.

The reason we fall back instead of exit 1 in that unconfigured case is OSS-Fuzz: it runs these binaries continuously with no OPENDAL_TEST env set (that's the root cause of the zero coverage in #7722). If the target exited 1 / panicked when unconfigured, OSS-Fuzz would just record every run as an immediate crash and we'd still get no useful coverage — the same dead state we're fixing, only louder. Falling back to fs (which needs no external setup beyond a temp dir, and exercises OpenDAL's real path-handling code) gives the fuzzer something concrete to drive, while local/CI runs can still target any specific service via OPENDAL_TEST.

Happy to switch the unconfigured default to memory instead if you'd prefer to avoid filesystem side effects — just let me know which you'd like.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a log::warn! on the unconfigured fallback per your suggestion (pushed in 1468565) so it's visible in logs. PTAL!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. I understand the problem much better.

If the target exited 1 / panicked when unconfigured, OSS-Fuzz would just record every run as an immediate crash and we'd still get no useful coverage — the same dead state we're fixing, only louder

Indeed true. Having a silent fallback to fs works to bring back fuzz coverage. This would be a good fix.
Now considering a maintenance perspective, if Ok(None) panics, any maintainers would be able to tell something requires for a fix. It might be stringent to understand fuzz setup but that would help someone else to jump in.
Nevertheless, I would be happy to see we are getting fuzz coverage. So your call.

uuid = { workspace = true, features = ["v4"] }

[[bin]]
name = "fuzz_from_uri"
path = "fuzz_from_uri.rs"
test = false

[[bin]]
name = "fuzz_path"
path = "fuzz_path.rs"
test = false

[[bin]]
name = "fuzz_reader"
path = "fuzz_reader.rs"
Expand Down
37 changes: 37 additions & 0 deletions core/fuzz/fuzz_from_uri.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

#![no_main]

use libfuzzer_sys::arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use opendal::Operator;
use opendal::OperatorUri;

#[derive(Debug, Clone, Arbitrary)]
struct FuzzInput {
uri: String,
options: Vec<(String, String)>,
}

fuzz_target!(|input: FuzzInput| {
let _ = logforth::starter_log::stderr().try_apply();

let _ = OperatorUri::new(&input.uri, input.options.clone());
let _ = Operator::from_uri(input.uri.as_str());
let _ = Operator::via_iter(&input.uri, input.options);
});
65 changes: 65 additions & 0 deletions core/fuzz/fuzz_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

#![no_main]

use std::sync::LazyLock;

use libfuzzer_sys::arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use opendal::Operator;
use opendal::tests::TEST_RUNTIME;
use opendal::tests::init_test_service;

#[derive(Debug, Clone, Arbitrary)]
struct FuzzInput {
path: String,
target: String,
}

static OPERATOR: LazyLock<Operator> = LazyLock::new(|| {
if let Some(op) = init_test_service().expect("operator init must succeed") {
return op;
}

log::warn!("OPENDAL_TEST is not set; falling back to a temporary fs operator");
let root = std::env::temp_dir().join(format!("opendal-fuzz-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).expect("create fuzz root dir must succeed");
Operator::new(opendal::services::Fs::default().root(&root.to_string_lossy()))
.expect("operator init must succeed")
.finish()
});

async fn fuzz_path(op: Operator, input: FuzzInput) {
let _ = op.write(&input.path, "data".as_bytes()).await;
let _ = op.stat(&input.path).await;
let _ = op.list(&input.path).await;
let _ = op.create_dir(&input.path).await;
let _ = op.copy(&input.path, &input.target).await;
let _ = op.rename(&input.path, &input.target).await;
let _ = op.delete(&input.target).await;
let _ = op.delete(&input.path).await;
}

fuzz_target!(|input: FuzzInput| {
let _ = logforth::starter_log::stderr().try_apply();

let op = OPERATOR.clone();
TEST_RUNTIME.block_on(async {
fuzz_path(op, input.clone()).await;
})
});
28 changes: 20 additions & 8 deletions core/fuzz/fuzz_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use std::fmt::Debug;
use std::fmt::Formatter;
use std::sync::LazyLock;

use libfuzzer_sys::arbitrary::Arbitrary;
use libfuzzer_sys::arbitrary::Unstructured;
Expand Down Expand Up @@ -88,15 +89,26 @@ async fn fuzz_reader(op: Operator, input: FuzzInput) -> Result<()> {
Ok(())
}

static OPERATOR: LazyLock<Operator> = LazyLock::new(|| {
if let Some(op) = init_test_service().expect("operator init must succeed") {
return op;
}

log::warn!("OPENDAL_TEST is not set; falling back to a temporary fs operator");
let root = std::env::temp_dir().join(format!("opendal-fuzz-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).expect("create fuzz root dir must succeed");
Operator::new(opendal::services::Fs::default().root(&root.to_string_lossy()))
.expect("operator init must succeed")
.finish()
});

fuzz_target!(|input: FuzzInput| {
let _ = logforth::starter_log::stderr().try_apply();

let op = init_test_service().expect("operator init must succeed");
if let Some(op) = op {
TEST_RUNTIME.block_on(async {
fuzz_reader(op, input.clone())
.await
.unwrap_or_else(|err| panic!("fuzz reader must succeed: {err:?}"));
})
}
let op = OPERATOR.clone();
TEST_RUNTIME.block_on(async {
fuzz_reader(op, input.clone())
.await
.unwrap_or_else(|err| panic!("fuzz reader must succeed: {err:?}"));
})
});
37 changes: 25 additions & 12 deletions core/fuzz/fuzz_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#![no_main]

use std::sync::LazyLock;

use libfuzzer_sys::arbitrary::Arbitrary;
use libfuzzer_sys::arbitrary::Unstructured;
use libfuzzer_sys::fuzz_target;
Expand Down Expand Up @@ -104,20 +106,31 @@ async fn fuzz_writer(op: Operator, input: FuzzInput) -> Result<()> {
Ok(())
}

static OPERATOR: LazyLock<Operator> = LazyLock::new(|| {
if let Some(op) = init_test_service().expect("operator init must succeed") {
return op;
}

log::warn!("OPENDAL_TEST is not set; falling back to a temporary fs operator");
let root = std::env::temp_dir().join(format!("opendal-fuzz-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).expect("create fuzz root dir must succeed");
Operator::new(opendal::services::Fs::default().root(&root.to_string_lossy()))
.expect("operator init must succeed")
.finish()
});

fuzz_target!(|input: FuzzInput| {
let _ = logforth::starter_log::stderr().try_apply();

let op = init_test_service().expect("operator init must succeed");
if let Some(op) = op {
if !op.info().full_capability().write_can_multi {
log::warn!("service doesn't support write multi, skip fuzzing");
return;
}

TEST_RUNTIME.block_on(async {
fuzz_writer(op, input.clone())
.await
.unwrap_or_else(|err| panic!("fuzz reader must succeed: {err:?}"));
})
let op = OPERATOR.clone();
if !op.info().full_capability().write_can_multi {
log::warn!("service doesn't support write multi, skip fuzzing");
return;
}

TEST_RUNTIME.block_on(async {
fuzz_writer(op, input.clone())
.await
.unwrap_or_else(|err| panic!("fuzz writer must succeed: {err:?}"));
})
});
Loading