diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..7eaff139 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tar-fuzz" +version = "0.0.0" +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = "1.3.2" +cap-std = "3.4.0" +derive_arbitrary = "1.3.2" +libfuzzer-sys = "0.4" +tempfile = "3.3" + +[dependencies.tar] +path = ".." + +[[bin]] +name = "archive" +path = "fuzz_targets/archive.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "tar" +path = "fuzz_targets/tar.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/archive.rs b/fuzz/fuzz_targets/archive.rs new file mode 100644 index 00000000..79c26aee --- /dev/null +++ b/fuzz/fuzz_targets/archive.rs @@ -0,0 +1,124 @@ +// Copyright 2024 Google LLC +// +// 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. + +#![no_main] + +use arbitrary::{Arbitrary, Unstructured}; +use cap_std::fs::Dir; +use cap_std::ambient_authority; +use derive_arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use std::io::{Cursor, Write}; +use tar::{Archive, Builder, EntryType, Header}; +use tempfile::tempdir; + +// Define ArchiveEntry for arbitrary crate +#[derive(Debug, Arbitrary)] +struct ArchiveEntry { + path: String, + entry_type: u8, + content: Vec, +} + +// Define FuzzInput for arbitrary crate +#[derive(Debug, Arbitrary)] +struct FuzzInput { + entries: Vec, +} + +fuzz_target!(|data: &[u8]| { + // Prepare FuzzInput with Arbitrary + let mut unstructured = Unstructured::new(data); + let input: FuzzInput = match FuzzInput::arbitrary(&mut unstructured) { + Ok(val) => val, + Err(_) => return, + }; + + // Create a sandbox directory with cap_std + let temp_dir = match tempdir() { + Ok(dir) => dir, + Err(_) => return, + }; + let sandbox_dir = match Dir::open_ambient_dir(temp_dir.path(), ambient_authority()) { + Ok(dir) => dir, + Err(_) => return, + }; + let temp_file_path = "archive_file.tar"; + let mut builder = Builder::new(Vec::new()); + + // Iterate through the archive entries to build a tar structure + for entry in &input.entries { + let mut header = Header::new_gnu(); + + // Ensure content size is reasonable to avoid potential overflow issues + let file_size = entry.content.len() as u64; + if file_size > u32::MAX as u64 { + continue; + } + header.set_size(file_size); + + // Determine the entry type from fuzzed data + let entry_type = match entry.entry_type % 5 { + 0 => EntryType::Regular, + 1 => EntryType::Directory, + 2 => EntryType::Symlink, + 3 => EntryType::hard_link(), + _ => EntryType::character_special(), + }; + header.set_entry_type(entry_type); + + // Process entry types using cap_std sandbox + match entry_type { + EntryType::Directory => { + if let Err(_) = sandbox_dir.create_dir_all(&entry.path) { + continue; + } + if builder.append_dir(&entry.path, &entry.path).is_err() { + continue; + } + } + EntryType::Regular => { + let mut cursor = Cursor::new(entry.content.clone()); + if builder.append_data(&mut header, entry.path.as_str(), &mut cursor).is_err() { + continue; + } + } + _ => { + // Handle other types with appropriate mock content or skip unsupported + let mut cursor = Cursor::new(entry.content.clone()); + if builder.append_data(&mut header, entry.path.as_str(), &mut cursor).is_err() { + continue; + } + } + } + } + + // Write the builder content to the temporary tar file within the sandbox + if let Ok(mut temp_file) = sandbox_dir.create(temp_file_path) { + if temp_file.write_all(&builder.into_inner().unwrap_or_default()).is_ok() { + let mut archive = Archive::new(temp_file); + if let Ok(entries) = archive.entries() { + for entry in entries { + if entry.is_err() { + return; + } + } + } + } + } + + // Cleanup temp directory and sandbox directory + drop(sandbox_dir); + drop(temp_dir); +}); diff --git a/fuzz/fuzz_targets/tar.rs b/fuzz/fuzz_targets/tar.rs new file mode 100644 index 00000000..aa414169 --- /dev/null +++ b/fuzz/fuzz_targets/tar.rs @@ -0,0 +1,137 @@ +// Copyright 2024 Google LLC +// +// 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. + +#![no_main] + +use arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::fuzz_target; +use std::io::{Cursor, Read, Seek, Write}; +use tar::{Archive, Builder, EntryType, Header}; +use tempfile::{tempdir, NamedTempFile}; + +// Define FuzzInput for arbitrary crate +#[derive(Debug)] +struct FuzzInput { + data: Vec, + file_name: String, + link_path: String, + target_path: String, + entry_type: u8, + metadata_size: u64, +} + +// Implement Arbitrary for FuzzInput +impl<'a> Arbitrary<'a> for FuzzInput { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(FuzzInput { + data: u.arbitrary()?, + file_name: u.arbitrary::<&str>()?.to_string(), + link_path: u.arbitrary::<&str>()?.to_string(), + target_path: u.arbitrary::<&str>()?.to_string(), + entry_type: u.arbitrary()?, + metadata_size: u.int_in_range(0..=1000)?, + }) + } +} + +fuzz_target!(|data: &[u8]| { + // Prepare FuzzInput by Arbitrary crate + let mut unstructured = Unstructured::new(data); + let input: FuzzInput = match FuzzInput::arbitrary(&mut unstructured) { + Ok(val) => val, + Err(_) => return, + }; + + // Setup temporary directory and initialize builder + let temp_dir = match tempdir() { + Ok(dir) => dir, + Err(_) => return, + }; + let archive_data = Cursor::new(&input.data); + let mut builder = Builder::new(Cursor::new(Vec::new())); + let mut header = Header::new_gnu(); + + // Set random header metadata + header.set_size(input.metadata_size.min(input.data.len() as u64)); + header.set_cksum(); + let entry_type = match input.entry_type % 5 { + 0 => EntryType::Regular, + 1 => EntryType::Directory, + 2 => EntryType::Symlink, + 3 => EntryType::Link, + _ => EntryType::Fifo, + }; + header.set_entry_type(entry_type); + + // Append data + let _ = builder.append_data(&mut header, &input.file_name, archive_data); + if let Ok(mut temp_file) = NamedTempFile::new() { + let _ = temp_file.write_all(&input.data); + let _ = builder.append_file("fuzzed/file2", temp_file.as_file_mut()).ok(); + } + + #[cfg(unix)] + let _ = builder.append_link(&mut header, &input.link_path, &input.target_path).ok(); + let _ = builder.finish(); + + // Fuzzing Archive and Entry logic + let mut archive = Archive::new(Cursor::new(&input.data)); + if let Ok(mut entries) = archive.entries() { + while let Some(Ok(mut entry)) = entries.next() { + let _ = entry.path().map(|p| p.to_owned()); + let _ = entry.link_name().map(|l| l.map(|ln| ln.to_owned())); + let _ = entry.size(); + let _ = entry.header(); + let _ = entry.raw_header_position(); + let _ = entry.raw_file_position(); + + // Randomly choose entry actions based on entry type + match entry.header().entry_type() { + EntryType::Regular => { /* Do nothing */ } + EntryType::Directory | EntryType::Symlink | EntryType::Link => { + let _ = entry.unpack_in(temp_dir.path()).ok(); + } + EntryType::Fifo => { /* Do nothing */ } + _ => { /* Do nothing */ } + } + + // Randomly read contents and adjust permissions and attributes + let mut buffer = Vec::new(); + let _ = entry.read_to_end(&mut buffer).ok(); + entry.set_mask(0o755); + entry.set_unpack_xattrs(true); + entry.set_preserve_permissions(true); + entry.set_preserve_mtime(true); + + // Fuzz unpack to randomized destination path + let dst_path = temp_dir.path().join(&input.file_name); + let _ = entry.unpack(&dst_path).ok(); + let _ = entry.unpack_in(temp_dir.path()).ok(); + + // Fuzz PaxExtensions + if let Ok(Some(pax_extensions)) = entry.pax_extensions() { + for ext in pax_extensions { + let _ = ext.ok(); + } + } + + // Randomized file search with tar entry position + if entry.size() > 0 { + let mut data_cursor = Cursor::new(&input.data); + let _ = data_cursor.seek(std::io::SeekFrom::Start(entry.raw_file_position())).ok(); + let _ = data_cursor.read(&mut buffer).ok(); + } + } + } +});