@@ -137,18 +137,33 @@ func cleanEnvForReproducibility() []string {
137137 return append (env , "LC_ALL=C" , "TZ=UTC" , "SOURCE_DATE_EPOCH=0" )
138138}
139139
140- // ExtraFile represents a file to be injected into the rootfs.
141- type ExtraFile struct {
142- HostPath string // Path on host filesystem
143- TarPath string // Path inside the tar/rootfs (e.g., "init" or "etc/config")
144- Mode os.FileMode // File mode (e.g., 0o755 for executables)
140+ // extraFile represents a file to be injected into the rootfs.
141+ type extraFile struct {
142+ HostPath string
143+ TarPath string
144+ Mode os.FileMode
145+ }
146+
147+ // normalizeHeader normalizes a tar header for reproducible builds.
148+ // Uses GNU format because sqfstar < 4.6 has a bug with PAX headers
149+ // that incorrectly strips pathname components in linkpath for symlinks.
150+ func normalizeHeader (header * tar.Header ) {
151+ header .Format = tar .FormatGNU
152+ header .Uid = 0
153+ header .Gid = 0
154+ header .Uname = ""
155+ header .Gname = ""
156+ header .ModTime = time .Unix (0 , 0 )
157+ header .AccessTime = time.Time {}
158+ header .ChangeTime = time.Time {}
159+ header .PAXRecords = nil
145160}
146161
147162// createSquashFsFromTar creates a squashfs filesystem by streaming a tar.bz2 template
148163// and injecting extra files, without extracting to disk.
149164//
150165// Returns the size of the created filesystem image in bytes.
151- func createSquashFsFromTar (buildEnv env.ExecEnv , outputFn , templateFn string , extraFiles []ExtraFile ) (int64 , error ) {
166+ func createSquashFsFromTar (buildEnv env.ExecEnv , outputFn , templateFn string , extraFiles []extraFile ) (int64 , error ) {
152167 const (
153168 sqfstarBin = "sqfstar"
154169 fakerootBin = "fakeroot"
@@ -216,114 +231,83 @@ func createSquashFsFromTar(buildEnv env.ExecEnv, outputFn, templateFn string, ex
216231 tarHasher := sha256 .New ()
217232 tarWriter := tar .NewWriter (io .MultiWriter (stdinPipe , tarHasher ))
218233
234+ // Ensure cleanup on error.
235+ var cmdErr error
236+ defer func () {
237+ if cmdErr != nil {
238+ tarWriter .Close ()
239+ stdinPipe .Close ()
240+ cmd .Wait () //nolint:errcheck
241+ }
242+ }()
243+
219244 // Build a map of extra files by their tar path for quick lookup.
220- extraFileMap := make (map [string ]ExtraFile )
245+ extraFileMap := make (map [string ]extraFile )
221246 for _ , ef := range extraFiles {
222247 extraFileMap [ef .TarPath ] = ef
223248 }
224249
225- // Copy entries from template, potentially skipping ones we'll replace.
250+ // Copy entries from template, skipping ones we'll replace.
226251 for {
227252 header , err := tarReader .Next ()
228253 if errors .Is (err , io .EOF ) {
229254 break
230255 }
231256 if err != nil {
232- tarWriter .Close ()
233- stdinPipe .Close ()
234- cmd .Wait () //nolint:errcheck
235- return 0 , fmt .Errorf ("error reading template archive: %w" , err )
236- }
237-
238- // Skip PAX/GNU special headers - these are metadata entries that tar.Reader
239- // normally handles internally, but if we see them we should not emit them.
240- // They have bodies that must be drained.
241- switch header .Typeflag {
242- case tar .TypeXHeader , tar .TypeXGlobalHeader , tar .TypeGNULongName , tar .TypeGNULongLink :
243- //nolint:gosec // G110: This is trusted input from our own template archives.
244- if _ , err := io .Copy (io .Discard , tarReader ); err != nil {
245- tarWriter .Close ()
246- stdinPipe .Close ()
247- cmd .Wait () //nolint:errcheck
248- return 0 , fmt .Errorf ("failed to skip special header content: %w" , err )
249- }
250- continue
257+ cmdErr = fmt .Errorf ("error reading template archive: %w" , err )
258+ return 0 , cmdErr
251259 }
252260
253- // Normalize the path (remove leading ./) .
261+ // Normalize the path.
254262 cleanPath := strings .TrimPrefix (header .Name , "./" )
255263
256264 // Skip if this path will be replaced by an extra file.
257265 if _ , willReplace := extraFileMap [cleanPath ]; willReplace {
258- // Skip this entry, we'll add the replacement later.
259- if header .Typeflag == tar .TypeReg || header .Typeflag == tar .TypeRegA { //nolint:staticcheck // TypeRegA for backward compat
260- // Drain the content.
261- //nolint:gosec // G110: This is trusted input from our own template archives.
262- if _ , err := io .Copy (io .Discard , tarReader ); err != nil {
263- tarWriter .Close ()
264- stdinPipe .Close ()
265- cmd .Wait () //nolint:errcheck
266- return 0 , fmt .Errorf ("failed to skip replaced file content: %w" , err )
267- }
268- }
269266 continue
270267 }
271268
272269 // Normalize header for reproducibility.
273- // Use GNU format because sqfstar < 4.6 has a bug with PAX headers
274- // that incorrectly strips pathname components in linkpath for symlinks.
275- header .Format = tar .FormatGNU
276- header .Uid = 0
277- header .Gid = 0
278- header .Uname = ""
279- header .Gname = ""
280- header .ModTime = time .Unix (0 , 0 )
281- header .AccessTime = time.Time {} // Zero value = not set
282- header .ChangeTime = time.Time {} // Zero value = not set
283- header .PAXRecords = nil
270+ normalizeHeader (header )
284271
285272 if err := tarWriter .WriteHeader (header ); err != nil {
286- tarWriter .Close ()
287- stdinPipe .Close ()
288- cmd .Wait () //nolint:errcheck
289- return 0 , fmt .Errorf ("failed to write tar header: %w" , err )
273+ cmdErr = fmt .Errorf ("failed to write tar header: %w" , err )
274+ return 0 , cmdErr
290275 }
291276
292- // Copy body for entry types that have content (regular files).
293- if header .Typeflag == tar .TypeReg || header .Typeflag == tar .TypeRegA { //nolint:staticcheck // TypeRegA for backward compat
294- //nolint:gosec // G110: This is trusted input from our own template archives.
295- if _ , err := io .Copy (tarWriter , tarReader ); err != nil {
296- tarWriter .Close ()
297- stdinPipe .Close ()
298- cmd .Wait () //nolint:errcheck
299- return 0 , fmt .Errorf ("failed to copy file content: %w" , err )
277+ // Copy body for regular files.
278+ if header .Typeflag == tar .TypeReg {
279+ const maxFileSize = 500 * 1024 * 1024 // 500 MB sanity limit
280+ if header .Size > maxFileSize {
281+ cmdErr = fmt .Errorf ("file too large: %s (%d bytes)" , header .Name , header .Size )
282+ return 0 , cmdErr
283+ }
284+ if _ , err := io .Copy (tarWriter , io .LimitReader (tarReader , header .Size )); err != nil {
285+ cmdErr = fmt .Errorf ("failed to copy file content: %w" , err )
286+ return 0 , cmdErr
300287 }
301288 }
302289 }
303290
304291 // Add extra files.
305292 for _ , ef := range extraFiles {
306293 if err := addFileToTar (tarWriter , ef .HostPath , ef .TarPath , ef .Mode ); err != nil {
307- tarWriter .Close ()
308- stdinPipe .Close ()
309- cmd .Wait () //nolint:errcheck
310- return 0 , fmt .Errorf ("failed to add extra file %s: %w" , ef .TarPath , err )
294+ cmdErr = fmt .Errorf ("failed to add extra file %s: %w" , ef .TarPath , err )
295+ return 0 , cmdErr
311296 }
312297 }
313298
314299 // Close tar writer and stdin pipe.
315300 if err := tarWriter .Close (); err != nil {
316- stdinPipe .Close ()
317- cmd .Wait () //nolint:errcheck
318- return 0 , fmt .Errorf ("failed to close tar writer: %w" , err )
301+ cmdErr = fmt .Errorf ("failed to close tar writer: %w" , err )
302+ return 0 , cmdErr
319303 }
320304
321305 // Print tar hash for verification.
322306 fmt .Printf ("TAR archive SHA256: %s\n " , hex .EncodeToString (tarHasher .Sum (nil )))
323307
324308 if err := stdinPipe .Close (); err != nil {
325- cmd . Wait () //nolint:errcheck
326- return 0 , fmt . Errorf ( "failed to close stdin pipe: %w" , err )
309+ cmdErr = fmt . Errorf ( "failed to close stdin pipe: %w" , err )
310+ return 0 , cmdErr
327311 }
328312
329313 // Wait for sqfstar to finish.
@@ -353,15 +337,12 @@ func addFileToTar(tw *tar.Writer, hostPath, tarPath string, mode os.FileMode) er
353337 }
354338
355339 header := & tar.Header {
356- Format : tar .FormatGNU ,
357340 Name : "./" + tarPath ,
358341 Mode : int64 (mode ),
359342 Size : fi .Size (),
360- Uid : 0 ,
361- Gid : 0 ,
362- ModTime : time .Unix (0 , 0 ),
363343 Typeflag : tar .TypeReg ,
364344 }
345+ normalizeHeader (header )
365346
366347 if err := tw .WriteHeader (header ); err != nil {
367348 return fmt .Errorf ("failed to write header: %w" , err )
0 commit comments