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
3 changes: 3 additions & 0 deletions .github/contributors.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
users:
kastakhov:
name: Kostiantyn Astakhov
email: 16296930+kastakhov@users.noreply.github.com
ananos:
name: Anastassios Nanos
email: ananos@nubificus.co.uk
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ test: unittest e2etest

## unittest Run all unit tests
.PHONY: unittest
unittest: test_unikontainers test_metrics test_network test_hypervisors test_unikernels
unittest: test_unikontainers test_initrd test_metrics test_network test_hypervisors test_unikernels

## e2etest Run all end-to-end tests
.PHONY: e2etest
Expand All @@ -245,6 +245,13 @@ test_unikontainers:
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/unikontainers -v
@echo " "

## test_initrd Run unit tests for initrd package
.PHONY: test_initrd
test_initrd:
@echo "Unit testing in initrd"
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/unikontainers/initrd -v
@echo " "

## test_metrics Run unit tests for metrics package
test_metrics:
@echo "Unit testing in internal/metrics"
Expand Down
213 changes: 210 additions & 3 deletions pkg/unikontainers/initrd/initrd.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,59 @@
package initrd

import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/cavaliergopher/cpio"
"github.com/opencontainers/runtime-spec/specs-go"
)

func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, name string) error {
const maxNewcInode int64 = 1<<32 - 1

// inodeAllocator prevents newly merged entries from colliding with existing
// c_ino values. Unikraft treats repeated inodes with multiple links as hard
// links, even when the records have different file types.
type inodeAllocator struct {
used map[int64]struct{}
next int64
}

func newInodeAllocator() *inodeAllocator {
return &inodeAllocator{
used: make(map[int64]struct{}),
next: 1,
}
}

func (a *inodeAllocator) reserve(inode int64) {
a.used[inode] = struct{}{}
}

func (a *inodeAllocator) allocate() (int64, error) {
for a.next <= maxNewcInode {
inode := a.next
a.next++
if _, exists := a.used[inode]; exists {
continue
}
a.used[inode] = struct{}{}
return inode, nil
}

return 0, errors.New("newc inode space exhausted")
}

func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, name string, inode int64) error {
hdr := &cpio.Header{
Name: name,
Inode: inode,
Mode: cpio.FileMode(fileInfo.Mode),
Uid: int(fileInfo.Uid),
Guid: int(fileInfo.Gid),
Expand All @@ -46,6 +87,10 @@ func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, n
}

func CopyFileToInitrd(w *cpio.Writer, srcFile string, destFile string) error {
return copyFileToInitrdWithInode(w, srcFile, destFile, 0)
}

func copyFileToInitrdWithInode(w *cpio.Writer, srcFile string, destFile string, inode int64) error {
// Get the info of the original file
fi, err := os.Stat(srcFile)
if err != nil {
Expand All @@ -57,7 +102,7 @@ func CopyFileToInitrd(w *cpio.Writer, srcFile string, destFile string) error {
if err != nil {
return fmt.Errorf("could not read file %s: %w", srcFile, err)
}
err = addInitrdRecord(w, content, fileInfo, destFile)
err = addInitrdRecord(w, content, fileInfo, destFile, inode)
if err != nil {
return fmt.Errorf("could not add record for %s: %w", srcFile, err)
}
Expand Down Expand Up @@ -91,6 +136,168 @@ func CopyFileMountsToInitrd(oldInitrd string, mounts []specs.Mount) error {
return nil
}

// MergeFileMountsIntoInitrd adds regular-file bind mounts before the trailer
// of an uncompressed newc archive. The original initrd is replaced only after
// the complete updated archive has been written successfully.
func MergeFileMountsIntoInitrd(oldInitrd string, mounts []specs.Mount) error {
fileMounts, err := regularFileBindMounts(mounts)
if err != nil {
return err
}
if len(fileMounts) == 0 {
return nil
}

oldFile, err := os.Open(oldInitrd)
if err != nil {
return fmt.Errorf("could not open %s: %w", oldInitrd, err)
}
oldInfo, err := oldFile.Stat()
if err != nil {
oldFile.Close()
return fmt.Errorf("could not stat %s: %w", oldInitrd, err)
}

tmp, err := os.CreateTemp(filepath.Dir(oldInitrd), "."+filepath.Base(oldInitrd)+".*")
if err != nil {
oldFile.Close()
return fmt.Errorf("could not create temporary initrd: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
defer tmp.Close()

if _, err := io.Copy(tmp, oldFile); err != nil {
oldFile.Close()
return fmt.Errorf("could not copy %s: %w", oldInitrd, err)
}
if err := oldFile.Close(); err != nil {
return fmt.Errorf("could not close %s: %w", oldInitrd, err)
}

trailerOffset, existingNames, inodes, err := findTrailer(tmp)
if err != nil {
return fmt.Errorf("could not parse %s: %w", oldInitrd, err)
}
if err := tmp.Truncate(trailerOffset); err != nil {
return fmt.Errorf("could not truncate temporary initrd: %w", err)
}
if _, err := tmp.Seek(trailerOffset, io.SeekStart); err != nil {
return fmt.Errorf("could not seek in temporary initrd: %w", err)
}

w := cpio.NewWriter(tmp)
for _, mount := range fileMounts {
destination, err := archivePath(mount.Destination)
if err != nil {
return err
}
if err := addMissingParents(w, destination, existingNames, inodes); err != nil {
return err
}
inode, err := inodes.allocate()
if err != nil {
return fmt.Errorf("could not allocate inode for file %s: %w", destination, err)
}
if err := copyFileToInitrdWithInode(w, mount.Source, destination, inode); err != nil {
return fmt.Errorf("could not add file %s to initrd: %w", mount.Source, err)
}
existingNames[destination] = struct{}{}
}
if err := w.Close(); err != nil {
return fmt.Errorf("could not close initrd writer: %w", err)
}
permissionBits := oldInfo.Mode() & (os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky)
if err := tmp.Chmod(permissionBits); err != nil {
return fmt.Errorf("could not preserve initrd permissions: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("could not close temporary initrd: %w", err)
}
if err := os.Rename(tmpName, oldInitrd); err != nil {
return fmt.Errorf("could not replace %s: %w", oldInitrd, err)
}

return nil
}

func regularFileBindMounts(mounts []specs.Mount) ([]specs.Mount, error) {
var fileMounts []specs.Mount
for _, mount := range mounts {
if mount.Type != "bind" {
continue
}
info, err := os.Stat(mount.Source)
if err != nil {
return nil, fmt.Errorf("could not stat file %s: %w", mount.Source, err)
}
if info.Mode().IsRegular() {
fileMounts = append(fileMounts, mount)
}
}
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).

if _, err := f.Seek(0, io.SeekStart); err != nil {
return 0, nil, nil, err
}

r := cpio.NewReader(f)
names := make(map[string]struct{})
inodes := newInodeAllocator()
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).

return trailerOffset, names, inodes, nil
}
if err != nil {
return 0, nil, nil, err
}
names[hdr.Name] = struct{}{}
inodes.reserve(hdr.Inode)
if _, err := io.Copy(io.Discard, r); err != nil {
return 0, nil, nil, err
}
offset, err := f.Seek(0, io.SeekCurrent)
if err != nil {
return 0, nil, nil, err
}
trailerOffset = (offset + 3) &^ 3
}
}

func archivePath(destination string) (string, error) {
if !path.IsAbs(destination) {
return "", fmt.Errorf("initrd mount destination %q is not absolute", destination)
}
return "./" + strings.TrimPrefix(path.Clean(destination), "/"), nil
}

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.

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.

missing = append(missing, archiveParent)
}
}
for i := len(missing) - 1; i >= 0; i-- {
parent := missing[i]
inode, err := inodes.allocate()
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.

if err := w.WriteHeader(hdr); err != nil {
return fmt.Errorf("could not add directory %s to initrd: %w", parent, err)
}
existingNames[parent] = struct{}{}
}
return nil
}

func AddFileToInitrd(oldInitrd string, data string, name string) error {
f, err := os.OpenFile(oldInitrd, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
Expand All @@ -106,7 +313,7 @@ func AddFileToInitrd(oldInitrd string, data string, name string) error {
Uid: 0,
Gid: 0,
}
err = addInitrdRecord(w, []byte(data), &fileInfo, name)
err = addInitrdRecord(w, []byte(data), &fileInfo, name, 0)
if err != nil {
return fmt.Errorf("could not add file %s to initrd: %w", name, err)
}
Expand Down
Loading