Skip to content

Embed SBOM in native image via generated GraalVM Feature#53923

Merged
aloubyansky merged 4 commits intoquarkusio:mainfrom
zakkak:2026-04-30-sbom-embedding-native
May 5, 2026
Merged

Embed SBOM in native image via generated GraalVM Feature#53923
aloubyansky merged 4 commits intoquarkusio:mainfrom
zakkak:2026-04-30-sbom-embedding-native

Conversation

@zakkak
Copy link
Copy Markdown
Member

@zakkak zakkak commented May 4, 2026

Generate a GraalVM Feature (io.quarkus.runner.SbomEmbedFeature) using Gizmo2 that embeds the application SBOM into the native image, following the GraalVM SBOM spec.

Internal GraalVM APIs (CGlobalDataFactory, CGlobalDataFeature, Word) are referenced via ClassMethodDesc to avoid compile-time dependencies on internal packages that may change across GraalVM versions.

CGlobalDataFactory and CGlobalData moved from com.oracle.svm.core.c to com.oracle.svm.guest.staging.c in GraalVM 25.1.
The generated Feature checks the GraalVM version at native image build time and calls the correct API.

Tested with:

  • Mandrel 23.1
  • Mandrel 25.0
  • GraalVM CE 25.1-dev

The disassembly of the generated feature looks like this:

public class SbomEmbedFeature implements Feature {
    public String getDescription() {
        return "Embeds the application SBOM in the native image";
    }

    public void afterAnalysis(Feature.AfterAnalysisAccess access) {
        try {
            InputStream is = SbomEmbedFeature.class.getResourceAsStream("/META-INF/sbom/dependency.cdx.json.gz");
            if (is == null) {
                throw new RuntimeException("SBOM resource not found on classpath: META-INF/sbom/dependency.cdx.json.gz");
            } else {
                byte[] resourceBytes = is.readAllBytes();
                is.close();
                Supplier supplier = () -> resourceBytes;
                UnsignedWord unsignedLen = WordFactory.unsigned((long)resourceBytes.length);
                GraalVM.Version graalVMVersion = Version.getCurrent();
                CGlobalDataFeature cgFeature = CGlobalDataFeature.singleton();
                if (graalVMVersion.compareTo(new int[]{25, 1}) < 0) {
                    CGlobalData sbomData = CGlobalDataFactory.createBytes(supplier, "sbom");
                    cgFeature.registerWithGlobalSymbol(sbomData);
                    CGlobalData sbomLenData = CGlobalDataFactory.createWord(unsignedLen, "sbom_length");
                    cgFeature.registerWithGlobalSymbol(sbomLenData);
                } else {
                    com.oracle.svm.guest.staging.c.CGlobalData sbomData = com.oracle.svm.guest.staging.c.CGlobalDataFactory.createBytes(supplier, "sbom");
                    cgFeature.registerWithGlobalSymbol(sbomData);
                    com.oracle.svm.guest.staging.c.CGlobalData sbomLenData = com.oracle.svm.guest.staging.c.CGlobalDataFactory.createWord(unsignedLen, "sbom_length");
                    cgFeature.registerWithGlobalSymbol(sbomLenData);
                }

            }
        } catch (Exception var11) {
            ((Throwable)var11).printStackTrace();
            throw new RuntimeException(var11);
        }
    }
}

Assisted-by: Claude Opus 4.6 noreply@anthropic.com

Follow up to #53552

Supersedes #53861 and graalvm/mandrel#962

cc @jerboaa

zakkak added 2 commits April 30, 2026 18:02
Generate a GraalVM Feature (`io.quarkus.runner.SbomEmbedFeature`) using
Gizmo2 that embeds the application SBOM into the native image, following
the GraalVM SBOM spec.

Internal GraalVM APIs (CGlobalDataFactory, CGlobalDataFeature, Word) are
referenced via ClassMethodDesc to avoid compile-time dependencies on
internal packages that may change across GraalVM versions.

The Feature is only generated when EmbeddedSbomMetadataBuildItem is
present (i.e. when CycloneDX SBOM embedding is enabled).

See: graalvm/mandrel#962 and
quarkusio#53552

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
CGlobalDataFactory and CGlobalData moved from com.oracle.svm.core.c to
com.oracle.svm.guest.staging.c in GraalVM 25.1. The generated Feature
now checks the GraalVM version at native image build time and calls the
correct API. Also switch from Word.unsigned() to the public
WordFactory.unsigned() and fix registerWithGlobalSymbol return type
(void, not CGlobalData).

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@jerboaa jerboaa left a comment

Choose a reason for hiding this comment

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

Seems OK to me. I think we are going to need a test for this functionality eventually as this could easily break.

One more thought:
#53552 potentially registers the SBOM as a native resource (so we could end up with two copies of the same SBOM in two different places). Not sure how to fix this duplication, though.

@aloubyansky
Copy link
Copy Markdown
Member

@zakkak thanks! I had the same points as @jerboaa plus a mention in docs.

I guess it wouldn't make sense practically but out of curiosity, if a user enabled SBOM embedding in GraalVM EE and in Quarkus, would GraalVM EE win?


