Skip to content
Open
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
106 changes: 106 additions & 0 deletions crates/confuzzled_lib/src/mutations/extreme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,112 @@ impl Mutation for EmptyEverything {
}
}

/// Empty layers with null diff_ids
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The documentation comment says 'null diff_ids' but with the skip_serializing_if attribute on the field, setting diff_ids to None will omit the field from serialization rather than serialize it as null. Update the comment to reflect that the field will be 'omitted' rather than 'null'.

Copilot uses AI. Check for mistakes.
#[derive(Debug)]
pub struct EmptyLayersNullDiffIds;

impl Mutation for EmptyLayersNullDiffIds {
fn name(&self) -> &'static str {
"empty-layers-null-diffids"
}
fn category(&self) -> MutationCategory {
MutationCategory::Extreme
}

fn apply(&self, mut image: Image, _rng: &mut StdRng) -> Result<Image> {
// Clear all layers from manifest
image.layers.clear();

// Set diff_ids to None (serializes as null or omitted)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The comment says 'serializes as null or omitted' but should be more precise. With skip_serializing_if = \"Option::is_none\", it will be omitted, not serialized as null. Either update the comment to say 'omitted' or clarify the actual behavior intended.

Suggested change
// Set diff_ids to None (serializes as null or omitted)
// Set diff_ids to None (will be omitted from serialization if using `skip_serializing_if = "Option::is_none"`)

Copilot uses AI. Check for mistakes.
if let Some(ref mut config) = image.config {
config.rootfs.diff_ids = None;
}

Ok(image)
}
}

/// Null diff_ids with layers intact
///
/// Sets diff_ids to null while keeping the actual layers.
Comment on lines +144 to +146
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The documentation says 'Sets diff_ids to null' but with the skip_serializing_if attribute, setting to None will omit the field rather than serialize it as null. Update the documentation to accurately describe the behavior as 'omits diff_ids' or 'removes diff_ids field'.

Copilot uses AI. Check for mistakes.
/// This creates a mismatch where layers exist but config claims none.
#[derive(Debug)]
pub struct NullDiffIdsOnly;

impl Mutation for NullDiffIdsOnly {
fn name(&self) -> &'static str {
"null-diffids-only"
}
fn category(&self) -> MutationCategory {
MutationCategory::Extreme
}

fn apply(&self, mut image: Image, _rng: &mut StdRng) -> Result<Image> {
// Keep layers but null out diff_ids
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The comment says 'null out diff_ids' but the field will be omitted during serialization due to skip_serializing_if. Consider changing to 'omit diff_ids' or 'remove diff_ids' for accuracy.

Copilot uses AI. Check for mistakes.
if let Some(ref mut config) = image.config {
config.rootfs.diff_ids = None;
}
Ok(image)
}
}

/// Empty diff_ids array with layers intact
///
/// Sets diff_ids to an empty array while keeping actual layers.
/// This creates a count mismatch (N layers, 0 diff_ids).
#[derive(Debug)]
pub struct EmptyDiffIds;

impl Mutation for EmptyDiffIds {
fn name(&self) -> &'static str {
"empty-diffids"
}
fn category(&self) -> MutationCategory {
MutationCategory::Extreme
}

fn apply(&self, mut image: Image, _rng: &mut StdRng) -> Result<Image> {
if let Some(ref mut config) = image.config {
config.rootfs.diff_ids = Some(vec![]);
}
Ok(image)
}
}

/// Mismatched diff_ids count
///
/// Adds extra fake diff_ids or removes some, creating a count mismatch
/// between manifest layers and config diff_ids.
#[derive(Debug)]
pub struct MismatchedDiffIdsCount;

impl Mutation for MismatchedDiffIdsCount {
fn name(&self) -> &'static str {
"mismatched-diffids-count"
}
fn category(&self) -> MutationCategory {
MutationCategory::Extreme
}

fn apply(&self, mut image: Image, rng: &mut StdRng) -> Result<Image> {
if let Some(ref mut config) = image.config {
if let Some(ref mut diff_ids) = config.rootfs.diff_ids {
// Randomly add extra or remove entries
if rng.gen_bool(0.5) && !diff_ids.is_empty() {
// Remove half of the diff_ids
diff_ids.truncate(diff_ids.len() / 2);
} else {
// Add fake diff_ids
for _ in 0..rng.gen_range(1..5) {
diff_ids.push(format!("sha256:{:064x}", rng.gen::<u128>()));
}
}
}
}
Ok(image)
}
}

/// Create extremely long strings
#[derive(Debug)]
pub struct HugeStrings;
Expand Down
4 changes: 4 additions & 0 deletions crates/confuzzled_lib/src/mutations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ impl MutationSet {
Box::new(DeepPathNesting),
Box::new(ExtremeValues),
Box::new(EmptyEverything),
Box::new(EmptyLayersNullDiffIds),
Box::new(NullDiffIdsOnly),
Box::new(EmptyDiffIds),
Box::new(MismatchedDiffIdsCount),
];
self.pick_random(all, rng)
}
Expand Down
9 changes: 6 additions & 3 deletions crates/confuzzled_lib/src/oci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,10 @@ impl ImageConfig {
rootfs: RootFs {
// Empty tar has this digest (uncompressed)
// The hash below is for empty content
diff_ids: vec![
diff_ids: Some(vec![
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
.to_string(),
],
]),
rootfs_type: "layers".to_string(),
},
history: None,
Expand Down Expand Up @@ -389,7 +389,10 @@ pub struct RootFs {
#[serde(rename = "type")]
pub rootfs_type: String,

pub diff_ids: Vec<String>,
/// Layer diff IDs. When None, serializes as null which can trigger
/// edge cases in container runtimes that expect this field.
#[serde(skip_serializing_if = "Option::is_none")]
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The documentation states 'serializes as null' but with skip_serializing_if = \"Option::is_none\", a None value will be omitted from serialization entirely, not serialized as null. The comment should clarify this behavior or the attribute should be changed to serialize as explicit null if that's the intended behavior.

Suggested change
#[serde(skip_serializing_if = "Option::is_none")]

Copilot uses AI. Check for mistakes.
pub diff_ids: Option<Vec<String>>,
}

/// Image history entry
Expand Down