Skip to content

Commit 8947fdf

Browse files
kastakhovurunc-bot[bot]
authored andcommitted
fix(initrd): merge file mounts into the existing CPIO archive
Merge regular-file bind mounts into the existing Unikraft newc archive before its trailer, while preserving append behavior for other guests. Canonicalize archive paths, create missing parent directories, reserve unique inodes, and validate the trailer before updating the initrd. Add focused coverage for merge behavior, invalid archives, inode allocation, and guest-specific dispatch, and include the initrd package in unit tests. Fixes #985 PR: #991 Signed-off-by: kastakhov <16296930+kastakhov@users.noreply.github.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
1 parent db506ce commit 8947fdf

8 files changed

Lines changed: 707 additions & 5 deletions

File tree

.github/contributors.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
users:
2+
kastakhov:
3+
name: Kostiantyn Astakhov
4+
email: me@lvfrfn.in.ua
25
ananos:
36
name: Anastassios Nanos
47
email: ananos@nubificus.co.uk

.github/linters/urunc-dict.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,5 @@ Readonlyfs
439439
capab
440440
werr
441441
cerr
442+
inodes
443+
newc

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ test: unittest e2etest
237237

238238
## unittest Run all unit tests
239239
.PHONY: unittest
240-
unittest: test_unikontainers test_metrics test_network test_hypervisors test_unikernels
240+
unittest: test_unikontainers test_initrd test_metrics test_network test_hypervisors test_unikernels
241241

242242
## e2etest Run all end-to-end tests
243243
.PHONY: e2etest
@@ -249,6 +249,13 @@ test_unikontainers:
249249
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/unikontainers -v
250250
@echo " "
251251

252+
## test_initrd Run unit tests for initrd package
253+
.PHONY: test_initrd
254+
test_initrd:
255+
@echo "Unit testing in initrd"
256+
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/unikontainers/initrd -v
257+
@echo " "
258+
252259
## test_metrics Run unit tests for metrics package
253260
test_metrics:
254261
@echo "Unit testing in internal/metrics"

pkg/unikontainers/initrd/initrd.go

Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,64 @@
1515
package initrd
1616

1717
import (
18+
"errors"
1819
"fmt"
20+
"io"
1921
"os"
22+
"path"
23+
"strconv"
24+
"strings"
2025
"syscall"
2126
"time"
2227

2328
"github.com/cavaliergopher/cpio"
2429
"github.com/opencontainers/runtime-spec/specs-go"
2530
)
2631

