1515package initrd
1616
1717import (
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
4894func 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+
94341func 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