@@ -47,6 +47,13 @@ const (
47
47
// since that is the maximum number of symlinks the os.Root API will handle. From the os.Root API,
48
48
// "8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX), and a common limit".
49
49
DefaultMaxSymlinkDepth = 6
50
+
51
+ // filePermission represents the permission bits for a file, which are minimal since files in the
52
+ // layer scanning use case are read-only.
53
+ filePermission = 0600
54
+ // dirPermission represents the permission bits for a directory, which are minimal since
55
+ // directories in the layer scanning use case are read-only.
56
+ dirPermission = 0700
50
57
)
51
58
52
59
var (
@@ -83,8 +90,7 @@ func DefaultConfig() *Config {
83
90
// image. Checks include:
84
91
//
85
92
// (1) MaxFileBytes is positive.
86
- // (2) Requirer is not nil.
87
- // (3) MaxSymlinkDepth is non-negative.
93
+ // (2) MaxSymlinkDepth is non-negative.
88
94
func validateConfig (config * Config ) error {
89
95
if config .MaxFileBytes <= 0 {
90
96
return fmt .Errorf ("%w: max file bytes must be positive: %d" , ErrInvalidConfig , config .MaxFileBytes )
@@ -101,9 +107,8 @@ type Image struct {
101
107
chainLayers []* chainLayer
102
108
config * Config
103
109
size int64
104
- root * os.Root
105
- ExtractDir string
106
110
BaseImageIndex int
111
+ contentBlob * os.File
107
112
}
108
113
109
114
// TopFS returns the filesystem of the top-most chainlayer of the image. All available files should
@@ -129,7 +134,15 @@ func (img *Image) ChainLayers() ([]scalibrImage.ChainLayer, error) {
129
134
130
135
// CleanUp removes the temporary directory used to store the image files.
131
136
func (img * Image ) CleanUp () error {
132
- return os .RemoveAll (img .ExtractDir )
137
+ if img .contentBlob == nil {
138
+ return nil
139
+ }
140
+
141
+ if err := img .contentBlob .Close (); err != nil {
142
+ log .Warnf ("failed to close content blob: %v" , err )
143
+ }
144
+
145
+ return os .Remove (img .contentBlob .Name ())
133
146
}
134
147
135
148
// Size returns the size of the underlying directory of the image in bytes.
@@ -192,20 +205,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) {
192
205
return nil , fmt .Errorf ("failed to initialize chain layers: %w" , err )
193
206
}
194
207
195
- imageExtractionPath , err := os .MkdirTemp ("" , "osv-scalibr-image-scanning-*" )
196
- if err != nil {
197
- return nil , fmt .Errorf ("failed to create temporary directory: %w" , err )
198
- }
199
-
200
- // OpenRoot assumes that the provided directory is trusted. In this case, we created the
201
- // imageExtractionPath directory, so it is indeed trusted.
202
- root , err := os .OpenRoot (imageExtractionPath )
208
+ imageContentBlob , err := os .CreateTemp ("" , "image-blob-*" )
203
209
if err != nil {
204
- return nil , fmt .Errorf ("failed to open root directory : %w" , err )
210
+ return nil , fmt .Errorf ("failed to create image content file : %w" , err )
205
211
}
206
- // Close the root directory at the end of the function, since no more files will be unpacked
207
- // afterward.
208
- defer root .Close ()
209
212
210
213
baseImageIndex , err := findBaseImageIndex (history )
211
214
if err != nil {
@@ -215,14 +218,13 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) {
215
218
outputImage := & Image {
216
219
chainLayers : chainLayers ,
217
220
config : config ,
218
- root : root ,
219
- ExtractDir : imageExtractionPath ,
220
221
BaseImageIndex : baseImageIndex ,
222
+ contentBlob : imageContentBlob ,
221
223
}
222
224
223
225
// Add the root directory to each chain layer. If this is not done, then the virtual paths won't
224
226
// be rooted, and traversal in the virtual filesystem will be broken.
225
- if err := addRootDirectoryToChainLayers (outputImage .chainLayers , imageExtractionPath ); err != nil {
227
+ if err := addRootDirectoryToChainLayers (outputImage .chainLayers ); err != nil {
226
228
return nil , handleImageError (outputImage , fmt .Errorf ("failed to add root directory to chain layers: %w" , err ))
227
229
}
228
230
@@ -240,13 +242,6 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) {
240
242
continue
241
243
}
242
244
243
- layerDir := layerDirectory (i )
244
-
245
- // Create the chain layer directory if it doesn't exist.
246
- if err := root .Mkdir (layerDir , dirPermission ); err != nil && ! errors .Is (err , fs .ErrExist ) {
247
- return nil , handleImageError (outputImage , fmt .Errorf ("failed to create chain layer directory: %w" , err ))
248
- }
249
-
250
245
if v1LayerIndex < 0 {
251
246
return nil , handleImageError (outputImage , fmt .Errorf ("mismatch between v1 layers and chain layers, on v1 layer index %d, but only %d v1 layers" , v1LayerIndex , len (v1Layers )))
252
247
}
@@ -265,12 +260,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) {
265
260
defer layerReader .Close ()
266
261
267
262
tarReader := tar .NewReader (layerReader )
268
- layerSize , err := fillChainLayersWithFilesFromTar (outputImage , tarReader , layerDir , chainLayersToFill )
269
- if err != nil {
263
+ if err := fillChainLayersWithFilesFromTar (outputImage , tarReader , chainLayersToFill ); err != nil {
270
264
return fmt .Errorf ("failed to fill chain layer with v1 layer tar: %w" , err )
271
265
}
272
266
273
- outputImage .size += layerSize
274
267
return nil
275
268
}()
276
269
@@ -286,16 +279,10 @@ func FromV1Image(v1Image v1.Image, config *Config) (*Image, error) {
286
279
// Helper functions
287
280
// ========================================================
288
281
289
- func layerDirectory (layerIndex int ) string {
290
- return fmt .Sprintf ("layer-%d" , layerIndex )
291
- }
292
-
293
282
// addRootDirectoryToChainLayers adds the root ("/") directory to each chain layer.
294
- func addRootDirectoryToChainLayers (chainLayers []* chainLayer , extractDir string ) error {
295
- for i , chainLayer := range chainLayers {
283
+ func addRootDirectoryToChainLayers (chainLayers []* chainLayer ) error {
284
+ for _ , chainLayer := range chainLayers {
296
285
err := chainLayer .fileNodeTree .Insert ("/" , & virtualFile {
297
- extractDir : extractDir ,
298
- layerDir : layerDirectory (i ),
299
286
virtualPath : "/" ,
300
287
isWhiteout : false ,
301
288
mode : fs .ModeDir ,
@@ -430,23 +417,20 @@ func initializeChainLayers(v1Layers []v1.Layer, history []v1.History, maxSymlink
430
417
// fillChainLayersWithFilåesFromTar fills the chain layers with the files found in the tar. The
431
418
// chainLayersToFill are the chain layers that will be filled with the files via the virtual
432
419
// filesystem.
433
- func fillChainLayersWithFilesFromTar (img * Image , tarReader * tar.Reader , layerDir string , chainLayersToFill []* chainLayer ) ( int64 , error ) {
420
+ func fillChainLayersWithFilesFromTar (img * Image , tarReader * tar.Reader , chainLayersToFill []* chainLayer ) error {
434
421
if len (chainLayersToFill ) == 0 {
435
- return 0 , errors .New ("no chain layers provided, this should not happen" )
422
+ return errors .New ("no chain layers provided, this should not happen" )
436
423
}
437
424
438
425
currentChainLayer := chainLayersToFill [0 ]
439
426
440
- // layerSize is the cumulative size of all the extracted files in the tar.
441
- var layerSize int64
442
-
443
427
for {
444
428
header , err := tarReader .Next ()
445
429
if errors .Is (err , io .EOF ) {
446
430
break
447
431
}
448
432
if err != nil {
449
- return 0 , fmt .Errorf ("could not read tar: %w" , err )
433
+ return fmt .Errorf ("could not read tar: %w" , err )
450
434
}
451
435
452
436
// Some tools prepend everything with "./", so if we don't path.Clean the name, we may have
@@ -501,11 +485,11 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir
501
485
var newVirtualFile * virtualFile
502
486
switch header .Typeflag {
503
487
case tar .TypeDir :
504
- newVirtualFile , err = img .handleDir (virtualPath , layerDir , header , isWhiteout )
488
+ newVirtualFile = img .handleDir (virtualPath , header , isWhiteout )
505
489
case tar .TypeReg :
506
- newVirtualFile , err = img .handleFile (virtualPath , layerDir , tarReader , header , isWhiteout )
490
+ newVirtualFile , err = img .handleFile (virtualPath , tarReader , header , isWhiteout )
507
491
case tar .TypeSymlink , tar .TypeLink :
508
- newVirtualFile , err = img .handleSymlink (virtualPath , layerDir , header , isWhiteout )
492
+ newVirtualFile , err = img .handleSymlink (virtualPath , header , isWhiteout )
509
493
default :
510
494
log .Warnf ("unsupported file type: %v, path: %s" , header .Typeflag , header .Name )
511
495
continue
@@ -518,14 +502,12 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir
518
502
log .Warnf ("failed to handle tar entry with path %s: %w" , virtualPath , err )
519
503
continue
520
504
}
521
- return 0 , fmt .Errorf ("failed to handle tar entry with path %s: %w" , virtualPath , err )
505
+ return fmt .Errorf ("failed to handle tar entry with path %s: %w" , virtualPath , err )
522
506
}
523
507
524
- layerSize += header .Size
525
-
526
508
// If the virtual path has any directories and those directories have not been populated, then
527
509
// populate them with file nodes.
528
- populateEmptyDirectoryNodes (virtualPath , layerDir , img . ExtractDir , chainLayersToFill )
510
+ populateEmptyDirectoryNodes (virtualPath , chainLayersToFill )
529
511
530
512
// In each outer loop, a layer is added to each relevant output chainLayer slice. Because the
531
513
// outer loop is looping backwards (latest layer first), we ignore any files that are already in
@@ -536,13 +518,13 @@ func fillChainLayersWithFilesFromTar(img *Image, tarReader *tar.Reader, layerDir
536
518
layer := currentChainLayer .latestLayer .(* Layer )
537
519
_ = layer .fileNodeTree .Insert (virtualPath , newVirtualFile )
538
520
}
539
- return layerSize , nil
521
+ return nil
540
522
}
541
523
542
524
// populateEmptyDirectoryNodes populates the chain layers with file nodes for any directory paths
543
525
// that do not have an associated file node. This is done by creating a file node for each directory
544
526
// in the virtual path and then filling the chain layers with that file node.
545
- func populateEmptyDirectoryNodes (virtualPath , layerDir , extractDir string , chainLayersToFill []* chainLayer ) {
527
+ func populateEmptyDirectoryNodes (virtualPath string , chainLayersToFill []* chainLayer ) {
546
528
currentChainLayer := chainLayersToFill [0 ]
547
529
548
530
runningDir := "/"
@@ -557,8 +539,6 @@ func populateEmptyDirectoryNodes(virtualPath, layerDir, extractDir string, chain
557
539
}
558
540
559
541
node := & virtualFile {
560
- extractDir : extractDir ,
561
- layerDir : layerDir ,
562
542
virtualPath : runningDir ,
563
543
isWhiteout : false ,
564
544
mode : fs .ModeDir ,
@@ -569,7 +549,7 @@ func populateEmptyDirectoryNodes(virtualPath, layerDir, extractDir string, chain
569
549
570
550
// handleSymlink returns the symlink header mode. Symlinks are handled by creating a virtual file
571
551
// with the symlink mode with additional metadata.
572
- func (img * Image ) handleSymlink (virtualPath , layerDir string , header * tar.Header , isWhiteout bool ) (* virtualFile , error ) {
552
+ func (img * Image ) handleSymlink (virtualPath string , header * tar.Header , isWhiteout bool ) (* virtualFile , error ) {
573
553
targetPath := filepath .ToSlash (header .Linkname )
574
554
if targetPath == "" {
575
555
return nil , errors .New ("symlink header has no target path" )
@@ -586,8 +566,6 @@ func (img *Image) handleSymlink(virtualPath, layerDir string, header *tar.Header
586
566
}
587
567
588
568
return & virtualFile {
589
- extractDir : img .ExtractDir ,
590
- layerDir : layerDir ,
591
569
virtualPath : virtualPath ,
592
570
targetPath : targetPath ,
593
571
isWhiteout : isWhiteout ,
@@ -596,46 +574,23 @@ func (img *Image) handleSymlink(virtualPath, layerDir string, header *tar.Header
596
574
}
597
575
598
576
// handleDir creates the directory specified by path, if it doesn't exist.
599
- func (img * Image ) handleDir (virtualPath , layerDir string , header * tar.Header , isWhiteout bool ) (* virtualFile , error ) {
600
- realFilePath := filepath .Join (img .ExtractDir , layerDir , filepath .FromSlash (virtualPath ))
601
- if _ , err := img .root .Stat (filepath .Join (layerDir , filepath .FromSlash (virtualPath ))); err != nil {
602
- if err := os .MkdirAll (realFilePath , dirPermission ); err != nil {
603
- return nil , fmt .Errorf ("failed to create directory with realFilePath %s: %w" , realFilePath , err )
604
- }
605
- }
606
-
577
+ func (img * Image ) handleDir (virtualPath string , header * tar.Header , isWhiteout bool ) * virtualFile {
607
578
fileInfo := header .FileInfo ()
608
579
609
580
return & virtualFile {
610
- extractDir : img .ExtractDir ,
611
- layerDir : layerDir ,
612
581
virtualPath : virtualPath ,
613
582
isWhiteout : isWhiteout ,
614
583
mode : fileInfo .Mode () | fs .ModeDir ,
615
584
size : fileInfo .Size (),
616
585
modTime : fileInfo .ModTime (),
617
- }, nil
586
+ }
618
587
}
619
588
620
589
// handleFile creates the file specified by path, and then copies the contents of the tarReader into
621
590
// the file. The function returns a virtual file, which is meant to represent the file in a virtual
622
591
// filesystem.
623
- func (img * Image ) handleFile (virtualPath , layerDir string , tarReader * tar.Reader , header * tar.Header , isWhiteout bool ) (* virtualFile , error ) {
624
- realFilePath := filepath .Join (img .ExtractDir , layerDir , filepath .FromSlash (virtualPath ))
625
- parentDirectory := filepath .Dir (realFilePath )
626
- if err := os .MkdirAll (parentDirectory , dirPermission ); err != nil {
627
- return nil , fmt .Errorf ("failed to create parent directory %s: %w" , parentDirectory , err )
628
- }
629
-
630
- // Write all files as read/writable by the current user, inaccessible by anyone else. Actual
631
- // permission bits are stored in FileNode.
632
- f , err := img .root .OpenFile (filepath .Join (layerDir , filepath .FromSlash (virtualPath )), os .O_CREATE | os .O_RDWR , filePermission )
633
- if err != nil {
634
- return nil , err
635
- }
636
- defer f .Close ()
637
-
638
- numBytes , err := io .Copy (f , io .LimitReader (tarReader , img .config .MaxFileBytes ))
592
+ func (img * Image ) handleFile (virtualPath string , tarReader * tar.Reader , header * tar.Header , isWhiteout bool ) (* virtualFile , error ) {
593
+ numBytes , err := img .contentBlob .ReadFrom (io .LimitReader (tarReader , img .config .MaxFileBytes ))
639
594
if numBytes >= img .config .MaxFileBytes || errors .Is (err , io .EOF ) {
640
595
return nil , ErrFileReadLimitExceeded
641
596
}
@@ -644,16 +599,20 @@ func (img *Image) handleFile(virtualPath, layerDir string, tarReader *tar.Reader
644
599
return nil , fmt .Errorf ("unable to copy file: %w" , err )
645
600
}
646
601
602
+ // Record the offset of the file in the content blob before adding the new bytes. The offset is
603
+ // the current size of the content blob.
604
+ offset := img .size
605
+ // Update the image size with the number of bytes read into the content blob.
606
+ img .size += numBytes
647
607
fileInfo := header .FileInfo ()
648
608
649
609
return & virtualFile {
650
- extractDir : img .ExtractDir ,
651
- layerDir : layerDir ,
652
610
virtualPath : virtualPath ,
653
611
isWhiteout : isWhiteout ,
654
612
mode : fileInfo .Mode (),
655
- size : fileInfo .Size (),
656
613
modTime : fileInfo .ModTime (),
614
+ size : numBytes ,
615
+ reader : io .NewSectionReader (img .contentBlob , offset , numBytes ),
657
616
}, nil
658
617
}
659
618
0 commit comments