Skip to content

check the layer media type before flashing#9

Open
bennyz wants to merge 2 commits intojumpstarter-dev:mainfrom
bennyz:fix-layer-selection
Open

check the layer media type before flashing#9
bennyz wants to merge 2 commits intojumpstarter-dev:mainfrom
bennyz:fix-layer-selection

Conversation

@bennyz
Copy link
Copy Markdown
Member

@bennyz bennyz commented Mar 18, 2026

To avoid flashing non disk layers we need to check the media type

To avoid flashing non disk layers we need to check the media type

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-sonnet-4.6
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40a5aa53-1af4-4130-bca7-0e33bbd21fef

📥 Commits

Reviewing files that changed from the base of the PR and between ab866a4 and 5dfbeb5.

📒 Files selected for processing (2)
  • src/fls/oci/from_oci.rs
  • src/fls/oci/manifest.rs

📝 Walkthrough

Walkthrough

Added optional artifact_type to OCI ImageManifest and changed manifest layer selection to prefer exact artifact-type matches or disk-image media types; get_single_layer now errors when no suitable disk image layer is found. flash_from_oci now validates selected layer media type before proceeding.

Changes

Cohort / File(s) Summary
OCI manifest parsing & selection
src/fls/oci/manifest.rs
Added pub artifact_type: Option<String> with #[serde(default)]. Reworked Manifest::get_single_layer() to: return sole layer immediately when len() == 1; when multiple layers, prefer exact artifact_type match, else prefer media types starting with application/vnd.automotive.disk; return an error if no disk image layer is found. Added unit tests for selection and error cases.
OCI flash entrypoint validation
src/fls/oci/from_oci.rs
After manifest.get_single_layer()?, validate that the chosen layer’s media_type starts with application/vnd.automotive.disk and return an error if it does not (prevents proceeding to compute layer size/compression/download/flash for non-disk media types).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 I hopped through manifests, sniffed each type,
Chose the disk that promised a boot that's right,
If artifact matches, I give a twirl,
If not, I thump — no flash for that swirl. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: adding media type validation before flashing, which aligns with the primary modifications in both manifest.rs and from_oci.rs.
Description check ✅ Passed The description is related to the changeset, stating the purpose of checking media types to avoid flashing non-disk layers, which matches the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/fls/oci/manifest.rs (1)

67-69: Add direct tests for the new selection rules.

A couple of focused unit tests for (1) artifact_type selection, (2) single non-disk layer rejection, and (3) the no-match error would cover the new branches in get_single_layer(). The current tests in this file only exercise parsing.

Also applies to: 149-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/manifest.rs` around lines 67 - 69, Add three focused unit tests
for Manifest::get_single_layer covering the new selection branches: (1) create a
manifest with artifact_type set and layers such that the correct layer is chosen
then assert get_single_layer returns that layer, (2) create a manifest with a
single layer whose mediaType is not a disk type and assert get_single_layer
returns the expected rejection/error, and (3) create a manifest with no matching
layers and assert get_single_layer returns the no-match error. Locate the
Manifest struct and the get_single_layer method to construct test Manifests and
expected Layer descriptors, use the same error types returned by
get_single_layer for assertions, and add these tests adjacent to the existing
parsing tests so they exercise lines handling artifact_type and the
layer-selection branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fls/oci/manifest.rs`:
- Around line 145-163: The early-return and artifact_type match currently accept
any layer regardless of media_type; update the selection logic in manifest.rs so
every successful path validates the layer.media_type against the allowed
flashable disk types (e.g., check starts_with("application/vnd.automotive.disk")
or an equivalent allowed list) before returning; specifically, when
m.layers.len() == 1 verify m.layers[0].media_type is flashable, when matching
via m.artifact_type ensure the found layer also has a flashable media_type, and
keep the existing fallback that finds the first layer whose media_type
starts_with the disk prefix.

---

