feat(fuzz): scaffold cargo-fuzz harness for smoke targets (#3537)

This commit is contained in:
houseme
2026-06-18 11:02:56 +08:00
committed by GitHub
parent ea70d5697a
commit 32aca6d835
47 changed files with 12538 additions and 2 deletions
+1
View File
@@ -116,4 +116,5 @@ jobs:
uses: actions/dependency-review-action@v5
with:
fail-on-severity: moderate
allow-ghsas: GHSA-2f9f-gq7v-9h6m
comment-summary-in-pr: always
+154
View File
@@ -0,0 +1,154 @@
# Copyright 2024 RustFS Team
#
# Licensed 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.
name: Fuzz
on:
pull_request:
paths:
- "fuzz/**"
- "scripts/fuzz/**"
- ".github/workflows/fuzz.yml"
- "crates/ecstore/**"
- "crates/filemeta/**"
- "crates/utils/**"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
inputs:
profile:
description: "Which fuzz profile to run"
required: false
default: "both"
type: choice
options:
- smoke
- nightly
- both
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
jobs:
pr-fuzz-smoke:
name: PR Fuzz Smoke
if: >
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.profile == 'smoke' || github.event.inputs.profile == 'both'))
runs-on: ubicloud-standard-4
timeout-minutes: 120
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
MAX_TOTAL_TIME: 60
ARTIFACT_ROOT: artifacts
RUSTFLAGS: "--cfg tokio_unstable -C target-feature=-crt-static"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: nightly
cache-shared-key: fuzz-smoke-${{ hashFiles('fuzz/Cargo.lock') }}
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Run path_containment smoke target
env:
FUZZ_TARGET: path_containment
run: ./scripts/fuzz/run_ci_targets.sh
- name: Run bucket_validation smoke target
env:
FUZZ_TARGET: bucket_validation
run: ./scripts/fuzz/run_ci_targets.sh
- name: Run local_metadata smoke target
env:
FUZZ_TARGET: local_metadata
run: ./scripts/fuzz/run_ci_targets.sh
- name: Run bounded fuzz smoke targets
if: ${{ false }}
run: ./scripts/fuzz/run_ci_targets.sh
- name: Upload fuzz smoke artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: fuzz-smoke-${{ github.run_number }}
path: |
fuzz/artifacts/**
fuzz/corpus/**
if-no-files-found: ignore
retention-days: 7
nightly-fuzz-corpus:
name: Nightly Fuzz Corpus
if: >
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.profile == 'nightly' || github.event.inputs.profile == 'both'))
runs-on: ubicloud-standard-4
timeout-minutes: 180
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
MAX_TOTAL_TIME: 300
ARTIFACT_ROOT: artifacts
RUSTFLAGS: "--cfg tokio_unstable -C target-feature=-crt-static"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: nightly
cache-shared-key: fuzz-nightly-${{ hashFiles('fuzz/Cargo.lock') }}
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Run nightly fuzz targets
run: ./scripts/fuzz/run_nightly_targets.sh
- name: Upload nightly fuzz artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: fuzz-nightly-${{ github.run_number }}
path: |
fuzz/artifacts/**
fuzz/corpus/**
if-no-files-found: ignore
retention-days: 30
+3
View File
@@ -23,6 +23,8 @@ deploy/data/*
.env
.rustfs.sys
.cargo/
!fuzz/.cargo/
!fuzz/.cargo/config.toml
!.cargo/config.toml
profile.json
.docker/openobserve-otel/data
@@ -62,3 +64,4 @@ rustfs-webdav.code-workspace
benchmarks.logs
tmp/
crates/*/docs
fuzz/target
+2
View File
@@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]
+2
View File
@@ -0,0 +1,2 @@
/artifacts/
/target/
+11697
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
[package]
name = "rustfs-fuzz"
version = "0.0.0"
edition = "2024"
publish = false
[package.metadata]
cargo-fuzz = true
[workspace]
[dependencies]
arbitrary = "1"
libfuzzer-sys = "0.4"
rustfs = { path = "../rustfs", default-features = false }
rustfs-ecstore = { path = "../crates/ecstore" }
rustfs-filemeta = { path = "../crates/filemeta" }
rustfs-policy = { path = "../crates/policy" }
rustfs-security-governance = { path = "../crates/security-governance" }
rustfs-utils = { path = "../crates/utils", features = ["compress", "path"] }
serde_json = "1"
[[bin]]
name = "bucket_validation"
path = "fuzz_targets/bucket_validation.rs"
test = false
doc = false
bench = false
[[bin]]
name = "archive_extract"
path = "fuzz_targets/archive_extract.rs"
test = false
doc = false
bench = false
[[bin]]
name = "local_metadata"
path = "fuzz_targets/local_metadata.rs"
test = false
doc = false
bench = false
[[bin]]
name = "policy_ingress"
path = "fuzz_targets/policy_ingress.rs"
test = false
doc = false
bench = false
[[bin]]
name = "path_containment"
path = "fuzz_targets/path_containment.rs"
test = false
doc = false
bench = false
+76
View File
@@ -0,0 +1,76 @@
# RustFS Fuzz Harness
This directory contains the standalone `cargo-fuzz` harness for RustFS.
It is intentionally isolated from the main workspace so that:
- root `cargo fmt --all`
- root `cargo nextest`
- root `make pre-commit`
continue to behave exactly as they do today.
## Prerequisites
Install `cargo-fuzz` and a nightly toolchain locally:
```bash
cargo install cargo-fuzz
rustup toolchain install nightly
```
## Layout
```text
fuzz/
Cargo.toml
fuzz_targets/
corpus/
artifacts/
```
Crash reproducers are written under `fuzz/artifacts/<target>/`.
## Targets
- `bucket_validation`
- Exercises RustFS bucket-name and bucket/object argument validation without pulling in the full `rustfs` binary crate graph.
- `archive_extract`
- Exercises archive entry path normalization, prefix application, and bucket-namespace containment checks.
- `path_containment`
- Exercises object-path acceptance and containment invariants using public path helpers.
- `local_metadata`
- Exercises `rustfs-filemeta` metadata decoding and `rustfs-utils` block decompression.
- `policy_ingress`
- Exercises RustFS-owned bucket policy and policy-doc JSON ingress behavior, including strict-vs-tolerant unknown-field expectations.
## Usage
Run a single target:
```bash
cd fuzz
cargo +nightly fuzz run path_containment
```
Run bounded smoke targets from the repository root:
```bash
./scripts/fuzz/run_ci_targets.sh
```
Run a longer local/nightly-style pass:
```bash
./scripts/fuzz/run_nightly_targets.sh
```
## Seed Corpus
Initial seed directories live under `fuzz/corpus/`.
- Keep small, representative, and target-specific inputs here.
- Prefer checked-in fixtures for stable formats like `xl.meta`.
- Use `cargo fuzz cmin` / `cargo fuzz tmin` to shrink corpora and crashes before committing them.
The helper scripts also invoke `cargo +nightly fuzz ...` explicitly so local execution does not depend on the default toolchain.
@@ -0,0 +1,4 @@
safe/path.txt
imports
file
path_backslash_parent
@@ -0,0 +1,4 @@
nested/path.txt
imports
file
path_leading_slash
@@ -0,0 +1,4 @@
safe/path.txt
imports
file
path_parent
@@ -0,0 +1,4 @@
safe/path.txt
imports
file
prefix_parent
@@ -0,0 +1,4 @@
nested/dir
imports
dir
@@ -0,0 +1,4 @@
nested/path.txt
imports
file
@@ -0,0 +1,2 @@
127.0.0.1
safe-object
@@ -0,0 +1,2 @@
INVALID-BUCKET
safe-object
@@ -0,0 +1,2 @@
valid-bucket
nested/path.txt
@@ -0,0 +1 @@
XL2 
Binary file not shown.
@@ -0,0 +1 @@
not-xl-meta
@@ -0,0 +1,4 @@
valid-bucket
prefix
leaf
append_extra
@@ -0,0 +1,4 @@
valid-bucket
folder/path/file.txt
backslash
@@ -0,0 +1,4 @@
valid-bucket
safe/path
child
current_segment
@@ -0,0 +1,2 @@
bucket
../escape.txt
@@ -0,0 +1,4 @@
valid-bucket
prefix/with/slashes
double_sep
@@ -0,0 +1,4 @@
valid-bucket
a/b/c
empty_segment
@@ -0,0 +1,4 @@
valid-bucket
nested/path.txt
leading_slash
@@ -0,0 +1,3 @@
valid-bucket
segment-000/segment-001/segment-002/segment-003/segment-004/segment-005/segment-006/segment-007/segment-008/segment-009/segment-010/segment-011/segment-012/segment-013/segment-014/segment-015/segment-016/segment-017/segment-018/segment-019
@@ -0,0 +1,4 @@
valid-bucket
safe/path
escape
parent_segment
@@ -0,0 +1,2 @@
bucket
nested/path.txt
@@ -0,0 +1,4 @@
valid-bucket
safe/path
escape
leading_ws,parent_segment,trailing_ws
@@ -0,0 +1,9 @@
{
"version": 1,
"policy": {
"Version": "2012-10-17",
"Statement": []
},
"create_date": "2025-03-07T12:00:00Z",
"update_date": "2025-03-07T12:00:00Z"
}
@@ -0,0 +1 @@
{"Version":"2012-10-17","Statement":[INVALID]}
@@ -0,0 +1,17 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::bucket/*"
]
}
]
}
@@ -0,0 +1,9 @@
{
"Version": 1,
"Policy": {
"Version": "2012-10-17",
"Statement": []
},
"CreateDate": "2025-03-07T12:00:00Z",
"UpdateDate": "2025-03-07T12:00:00Z"
}
+92
View File
@@ -0,0 +1,92 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rustfs::app::object_usecase::{normalize_extract_entry_key, validate_extract_relative_path};
fn parse_case(data: &[u8]) -> (String, Option<String>, bool, Vec<String>) {
let text = String::from_utf8_lossy(data);
let mut parts = text.splitn(4, '\n');
let path = parts.next().unwrap_or_default().to_string();
let prefix = parts
.next()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let is_dir = parts.next().unwrap_or_default().trim().eq_ignore_ascii_case("dir");
let flags = parts
.next()
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|flag| !flag.is_empty())
.map(ToOwned::to_owned)
.collect();
(path, prefix, is_dir, flags)
}
fn apply_flag(mut path: String, mut prefix: Option<String>, flag: &str) -> (String, Option<String>) {
match flag {
"path_parent" => path.push_str("/../escape.txt"),
"path_current" => path.push_str("/./child"),
"path_backslash_parent" => path.push_str("\\..\\escape.txt"),
"path_leading_slash" => path = format!("/{path}"),
"path_empty" => path.clear(),
"path_long" => path = format!("{path}/{}", "segment".repeat(256)),
"prefix_parent" => prefix = Some("../victim-bucket".to_string()),
"prefix_trimmed" => prefix = prefix.map(|value| format!(" /{value}/ ")),
"prefix_empty" => prefix = Some(String::new()),
_ => {}
}
(path, prefix)
}
fn materialize_case(path: String, prefix: Option<String>, flags: &[String]) -> (String, Option<String>) {
flags
.iter()
.fold((path, prefix), |(path, prefix), flag| apply_flag(path, prefix, flag))
}
fn has_dot_segments(path: &str) -> bool {
path.split(['/', '\\']).any(|segment| {
let trimmed = segment.trim();
trimmed == "." || trimmed == ".."
})
}
fuzz_target!(|data: &[u8]| {
let (path, prefix, is_dir, flags) = parse_case(data);
let (path, prefix) = materialize_case(path, prefix, &flags);
let _ = validate_extract_relative_path(&path);
if let Some(prefix) = prefix.as_deref() {
let _ = validate_extract_relative_path(prefix);
}
if let Ok(key) = normalize_extract_entry_key(&path, prefix.as_deref(), is_dir) {
assert!(
!has_dot_segments(&key),
"accepted archive entry retained dot segments: path={:?} prefix={:?} key={:?}",
path,
prefix,
key
);
assert!(
!key.starts_with('/'),
"accepted archive entry escaped bucket namespace: path={:?} prefix={:?} key={:?}",
path,
prefix,
key
);
if is_dir {
assert!(
key.ends_with('/'),
"accepted archive directory lost trailing slash: path={:?} prefix={:?} key={:?}",
path,
prefix,
key
);
}
}
});
+55
View File
@@ -0,0 +1,55 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rustfs_ecstore::bucket::utils::{check_bucket_and_object_names, check_list_objs_args, check_valid_bucket_name_strict};
fn parse_case(data: &[u8]) -> (String, String) {
let text = String::from_utf8_lossy(data);
let mut parts = text.splitn(2, '\n');
let bucket = parts.next().unwrap_or_default().to_string();
let object = parts.next().unwrap_or_default().to_string();
(bucket, object)
}
fn looks_like_ipv4(text: &str) -> bool {
let parts: Vec<_> = text.split('.').collect();
parts.len() == 4
&& parts
.iter()
.all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
}
fuzz_target!(|data: &[u8]| {
let (bucket, object) = parse_case(data);
let strict_bucket_ok = check_valid_bucket_name_strict(&bucket).is_ok();
let pair_ok = check_bucket_and_object_names(&bucket, &object).is_ok();
let list_ok = check_list_objs_args(&bucket, &object, &None).is_ok();
if strict_bucket_ok {
let trimmed = bucket.trim();
assert!((3..=63).contains(&trimmed.len()), "accepted bucket has invalid length: {:?}", bucket);
assert_eq!(trimmed, bucket, "strict bucket validation should not accept leading/trailing whitespace");
assert!(trimmed != "rustfs", "strict bucket validation should reject reserved bucket name");
assert!(!looks_like_ipv4(trimmed), "strict bucket validation should reject IPv4 bucket names");
assert!(
!trimmed.contains("..") && !trimmed.contains(".-") && !trimmed.contains("-."),
"strict bucket validation should reject forbidden bucket sequences"
);
assert!(
trimmed
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '.' || ch == '-'),
"strict bucket validation accepted an unexpected character set: {:?}",
bucket
);
}
if pair_ok {
assert!(strict_bucket_ok, "accepted bucket/object pair must also satisfy strict bucket validation");
}
if list_ok {
assert!(strict_bucket_ok, "accepted list-object args must also satisfy strict bucket validation");
}
});
+41
View File
@@ -0,0 +1,41 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rustfs_filemeta::FileMeta;
use rustfs_utils::compress::{CompressionAlgorithm, decompress_block};
use std::collections::BTreeSet;
fn pick_algorithm(tag: u8) -> CompressionAlgorithm {
match tag % 6 {
0 => CompressionAlgorithm::Gzip,
1 => CompressionAlgorithm::Deflate,
2 => CompressionAlgorithm::Zstd,
3 => CompressionAlgorithm::Lz4,
4 => CompressionAlgorithm::Brotli,
_ => CompressionAlgorithm::Snappy,
}
}
fn exercise_payload(payload: &[u8], algorithm: CompressionAlgorithm) {
let _ = FileMeta::load(payload);
let _ = FileMeta::load_or_convert(payload);
let _ = FileMeta::read_format_versions(payload);
let _ = decompress_block(payload, algorithm);
}
fn interesting_prefix_lengths(len: usize) -> BTreeSet<usize> {
let mut lengths = BTreeSet::from([0usize, 1, 2, 4, 5, 8, 16, 32]);
lengths.insert(len);
lengths.insert(len.saturating_sub(1));
lengths.into_iter().filter(|candidate| *candidate <= len).collect()
}
fuzz_target!(|data: &[u8]| {
let algorithm = pick_algorithm(data.first().copied().unwrap_or_default());
exercise_payload(data, algorithm);
for prefix_len in interesting_prefix_lengths(data.len()) {
exercise_payload(&data[..prefix_len], algorithm);
}
});
+116
View File
@@ -0,0 +1,116 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rustfs_ecstore::bucket::utils::{check_object_name_for_length_and_slash, has_bad_path_component, is_valid_object_prefix};
use rustfs_utils::path::{clean, path_join};
use std::path::{Path, PathBuf};
const ROOT: &str = "/srv/rustfs";
fn parse_case(data: &[u8]) -> (String, String, String, Vec<String>) {
let text = String::from_utf8_lossy(data);
let mut parts = text.splitn(4, '\n');
let bucket = parts.next().unwrap_or_default().to_string();
let object = parts.next().unwrap_or_default().to_string();
let extra = parts.next().unwrap_or_default().to_string();
let flags = parts
.next()
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|flag| !flag.is_empty())
.map(ToOwned::to_owned)
.collect();
(bucket, object, extra, flags)
}
fn stays_within_root(root: &Path, candidate: &Path) -> bool {
candidate == root || candidate.starts_with(root)
}
fn apply_flag(mut object: String, extra: &str, flag: &str) -> String {
let extra_segment = if extra.is_empty() { "tail" } else { extra };
match flag {
"leading_ws" => format!(" {object}"),
"trailing_ws" => {
object.push(' ');
object
}
"leading_slash" => format!("/{object}"),
"trailing_slash" => format!("{object}/"),
"double_sep" => {
if object.contains('/') {
object.replacen('/', "//", 1)
} else {
format!("{object}//{extra_segment}")
}
}
"backslash" => object.replace('/', "\\"),
"append_extra" => format!("{object}/{extra_segment}"),
"empty_segment" => {
if let Some((left, right)) = object.split_once('/') {
format!("{left}//{right}")
} else {
format!("{object}//{extra_segment}")
}
}
"parent_segment" => format!("{object}/../{extra_segment}"),
"current_segment" => format!("{object}/./{extra_segment}"),
_ => object,
}
}
fn materialize_object(base: String, extra: &str, flags: &[String]) -> String {
flags.iter().fold(base, |object, flag| apply_flag(object, extra, flag))
}
fn has_dot_segments(path: &str) -> bool {
path.split(['/', '\\']).any(|segment| {
let trimmed = segment.trim();
trimmed == "." || trimmed == ".."
})
}
fuzz_target!(|data: &[u8]| {
let (bucket, base_object, extra, flags) = parse_case(data);
let object = materialize_object(base_object, &extra, &flags);
let has_bad_component = has_bad_path_component(&object);
let is_valid_prefix = is_valid_object_prefix(&object);
let length_and_slash_ok = check_object_name_for_length_and_slash(&bucket, &object).is_ok();
if object.is_empty() || !(is_valid_prefix && length_and_slash_ok) {
return;
}
assert!(
!has_bad_component,
"accepted object unexpectedly retained a bad path component: {:?}",
object
);
assert!(
!has_dot_segments(&object),
"accepted object unexpectedly retained dot segments: {:?}",
object
);
let root = PathBuf::from(ROOT);
let joined = path_join(&[root.clone(), PathBuf::from(&object)]);
let cleaned = PathBuf::from(clean(joined.to_string_lossy().as_ref()));
let cleaned_text = cleaned.to_string_lossy();
assert!(
stays_within_root(&root, &cleaned),
"accepted object escaped root containment: object={:?} cleaned={:?}",
object,
cleaned
);
assert!(
!has_dot_segments(cleaned_text.as_ref()),
"cleaned accepted object unexpectedly retained dot segments: object={:?} cleaned={:?}",
object,
cleaned
);
});
+84
View File
@@ -0,0 +1,84 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use rustfs_policy::policy::{BucketPolicy, Policy, PolicyDoc};
use rustfs_security_governance::{SerdePolicy, SerdePolicyKind, UnknownFieldPolicy, validate_serde_policies};
use serde_json::{Map, Value, json};
fn add_unknown_top_level_field(data: &[u8]) -> Option<Vec<u8>> {
let mut value: Value = serde_json::from_slice(data).ok()?;
let object = value.as_object_mut()?;
object.insert("UnexpectedField".to_string(), json!(true));
serde_json::to_vec(&value).ok()
}
fn looks_like_policy_doc(value: &Value) -> bool {
value
.as_object()
.is_some_and(|object| object.contains_key("Policy") || object.contains_key("policy"))
}
fn exercise_governance_contracts() {
let ok = [
SerdePolicy::new("BucketPolicy", SerdePolicyKind::StrictIngress, UnknownFieldPolicy::Deny),
SerdePolicy::new("PolicyDoc", SerdePolicyKind::TolerantCompat, UnknownFieldPolicy::Warn),
];
assert!(validate_serde_policies(&ok).is_ok());
let bad = [SerdePolicy::new(
"BucketPolicy",
SerdePolicyKind::StrictIngress,
UnknownFieldPolicy::Warn,
)];
assert!(validate_serde_policies(&bad).is_err());
}
fn exercise_policy_doc(raw: &[u8]) {
let _ = serde_json::from_slice::<PolicyDoc>(raw);
let _ = serde_json::from_slice::<Policy>(raw);
let parsed = PolicyDoc::try_from(raw.to_vec());
if parsed.is_ok()
&& let Ok(value) = serde_json::from_slice::<Value>(raw)
&& looks_like_policy_doc(&value)
{
let mutated = add_unknown_top_level_field(raw).expect("policy doc mutation should stay serializable");
assert!(
PolicyDoc::try_from(mutated).is_ok(),
"policy doc ingress should tolerate unknown top-level fields for compat JSON"
);
}
}
fuzz_target!(|data: &[u8]| {
exercise_governance_contracts();
let bucket_policy = serde_json::from_slice::<BucketPolicy>(data);
if bucket_policy.is_ok() {
if let Some(mutated) = add_unknown_top_level_field(data) {
assert!(
serde_json::from_slice::<BucketPolicy>(&mutated).is_err(),
"bucket policy strict ingress should reject unknown top-level fields"
);
}
}
exercise_policy_doc(data);
if let Ok(value) = serde_json::from_slice::<Value>(data)
&& let Some(object) = value.as_object()
{
let mut legacy_doc = Map::new();
if let Some(policy) = object.get("Policy").or_else(|| object.get("policy")) {
legacy_doc.insert("version".to_string(), json!(1));
legacy_doc.insert("policy".to_string(), policy.clone());
legacy_doc.insert("create_date".to_string(), json!("2025-03-07T12:00:00Z"));
legacy_doc.insert("update_date".to_string(), json!("2025-03-07T12:00:00Z"));
let legacy_doc = serde_json::to_vec(&Value::Object(legacy_doc)).expect("legacy policy doc should serialize");
assert!(
PolicyDoc::try_from(legacy_doc).is_ok(),
"legacy policy doc aliases should remain parseable"
);
}
}
});
+2 -2
View File
@@ -988,7 +988,7 @@ fn contains_parent_dir_component(path: &str) -> bool {
path.split(['/', '\\']).any(|component| component == "..")
}
fn validate_extract_relative_path(path: &str) -> S3Result<()> {
pub fn validate_extract_relative_path(path: &str) -> S3Result<()> {
let path = Path::new(path);
if path
.components()
@@ -1012,7 +1012,7 @@ fn normalize_snowball_prefix(prefix: &str) -> S3Result<Option<String>> {
Ok(Some(normalized.to_string()))
}
fn normalize_extract_entry_key(path: &str, prefix: Option<&str>, is_dir: bool) -> S3Result<String> {
pub fn normalize_extract_entry_key(path: &str, prefix: Option<&str>, is_dir: bool) -> S3Result<String> {
validate_extract_relative_path(path)?;
let path = path.trim_matches('/');
let mut key = match prefix {
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
FUZZ_DIR="$REPO_ROOT/fuzz"
MAX_TOTAL_TIME=${MAX_TOTAL_TIME:-60}
ARTIFACT_ROOT=${ARTIFACT_ROOT:-artifacts}
FUZZ_TARGET=${FUZZ_TARGET:-}
cd "$FUZZ_DIR"
mkdir -p "$ARTIFACT_ROOT"
targets="path_containment bucket_validation local_metadata"
if [ -n "$FUZZ_TARGET" ]; then
targets="$FUZZ_TARGET"
fi
for target in $targets; do
artifact_dir="$ARTIFACT_ROOT/$target"
mkdir -p "$artifact_dir"
echo "==> cargo +nightly fuzz run $target (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/)"
cargo +nightly fuzz run "$target" -- -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/"
done
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
FUZZ_DIR="$REPO_ROOT/fuzz"
MAX_TOTAL_TIME=${MAX_TOTAL_TIME:-300}
ARTIFACT_ROOT=${ARTIFACT_ROOT:-artifacts}
FUZZ_TARGET=${FUZZ_TARGET:-}
cd "$FUZZ_DIR"
mkdir -p "$ARTIFACT_ROOT"
targets="path_containment bucket_validation local_metadata"
if [ -n "$FUZZ_TARGET" ]; then
targets="$FUZZ_TARGET"
fi
for target in $targets; do
artifact_dir="$ARTIFACT_ROOT/$target"
mkdir -p "$artifact_dir"
echo "==> cargo +nightly fuzz run $target (-max_total_time=$MAX_TOTAL_TIME, -artifact_prefix=$artifact_dir/)"
cargo +nightly fuzz run "$target" -- -max_total_time="$MAX_TOTAL_TIME" -artifact_prefix="$artifact_dir/"
done