@BuildStep
void generateSbomEmbedFeature(
Optional<EmbeddedSbomMetadataBuildItem> embeddedSbomMetadata,
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.

Is there a reason to make it optional? Wouldn't a build step be skipped if required input wasn't available?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's what I thought as well, but it actually gets triggered with embeddedSbomMetadata being null. I also noticed you did the same in github.com//pull/53552 so I hoped you knew why it's happening. Perhaps it's worth further investigating (beyond the scope of this PR though).

@zakkak
Copy link
Copy Markdown
Member Author

zakkak commented May 4, 2026

Seems OK to me. I think we are going to need a test for this functionality eventually as this could easily break.

Since there is no native-image support (yet) for inspecting the embedded SBOM we can use nm and readelf to get access to the data or read the elf file in pure Java. Both approaches kind of complicate the test with the pure-java one adding quite some code with somewhat complex logic. Any preference?

The test would only work on linux by the way.

One more thought: #53552 potentially registers the SBOM as a native resource (so we could end up with two copies of the same SBOM in two different places). Not sure how to fix this duplication, though.

That's a good point. We can probably serve the endpoint from the embedded SBOM but the code will be somewhat complex (I am thinking using a helper method that would be generated using gizmo to access the necessary GraalVM APIs).

plus a mention in docs

Noted, I will add some info there.

I guess it wouldn't make sense practically but out of curiosity, if a user enabled SBOM embedding in GraalVM EE and in Quarkus, would GraalVM EE win?

I don't know to be honest. One would probably like GraalVM EE to win as its data would be more accurate due to taking in account the static analysis it performs.

@aloubyansky
Copy link
Copy Markdown
Member

I guess it wouldn't make sense practically but out of curiosity, if a user enabled SBOM embedding in GraalVM EE and in Quarkus, would GraalVM EE win?

I don't know to be honest. One would probably like GraalVM EE to win as its data would be more accurate due to taking in account the static analysis it performs.

That's actually not obvious. There will be important info in the Quarkus SBOM (metadata, deployment and npm dependencies) that GraalVM won't have. Anyway, it will be a user's choice.

@quarkus-bot

This comment has been minimized.

@jerboaa
Copy link
Copy Markdown
Contributor

jerboaa commented May 4, 2026

Since there is no native-image support (yet) for inspecting the embedded SBOM we can use nm and readelf to get access to the data or read the elf file in pure Java. Both approaches kind of complicate the test with the pure-java one adding quite some code with somewhat complex logic. Any preference?

I was thinking we could be using the anchore/syft container to do the SBOM extraction (e.g. by syft scan <binary> -o cyclonedx-json=out.json). Have you considered that?

@zakkak
Copy link
Copy Markdown
Member Author

zakkak commented May 4, 2026

Yes that's another option (and what I actually did locally to see if it works as expected) but I was trying to avoid another docker image dependency. If the Quarkus team is OK with that I will use the syft container. cc (usual suspects) @gsmet @geoand

@geoand
Copy link
Copy Markdown
Contributor

geoand commented May 5, 2026

Sorry if this sounds stupid, but as I don't know much about SBOM I understand when the docker image would be used

@zakkak
Copy link
Copy Markdown
Member Author

zakkak commented May 5, 2026

Sorry if this sounds stupid, but as I don't know much about SBOM I understand when the docker image would be used

@geoand in this case the SBOM (a json file essentially) is stored compressed in raw format in the native binary, so we need a tool to extract it from the binary and parse it. syft CLI is a tool that can achieve this. So the question is if it's OK to use the syft container image to verify the SBOM is correctly embedded in the binary and can be parsed by such tools. The alternative is to manually do the extraction and verification in the test code itself, which would introduce some complexity to the test itself.

@geoand
Copy link
Copy Markdown
Contributor

geoand commented May 5, 2026

So essentially you want to use syft in a test, right?

@zakkak
Copy link
Copy Markdown
Member Author

zakkak commented May 5, 2026

Correct.

@geoand
Copy link
Copy Markdown
Contributor

geoand commented May 5, 2026

I think it's fine

zakkak added 2 commits May 5, 2026 14:24
Add syft-based verification to CycloneDxNativeIT that confirms the SBOM
is properly embedded in the native executable and readable by external
tooling. The test runs anchore/syft in a container against the native
binary and asserts the extracted SBOM contains the expected components.
Skipped automatically when no container runtime is available.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@aloubyansky aloubyansky left a comment

Choose a reason for hiding this comment

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

Awesome, thanks @zakkak and @jerboaa!

@quarkus-bot
Copy link
Copy Markdown

quarkus-bot Bot commented May 5, 2026

Status for workflow Quarkus Documentation CI

This is the status report for running Quarkus Documentation CI on commit 78f292a.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

Warning

There are other workflow runs running, you probably need to wait for their status before merging.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

🙈 The PR is closed and the preview is expired.

Copy link
Copy Markdown
Contributor

@jerboaa jerboaa left a comment

Choose a reason for hiding this comment

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

Looks fine. Thanks for the test.

@zakkak zakkak added the triage/waiting-for-ci Ready to merge when CI successfully finishes label May 5, 2026
@quarkus-bot

This comment has been minimized.

@quarkus-bot
Copy link
Copy Markdown

quarkus-bot Bot commented May 5, 2026

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit 78f292a.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the Develocity build scans.

@aloubyansky aloubyansky merged commit 374cf01 into quarkusio:main May 5, 2026
127 of 129 checks passed
@quarkus-bot quarkus-bot Bot added this to the 3.36 - main milestone May 5, 2026
@quarkus-bot quarkus-bot Bot removed the triage/waiting-for-ci Ready to merge when CI successfully finishes label May 5, 2026
@zakkak zakkak deleted the 2026-04-30-sbom-embedding-native branch May 5, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants