Skip to content

fix(initrd): merge file mounts into the existing CPIO archive - #991

Open
kastakhov wants to merge 2 commits into
urunc-dev:mainfrom
kastakhov:fix/unikraft-initrd-file-mounts
Open

fix(initrd): merge file mounts into the existing CPIO archive#991
kastakhov wants to merge 2 commits into
urunc-dev:mainfrom
kastakhov:fix/unikraft-initrd-file-mounts

Conversation

@kastakhov

Copy link
Copy Markdown

Description

CopyFileMountsToInitrd appends file mounts as a second CPIO archive. Unikraft stops extracting at the original TRAILER!!!, making the appended files unavailable inside the guest.

This change:

  • Merges file mounts into the existing Unikraft CPIO archive before its trailer.
  • Adds missing parent directories and writes one final trailer.
  • Avoid inode collisions when merging mounts
  • Preserves the original initrd permissions and replaces it only after a successful rewrite.
  • Keeps the existing append behavior for Linux and other guest types.
  • Adds unit tests for archive merging and guest-type selection.

Related issues

How was this tested?

  • go test ./pkg/unikontainers/initrd
  • e2e tests: test_docker, test_ctr
  • Tested an identical Kubernetes workload with unpatched and patched runtimes:
    • The unpatched runtime produced two trailers, and mounted ConfigMap files were unavailable to Unikraft.
    • The patched runtime produced one trailer, and the mounted files were visible with the expected values.

LLM usage

OpenAI Codex (GPT-5.6) assisted with the implementation, test review, and PR description based on my specifications. I reviewed and approved the final changes.

Checklist

  • I have read the contribution guide.
  • The linter passes locally (make lint).
  • The e2e tests of at least one tool pass locally (make test_ctr, make test_nerdctl, make test_docker, make test_crictl).
  • If LLMs were used: I have read the llm policy.

Signed-off-by: kastakhov <16296930+kastakhov@users.noreply.github.com>
@netlify

netlify Bot commented Aug 24, 2026

Copy link
Copy Markdown

Deploy Preview for urunc canceled.

Name Link
🔨 Latest commit 09fe4f2
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/6a8c5534b1814f00087fece3

Signed-off-by: kastakhov <16296930+kastakhov@users.noreply.github.com>
@kastakhov
kastakhov force-pushed the fix/unikraft-initrd-file-mounts branch from e9308d3 to 09fe4f2 Compare August 24, 2026 14:29

@cmainas cmainas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello @kastakhov ,

thank you for this PR. I was wrong about my initial thought of simplification. This PR resolves multiple issues correctly. However, I have added some comments and some generic notes:

  • Some parts of the code would benefit from some comments to explain the rationale and the logic (e.g. ./ preifx in archivePath so we can think twice if we need to change that in the future).
  • We do not really need the temporary file. It is fine to parse and then append the original file. If something goes wrong we will fail the execution. Then a new container will start and the snapshotter will create a fresh rootfs with the original initrd.


func (i initrdRootfs) postSetup() error {
err := initrd.CopyFileMountsToInitrd(i.initrdHostFullPath, i.mounts)
updateInitrd := initrd.CopyFileMountsToInitrd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lets not use function assignment here and simply call the respective function inside the respective if branch. We avoid function assignments.

"github.com/urunc-dev/urunc/pkg/unikontainers/unikernels"
)

func TestNewRootfsBuilderPassesGuestTypeToInitrd(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's add a TODO comment to move this test somewhere else, since it tests a function that does not belong to initrd_rootfs.go

Comment on lines +59 to +65
w := cpio.NewWriter(archive)
require.NoError(t, w.WriteHeader(&cpio.Header{
Name: "./original", Mode: cpio.TypeReg | 0o644, Size: 3,
}))
_, err = w.Write([]byte("old"))
require.NoError(t, err)
require.NoError(t, w.Close())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: We can squash these lines to just one require.NoError(t, cpio.NewWriter(archive).Close()). An initrd with just just the trailer is valid.

var trailerOffset int64
for {
hdr, err := r.Next()
if errors.Is(err, io.EOF) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We still need to ensure that there is a valid trailer here. The "github.com/cavaliergopher/cpio" package will return EOF in the case trailer was read or if there are no bytes (actual eof).

var missing []string
for parent := path.Dir(strings.TrimPrefix(name, "./")); parent != "."; parent = path.Dir(parent) {
archiveParent := "./" + parent
if _, exists := existingNames[archiveParent]; !exists {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This check can lead to duplicates. The exisitngNames map contains all entries found from findTrailer. But in findTrailer the value is written as found in the initrd record. However, there is no guarantee that the name will have the prefix "./". So, we should ensure that the values stored in existingNames have the same format as the ones we compare here.


func addMissingParents(w *cpio.Writer, name string, existingNames map[string]struct{}, inodes *inodeAllocator) error {
var missing []string
for parent := path.Dir(strings.TrimPrefix(name, "./")); parent != "."; parent = path.Dir(parent) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This loop is bug prone. If in the future we are careless and remove the "./" prefix in archivePath, then we risk having a full path which will never end up in ".".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think (not really tested it out) if we reverse the order (from top directory to children, then we wll also reduce the complexity of parsing the missing list in reverse.

if err != nil {
return fmt.Errorf("could not allocate inode for directory %s: %w", parent, err)
}
hdr := &cpio.Header{Name: parent, Inode: inode, Mode: cpio.TypeDir | 0o755, Links: 2}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A comment here with the rational of Links: 2 will be helpful.

return fileMounts, nil
}

func findTrailer(f *os.File) (int64, map[string]struct{}, *inodeAllocator, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function should be renamed to something that captures its role better (e.g. parseInitrd).

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.

OCI file mounts are not extracted from Unikraft initrd

2 participants