27-
func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, name string) error {
32+
const maxNewcInode int64 = 1<<32 - 1
33+
34+
const (
35+
newcHeaderSize = 110
36+
trailerName = "TRAILER!!!"
37+
)
38+
39+
// inodeAllocator prevents newly merged entries from colliding with existing
40+
// c_ino values. Unikraft treats repeated inodes with multiple links as hard
41+
// links, even when the records have different file types.
42+
type inodeAllocator struct {
43+
used map[int64]struct{}
44+
next int64
45+
}
46+
47+
func newInodeAllocator() *inodeAllocator {
48+
return &inodeAllocator{
49+
used: make(map[int64]struct{}),
50+
next: 1,
51+
}
52+
}
53+
54+
func (a *inodeAllocator) reserve(inode int64) {
55+
a.used[inode] = struct{}{}
56+
}
57+
58+
func (a *inodeAllocator) allocate() (int64, error) {
59+
for a.next <= maxNewcInode {
60+
inode := a.next
61+
a.next++
62+
if _, exists := a.used[inode]; exists {
63+
continue
64+
}
65+
a.used[inode] = struct{}{}
66+
return inode, nil
67+
}
68+
69+
return 0, errors.New("newc inode space exhausted")
70+
}
71+
72+
func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, name string, inode int64) error {
2873
hdr := &cpio.Header{
2974
Name: name,
75+
Inode: inode,
3076
Mode: cpio.FileMode(fileInfo.Mode),
3177
Uid: int(fileInfo.Uid),
3278
Guid: int(fileInfo.Gid),
@@ -46,6 +92,10 @@ func addInitrdRecord(w *cpio.Writer, content []byte, fileInfo *syscall.Stat_t, n
4692
}
4793

4894
func CopyFileToInitrd(w *cpio.Writer, srcFile string, destFile string) error {
95+
return copyFileToInitrdWithInode(w, srcFile, destFile, 0)
96+
}
97+
98+
func copyFileToInitrdWithInode(w *cpio.Writer, srcFile string, destFile string, inode int64) error {
4999
// Get the info of the original file
50100
fi, err := os.Stat(srcFile)
51101
if err != nil {
@@ -57,7 +107,7 @@ func CopyFileToInitrd(w *cpio.Writer, srcFile string, destFile string) error {
57107
if err != nil {
58108
return fmt.Errorf("could not read file %s: %w", srcFile, err)
59109
}
60-
err = addInitrdRecord(w, content, fileInfo, destFile)
110+
err = addInitrdRecord(w, content, fileInfo, destFile, inode)
61111
if err != nil {
62112
return fmt.Errorf("could not add record for %s: %w", srcFile, err)
63113
}
@@ -91,6 +141,203 @@ func CopyFileMountsToInitrd(oldInitrd string, mounts []specs.Mount) error {
91141
return nil
92142
}
93143

144+
// MergeFileMountsIntoInitrd adds regular-file bind mounts before the trailer
145+
// of an uncompressed newc archive. The archive is fully parsed before the
146+
// disposable initrd is updated in place.
147+
func MergeFileMountsIntoInitrd(oldInitrd string, mounts []specs.Mount) (retErr error) {
148+
fileMounts, err := regularFileBindMounts(mounts)
149+
if err != nil {
150+
return err
151+
}
152+
if len(fileMounts) == 0 {
153+
return nil
154+
}
155+
156+
initrdFile, err := os.OpenFile(oldInitrd, os.O_RDWR, 0)
157+
if err != nil {
158+
return fmt.Errorf("could not open %s: %w", oldInitrd, err)
159+
}
160+
defer func() {
161+
if err := initrdFile.Close(); err != nil {
162+
closeErr := fmt.Errorf("could not close %s: %w", oldInitrd, err)
163+
if retErr == nil {
164+
retErr = closeErr
165+
} else {
166+
retErr = errors.Join(retErr, closeErr)
167+
}
168+
}
169+
}()
170+
171+
trailerOffset, existingNames, inodes, err := inspectInitrd(initrdFile)
172+
if err != nil {
173+
return fmt.Errorf("could not parse %s: %w", oldInitrd, err)
174+
}
175+
if err := initrdFile.Truncate(trailerOffset); err != nil {
176+
return fmt.Errorf("could not truncate %s: %w", oldInitrd, err)
177+
}
178+
if _, err := initrdFile.Seek(trailerOffset, io.SeekStart); err != nil {
179+
return fmt.Errorf("could not seek in %s: %w", oldInitrd, err)
180+
}
181+
182+
w := cpio.NewWriter(initrdFile)
183+
for _, mount := range fileMounts {
184+
archiveName, err := archivePath(mount.Destination)
185+
if err != nil {
186+
return err
187+
}
188+
lookupName := archiveLookupPath(archiveName)
189+
if err := addMissingParents(w, lookupName, existingNames, inodes); err != nil {
190+
return err
191+
}
192+
inode, err := inodes.allocate()
193+
if err != nil {
194+
return fmt.Errorf("could not allocate inode for file %s: %w", archiveName, err)
195+
}
196+
if err := copyFileToInitrdWithInode(w, mount.Source, archiveName, inode); err != nil {
197+
return fmt.Errorf("could not add file %s to initrd: %w", mount.Source, err)
198+
}
199+
existingNames[lookupName] = struct{}{}
200+
}
201+
if err := w.Close(); err != nil {
202+
return fmt.Errorf("could not close initrd writer: %w", err)
203+
}
204+
205+
return nil
206+
}
207+
208+
func regularFileBindMounts(mounts []specs.Mount) ([]specs.Mount, error) {
209+
var fileMounts []specs.Mount
210+
for _, mount := range mounts {
211+
if mount.Type != "bind" {
212+
continue
213+
}
214+
info, err := os.Stat(mount.Source)
215+
if err != nil {
216+
return nil, fmt.Errorf("could not stat file %s: %w", mount.Source, err)
217+
}
218+
if info.Mode().IsRegular() {
219+
fileMounts = append(fileMounts, mount)
220+
}
221+
}
222+
return fileMounts, nil
223+
}
224+
225+
func inspectInitrd(f *os.File) (int64, map[string]struct{}, *inodeAllocator, error) {
226+
if _, err := f.Seek(0, io.SeekStart); err != nil {
227+
return 0, nil, nil, err
228+
}
229+
230+
r := cpio.NewReader(f)
231+
names := make(map[string]struct{})
232+
inodes := newInodeAllocator()
233+
var trailerOffset int64
234+
for {
235+
hdr, err := r.Next()
236+
if errors.Is(err, io.EOF) {
237+
if err := validateTrailerAt(f, trailerOffset); err != nil {
238+
return 0, nil, nil, err
239+
}
240+
return trailerOffset, names, inodes, nil
241+
}
242+
if err != nil {
243+
return 0, nil, nil, fmt.Errorf("could not read newc record at offset %d: %w", trailerOffset, err)
244+
}
245+
names[archiveLookupPath(hdr.Name)] = struct{}{}
246+
inodes.reserve(hdr.Inode)
247+
if _, err := io.Copy(io.Discard, r); err != nil {
248+
return 0, nil, nil, err
249+
}
250+
offset, err := f.Seek(0, io.SeekCurrent)
251+
if err != nil {
252+
return 0, nil, nil, err
253+
}
254+
trailerOffset = (offset + 3) &^ 3
255+
}
256+
}
257+
258+
func validateTrailerAt(f *os.File, offset int64) error {
259+
var header [newcHeaderSize]byte
260+
n, err := f.ReadAt(header[:], offset)
261+
if err != nil {
262+
if errors.Is(err, io.EOF) && n == 0 {
263+
return fmt.Errorf("missing newc trailer at offset %d: reached physical EOF", offset)
264+
}
265+
return fmt.Errorf("could not read newc trailer header at offset %d: %w", offset, err)
266+
}
267+
268+
magic := string(header[:6])
269+
if magic != "070701" && magic != "070702" {
270+
return fmt.Errorf("invalid newc trailer at offset %d: unsupported magic %q", offset, magic)
271+
}
272+
273+
size, err := strconv.ParseUint(string(header[54:62]), 16, 32)
274+
if err != nil {
275+
return fmt.Errorf("invalid newc trailer at offset %d: invalid file size: %w", offset, err)
276+
}
277+
if size != 0 {
278+
return fmt.Errorf("invalid newc trailer at offset %d: file size is %d, not zero", offset, size)
279+
}
280+
281+
nameSize, err := strconv.ParseUint(string(header[94:102]), 16, 32)
282+
if err != nil {
283+
return fmt.Errorf("invalid newc trailer at offset %d: invalid name size: %w", offset, err)
284+
}
285+
expectedName := trailerName + "\x00"
286+
if nameSize != uint64(len(expectedName)) {
287+
return fmt.Errorf("invalid newc trailer at offset %d: name size is %d, not %d", offset, nameSize, len(expectedName))
288+
}
289+
290+
name := make([]byte, len(expectedName))
291+
if _, err := f.ReadAt(name, offset+newcHeaderSize); err != nil {
292+
return fmt.Errorf("could not read newc trailer name at offset %d: %w", offset, err)
293+
}
294+
if string(name) != expectedName {
295+
return fmt.Errorf("invalid newc trailer at offset %d: name is not %q", offset, trailerName)
296+
}
297+
298+
return nil
299+
}
300+
301+
func archivePath(destination string) (string, error) {
302+
if !path.IsAbs(destination) {
303+
return "", fmt.Errorf("initrd mount destination %q is not absolute", destination)
304+
}
305+
// The supported Unikraft image flow uses the ./ namespace. Keeping all new
306+
// records there also makes parent lookup and later-record replacement consistent.
307+
return "./" + archiveLookupPath(destination), nil
308+
}
309+
310+
func archiveLookupPath(name string) string {
311+
return strings.TrimPrefix(path.Clean(name), "/")
312+
}
313+
314+
func addMissingParents(w *cpio.Writer, lookupName string, existingNames map[string]struct{}, inodes *inodeAllocator) error {
315+
parentName := ""
316+
for _, component := range strings.Split(path.Dir(lookupName), "/") {
317+
if component == "" || component == "." {
318+
continue
319+
}
320+
parentName = path.Join(parentName, component)
321+
if _, exists := existingNames[parentName]; exists {
322+
continue
323+
}
324+
325+
archiveName := "./" + parentName
326+
inode, err := inodes.allocate()
327+
if err != nil {
328+
return fmt.Errorf("could not allocate inode for directory %s: %w", archiveName, err)
329+
}
330+
// Two is the conventional minimum link count for a directory. Unique
331+
// allocated inodes keep Unikraft from treating unrelated entries as hard links.
332+
hdr := &cpio.Header{Name: archiveName, Inode: inode, Mode: cpio.TypeDir | 0o755, Links: 2}
333+
if err := w.WriteHeader(hdr); err != nil {
334+
return fmt.Errorf("could not add directory %s to initrd: %w", archiveName, err)
335+
}
336+
existingNames[parentName] = struct{}{}
337+
}
338+
return nil
339+
}
340+
94341
func AddFileToInitrd(oldInitrd string, data string, name string) error {
95342
f, err := os.OpenFile(oldInitrd, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
96343
if err != nil {
@@ -106,7 +353,7 @@ func AddFileToInitrd(oldInitrd string, data string, name string) error {
106353
Uid: 0,
107354
Gid: 0,
108355
}
109-
err = addInitrdRecord(w, []byte(data), &fileInfo, name)
356+
err = addInitrdRecord(w, []byte(data), &fileInfo, name, 0)
110357
if err != nil {
111358
return fmt.Errorf("could not add file %s to initrd: %w", name, err)
112359
}

0 commit comments

Comments
 (0)