Nitpick comments:
In `@src/fls/oci/manifest.rs`:
- Around line 67-69: Add three focused unit tests for Manifest::get_single_layer
covering the new selection branches: (1) create a manifest with artifact_type
set and layers such that the correct layer is chosen then assert
get_single_layer returns that layer, (2) create a manifest with a single layer
whose mediaType is not a disk type and assert get_single_layer returns the
expected rejection/error, and (3) create a manifest with no matching layers and
assert get_single_layer returns the no-match error. Locate the Manifest struct
and the get_single_layer method to construct test Manifests and expected Layer
descriptors, use the same error types returned by get_single_layer for
assertions, and add these tests adjacent to the existing parsing tests so they
exercise lines handling artifact_type and the layer-selection branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55bbc641-f85a-4511-a0be-17bb83c99ad0

📥 Commits

Reviewing files that changed from the base of the PR and between 60e5a9b and eab5e55.

📒 Files selected for processing (1)
  • src/fls/oci/manifest.rs

Comment on lines +145 to +163
if m.layers.len() == 1 {
return Ok(&m.layers[0]);
}

// If artifactType is set, find the layer matching it
if let Some(ref artifact_type) = m.artifact_type {
if let Some(layer) = m.layers.iter().find(|l| l.media_type == *artifact_type) {
return Ok(layer);
}
}

// Fall back to the first disk image layer
if let Some(layer) = m
.layers
.iter()
.find(|l| l.media_type.starts_with("application/vnd.automotive.disk"))
{
return Ok(layer);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate all successful paths against the flashable media types.

Line 145 returns the only layer before any media-type check, and Lines 149-153 will also accept any artifact_type match. That still lets a non-disk blob through this path, which is exactly what this PR is trying to prevent.

Minimal fix
             Manifest::Image(ref m) => {
+                let is_flashable = |layer: &Descriptor| {
+                    layer.media_type.starts_with("application/vnd.automotive.disk")
+                };
+
                 if m.layers.is_empty() {
                     return Err("Manifest has no layers".to_string());
                 }
 
                 if m.layers.len() == 1 {
-                    return Ok(&m.layers[0]);
+                    let layer = &m.layers[0];
+                    return if is_flashable(layer) {
+                        Ok(layer)
+                    } else {
+                        Err(format!("Layer media type '{}' is not flashable", layer.media_type))
+                    };
                 }
 
                 // If artifactType is set, find the layer matching it
                 if let Some(ref artifact_type) = m.artifact_type {
-                    if let Some(layer) = m.layers.iter().find(|l| l.media_type == *artifact_type) {
+                    if let Some(layer) = m.layers.iter().find(|l| {
+                        l.media_type == *artifact_type && is_flashable(l)
+                    }) {
                         return Ok(layer);
                     }
                 }
 
                 // Fall back to the first disk image layer
                 if let Some(layer) = m
                     .layers
                     .iter()
-                    .find(|l| l.media_type.starts_with("application/vnd.automotive.disk"))
+                    .find(|l| is_flashable(l))
                 {
                     return Ok(layer);
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/manifest.rs` around lines 145 - 163, The early-return and
artifact_type match currently accept any layer regardless of media_type; update
the selection logic in manifest.rs so every successful path validates the
layer.media_type against the allowed flashable disk types (e.g., check
starts_with("application/vnd.automotive.disk") or an equivalent allowed list)
before returning; specifically, when m.layers.len() == 1 verify
m.layers[0].media_type is flashable, when matching via m.artifact_type ensure
the found layer also has a flashable media_type, and keep the existing fallback
that finds the first layer whose media_type starts_with the disk prefix.

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.

+1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mangelajo, thanks for the confirmation! The fix is straightforward — every successful return path in get_single_layer needs to validate the layer's media type against the flashable disk prefix. Here's the suggested update:

pub fn get_single_layer(&self) -> Result<&Descriptor, String> {
    match self {
        Manifest::Image(ref m) => {
            let is_flashable = |layer: &Descriptor| {
                layer.media_type.starts_with("application/vnd.automotive.disk")
            };

            if m.layers.is_empty() {
                return Err("Manifest has no layers".to_string());
            }

            if m.layers.len() == 1 {
                let layer = &m.layers[0];
                return if is_flashable(layer) {
                    Ok(layer)
                } else {
                    Err(format!(
                        "Layer media type '{}' is not a flashable disk image",
                        layer.media_type
                    ))
                };
            }

            // If artifactType is set, find a layer matching it that is also flashable
            if let Some(ref artifact_type) = m.artifact_type {
                if let Some(layer) = m.layers.iter().find(|l| {
                    l.media_type == *artifact_type && is_flashable(l)
                }) {
                    return Ok(layer);
                }
            }

            // Fall back to the first disk image layer
            if let Some(layer) = m.layers.iter().find(|l| is_flashable(l)) {
                return Ok(layer);
            }

            Err(format!(
                "No disk image layer found among {} layers",
                m.layers.len()
            ))
        }
        Manifest::Index(_) => Err(
            "Cannot get layer from manifest index - need to resolve platform first".to_string(),
        ),
    }
}

Key changes vs the current PR:

  • Single-layer path: now rejects the layer if its media_type doesn't start with application/vnd.automotive.disk.
  • artifact_type path: the closure additionally requires is_flashable, so a matching artifact type alone is not enough.
  • Fallback path: already correct, just uses the shared is_flashable closure for consistency.

Would you like me to open this as a plan for code changes?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in ab866a4. All three return paths in get_single_layer() now validate the media type using a shared is_flashable closure that checks for the application/vnd.automotive.disk prefix:

  1. Single layer path: now returns an error if the layer has a non-flashable media type
  2. artifact_type path: now requires both media_type == artifact_type AND is_flashable
  3. Fallback path: uses the same is_flashable closure (functionally unchanged, just refactored for consistency)

Also added unit tests covering: single flashable layer acceptance, single non-disk layer rejection, artifact_type-based selection, and no-match error.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed — but with an important design adjustment: get_single_layer() is shared between the flash and extract paths. The extraction path (extract_files_from_oci_image_to_dir) legitimately needs to work with non-disk media types (e.g. tar+gzip layers), so adding flashable validation inside get_single_layer() broke the 5 oci_extract integration tests.

The fix instead:

  • Keeps get_single_layer() generic — single-layer manifests return the layer directly (callers decide what's valid)
  • Adds flashable validation in flash_from_oci() — the flash-specific path now rejects layers that don't start with application/vnd.automotive.disk
  • Multi-layer selection still prefers artifact_type match, then falls back to the first disk image layer

Also fixed the &&layer clippy lint (needless borrow).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

@bennyz bennyz requested a review from mangelajo March 18, 2026 18:15
if let Some(layer) = m
.layers
.iter()
.find(|l| l.media_type.starts_with("application/vnd.automotive.disk"))
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.

I would probably add some other more generic media types as well, so the oci artifacts could be reused in other areas. applcation/vnd.embedded.disk, or similar.

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.

@coderabbitai can you open a follow-up issue for this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

gh issue create \
  --repo jumpstarter-dev/fls \
  --title "Support more generic media types for disk image layer selection" \
  --body "## Follow-up: Support more generic media types for disk image layer selection

Currently, the fallback layer selection in \`get_single_layer()\` (in \`src/fls/oci/manifest.rs\`) only matches layers whose \`mediaType\` starts with \`application/vnd.automotive.disk\`.

To allow OCI artifacts to be reused in other areas (e.g. embedded systems beyond automotive), the selection logic should also consider more generic media types such as \`application/vnd.embedded.disk\` or similar conventions.

### Suggested improvement

Expand the media type matching in the fallback layer selection to include additional generic disk image media types, for example:
- \`application/vnd.embedded.disk\`
- Other relevant generic media types as appropriate

This would make \`fls\` more broadly applicable beyond automotive use cases.

---

Raised by `@mangelajo` in `#9`: https://github.com/jumpstarter-dev/fls/pull/9#discussion_r3043976233" \
  --assignee mangelajo

Length of output: 48


@mangelajo, I've opened a follow-up issue for this: #12 – Support more generic media types for disk image layer selection.

It covers expanding the fallback layer selection in get_single_layer() to support more generic media types (e.g., application/vnd.embedded.disk) beyond the current application/vnd.automotive.disk prefix, making fls more broadly applicable beyond automotive use cases.

@mangelajo
Copy link
Copy Markdown
Member

@ambient-code please handle existing comments.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/fls/oci/manifest.rs (1)

340-365: Make the artifact_type test prove artifact_type precedence.

As written, this still passes if the artifact_type branch is removed, because the fallback path also returns the same layer. Use two flashable layers with different media types so the test verifies the explicit artifactType match wins, and add a regression case for a non-flashable artifactType.

🧪 Suggested test shape
 fn test_artifact_type_selection() {
     let json = r#"{
         "schemaVersion": 2,
-        "artifactType": "application/vnd.automotive.disk.raw",
+        "artifactType": "application/vnd.automotive.disk.qcow2",
         "config": {
             "mediaType": "application/vnd.oci.image.config.v1+json",
             "digest": "sha256:config123",
             "size": 100
         },
         "layers": [
             {
-                "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
-                "digest": "sha256:layer1",
+                "mediaType": "application/vnd.automotive.disk.raw",
+                "digest": "sha256:disk-raw",
                 "size": 1000
             },
             {
-                "mediaType": "application/vnd.automotive.disk.raw",
-                "digest": "sha256:disk1",
+                "mediaType": "application/vnd.automotive.disk.qcow2",
+                "digest": "sha256:disk-qcow2",
                 "size": 9999
             }
         ]
     }"#;
     let manifest = Manifest::parse(json.as_bytes(), None).unwrap();
     let layer = manifest.get_single_layer().unwrap();
-    assert_eq!(layer.digest, "sha256:disk1");
+    assert_eq!(layer.digest, "sha256:disk-qcow2");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/manifest.rs` around lines 340 - 365, The test
test_artifact_type_selection currently doesn't prove artifactType precedence
because the fallback path selects the same layer; update the test to include two
flashable layers with distinct mediaTypes (e.g., one with
"application/vnd.automotive.disk.raw" and another with a different flashable
mediaType) and set artifactType in the manifest JSON to match only one of them
so Manifest::parse(... ) and manifest.get_single_layer() must pick the layer
whose mediaType equals artifactType; also add a regression case where
artifactType is a non-flashable mediaType to assert that the fallback selection
logic is used (or an error is returned) to ensure the artifact_type branch is
actually exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/fls/oci/manifest.rs`:
- Around line 340-365: The test test_artifact_type_selection currently doesn't
prove artifactType precedence because the fallback path selects the same layer;
update the test to include two flashable layers with distinct mediaTypes (e.g.,
one with "application/vnd.automotive.disk.raw" and another with a different
flashable mediaType) and set artifactType in the manifest JSON to match only one
of them so Manifest::parse(... ) and manifest.get_single_layer() must pick the
layer whose mediaType equals artifactType; also add a regression case where
artifactType is a non-flashable mediaType to assert that the fallback selection
logic is used (or an error is returned) to ensure the artifact_type branch is
actually exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef4d7957-3342-4ec6-ac48-75165781075e

📥 Commits

Reviewing files that changed from the base of the PR and between eab5e55 and ab866a4.

📒 Files selected for processing (1)
  • src/fls/oci/manifest.rs

Address review feedback: add flashable media type validation
(application/vnd.automotive.disk.*) in the flash_from_oci function,
which is the correct place for flash-specific checks. Keep
get_single_layer() generic since it is also used by the extraction
path where any media type is valid.

For multi-layer manifests, get_single_layer() prefers artifact_type
matches and falls back to the first disk image layer.

Add unit tests for: artifact_type selection, single layer (any type)
returned, single flashable layer acceptance, and no-match error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ambient-code ambient-code bot force-pushed the fix-layer-selection branch from ab866a4 to 5dfbeb5 Compare April 8, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants