-
Notifications
You must be signed in to change notification settings - Fork 198
fix(initrd): merge file mounts into the existing CPIO archive #991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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), | ||
|
|
@@ -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 { | ||
|
|
@@ -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) | ||
| } | ||
|
|
@@ -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) { | ||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment here with the rational of |
||
| 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 { | ||
|
|
@@ -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) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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).