Skip to content

Commit

Permalink
Fuzz hashing collections with interior mutability
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Jun 4, 2024
1 parent 0a723ab commit 1d87737
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 3 deletions.
23 changes: 22 additions & 1 deletion crates/test-fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ gnustep-2-1 = ["gnustep-2-0", "objc2-foundation/gnustep-2-1"]
afl = ["dep:afl"]

# The features required for fuzzing all targets (used by CI)
fuzz-all = ["objc2-foundation/NSString"]
fuzz-all = [
"objc2-foundation/NSDictionary",
"objc2-foundation/NSEnumerator",
"objc2-foundation/NSObject",
"objc2-foundation/NSSet",
"objc2-foundation/NSString",
"objc2-foundation/NSZone",
]

[[bin]]
name = "class"
Expand Down Expand Up @@ -59,5 +66,19 @@ doc = false
bench = false
required-features = ["objc2-foundation/NSString"]

[[bin]]
name = "collection_interior_mut"
path = "fuzz_targets/collection_interior_mut.rs"
test = false
doc = false
bench = false
required-features = [
"objc2-foundation/NSDictionary",
"objc2-foundation/NSEnumerator",
"objc2-foundation/NSObject",
"objc2-foundation/NSSet",
"objc2-foundation/NSZone",
]

[package.metadata.release]
release = false
2 changes: 0 additions & 2 deletions crates/test-fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ Fuzz with AFL++ by doing:
(This is probably not the optimal settings, AFL has a lot of configuration options).

```sh
mkdir crates/test-fuzz/corpus/$fuzz_target
mkdir -p crates/test-fuzz/afl
cargo afl build --bin $fuzz_target --features=afl,fuzz-all --release
cargo afl fuzz -i crates/test-fuzz/corpus/$fuzz_target -o crates/test-fuzz/afl -- target/release/$fuzz_target
```
Empty file.
158 changes: 158 additions & 0 deletions crates/test-fuzz/fuzz_targets/collection_interior_mut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! Fuzz hashing collection operations with interior mutability.
//!
//! This is explicitly not done with any form of oracle, since while this is
//! not language-level undefined behaviour, the behaviour is not specified.
#![cfg_attr(not(feature = "afl"), no_main)]
use std::cell::Cell;
use std::hint::black_box;

use arbitrary::Arbitrary;
use objc2::rc::{autoreleasepool, Id, Retained};
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_foundation::{
NSCopying, NSMutableDictionary, NSMutableSet, NSObject, NSUInteger, NSZone,
};

/// Index into the global "keys" array.
type KeyIndex = u8;

/// The operations that the fuzzer can do on a set and the keys within.
#[derive(Arbitrary, Debug)]
enum Operation {
/// count
Count,
/// member: / objectForKey:
Get(KeyIndex),
/// objectEnumerator / keyEnumerator
Enumerate,
/// addObject: / setObject:forKey:
Add(KeyIndex),
/// removeObject: / removeObjectForKey:
Remove(KeyIndex),

/// Set the hash value of a key.
SetHash(KeyIndex, NSUInteger),
/// Set which other key masks this key is equal to.
SetEqualToMask(KeyIndex, u8),
}

struct KeyIvars {
index: KeyIndex,
hash: Cell<usize>,
equal_to_mask: Cell<u8>,
}

declare_class!(
struct Key;

unsafe impl ClassType for Key {
type Super = NSObject;
// Intentionally `Immutable` to see what breaks if we allow mutation.
type Mutability = mutability::Immutable;
const NAME: &'static str = "Key";
}

impl DeclaredClass for Key {
type Ivars = KeyIvars;
}

unsafe impl Key {
#[method(isEqual:)]
fn is_equal(&self, other: &AnyObject) -> bool {
assert_eq!(other.class(), Self::class());
let other: *const AnyObject = other;
let other: *const Self = other.cast();
// SAFETY: Just checked that the object is of this class
let other: &Self = unsafe { &*other };

(other.ivars().index & self.ivars().equal_to_mask.get()) != 0
}

#[method(hash)]
fn hash_(&self) -> NSUInteger {
self.ivars().hash.get()
}
}

unsafe impl NSCopying for Key {
#[method_id(copyWithZone:)]
fn copy_with_zone(&self, _zone: *mut NSZone) -> Retained<Self> {
self.retain()
}
}
);

impl Key {
fn new(index: KeyIndex) -> Retained<Self> {
let key = Key::alloc().set_ivars(KeyIvars {
index,
hash: Cell::new(0),
equal_to_mask: Cell::new(0),
});
unsafe { msg_send_id![super(key), init] }
}

fn validate(&self) {
black_box(self.ivars().index);
black_box(self.ivars().hash.get());
}
}

fn run(ops: Vec<Operation>) {
let keys: Vec<_> = (0..=KeyIndex::MAX).map(Key::new).collect();
let key = |idx: KeyIndex| -> &Key { &keys[idx as usize] };

let mut set: Id<NSMutableSet<Key>> = NSMutableSet::new();
let mut dict: Id<NSMutableDictionary<Key, NSObject>> = NSMutableDictionary::new();

for op in ops {
autoreleasepool(|_| match op {
Operation::Count => {
set.count();
dict.count();
}
Operation::Get(idx) => {
unsafe { set.member(key(idx)) };
unsafe { dict.objectForKey(key(idx)) };
}
Operation::Enumerate => {
for key in unsafe { set.objectEnumerator() } {
key.validate();
}
for key in &set {
key.validate();
}
for key in unsafe { dict.keyEnumerator() } {
key.validate();
}
}
Operation::Add(idx) => {
unsafe { set.addObject(key(idx)) };
unsafe {
dict.setObject_forKey(&NSObject::new(), ProtocolObject::from_ref(key(idx)))
};
}
Operation::Remove(idx) => {
unsafe { set.removeObject(key(idx)) };
dict.removeObjectForKey(key(idx));
}
Operation::SetHash(idx, hash) => {
key(idx).ivars().hash.set(hash);
}
Operation::SetEqualToMask(idx, equal_to_mask) => {
key(idx).ivars().equal_to_mask.set(equal_to_mask);
}
});
}
}

#[cfg(not(feature = "afl"))]
libfuzzer_sys::fuzz_target!(|ops: Vec<Operation>| run(ops));

#[cfg(feature = "afl")]
fn main() {
afl::fuzz!(|ops: Vec<Operation>| {
run(ops);
});
}

0 comments on commit 1d87737

Please sign in to comment.