@@ -59,6 +59,34 @@ const (
5959 // create uniquely-named files, but we don't want to try to use their
6060 // contents until after they've been written to
6161 containerExcludesSubstring = ".tmp"
62+
63+ // Windows-specific PAX record keys
64+ keyFileAttr = "MSWINDOWS.fileattr"
65+ keySDRaw = "MSWINDOWS.rawsd"
66+ keyCreationTime = "LIBARCHIVE.creationtime"
67+
68+ // Windows Security Descriptors (base64-encoded)
69+ // SDDL: O:BAG:SYD:(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;CO)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CI;DC;;;BU)
70+ // Owner: Built-in Administrators (BA)
71+ // Group: Local System (SY)
72+ // DACL:
73+ // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Built-in Administrators (BA)
74+ // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Full Access to Local System (SY)
75+ // - Allow Full Access to Built-in Administrators (BA)
76+ // - Allow OBJECT_INHERIT+CONTAINER_INHERIT+INHERIT_ONLY Generic All to Creator Owner (CO)
77+ // - Allow OBJECT_INHERIT+CONTAINER_INHERIT Read/Execute permissions to Built-in Users (BU)
78+ // - Allow CONTAINER_INHERIT List Contents to Built-in Users (BU)
79+ // - Allow CONTAINER_INHERIT Delete Child to Built-in Users (BU)
80+ winSecurityDescriptorDirectory = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgCoAAcAAAAAAxgA/wEfAAECAAAAAAAFIAAAACACAAAAAxQA/wEfAAEBAAAAAAAFEgAAAAAAGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAALFAAAAAAQAQEAAAAAAAMAAAAAAAMYAKkAEgABAgAAAAAABSAAAAAhAgAAAAIYAAQAAAABAgAAAAAABSAAAAAhAgAAAAIYAAIAAAABAgAAAAAABSAAAAAhAgAA"
81+
82+ // SDDL: O:BAG:SYD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;0x1200a9;;;BU)
83+ // Owner: Built-in Administrators (BA)
84+ // Group: Local System (SY)
85+ // DACL:
86+ // - Allow Full Access to Built-in Administrators (BA)
87+ // - Allow Full Access to Local System (SY)
88+ // - Allow Read/Execute permissions to Built-in Users (BU)
89+ winSecurityDescriptorFile = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgBMAAMAAAAAABgA/wEfAAECAAAAAAAFIAAAACACAAAAABQA/wEfAAEBAAAAAAAFEgAAAAAAGACpABIAAQIAAAAAAAUgAAAAIQIAAA=="
6290)
6391
6492// ExtractRootfsOptions is consumed by ExtractRootfs() which allows users to
@@ -112,6 +140,7 @@ type containerImageRef struct {
112140 unsetAnnotations []string
113141 setAnnotations []string
114142 createdAnnotation types.OptionalBool
143+ os string
115144}
116145
117146type blobLayerInfo struct {
@@ -1075,7 +1104,7 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
10751104 return nil , fmt .Errorf ("opening file for %s: %w" , what , err )
10761105 }
10771106
1078- layerFileWriter , err := newLayerWriter (layerFile , i .compression , i .layerModTime , i .layerLatestModTime , layerExclusions )
1107+ layerFileWriter , err := newLayerWriter (layerFile , i .compression , i .layerModTime , i .layerLatestModTime , layerExclusions , i . os == "windows" )
10791108 if err != nil {
10801109 layerFile .Close ()
10811110 rc .Close ()
@@ -1097,7 +1126,9 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
10971126 return nil , fmt .Errorf ("extracting container rootfs: %w" , err )
10981127 }
10991128 }
1100- if layerFileWriter .SourceDigest () != layerFileWriter .DestDigest () {
1129+ // If the stream was transformed (compression or Windows mutation), use TotalWritten()
1130+ // Otherwise verify that io.Copy size matches TotalWritten()
1131+ if layerFileWriter .SourceDigest () != layerFileWriter .DestDigest () || i .os == "windows" {
11011132 size = layerFileWriter .TotalWritten ()
11021133 } else {
11031134 if size != layerFileWriter .TotalWritten () {
@@ -1143,6 +1174,113 @@ func (i *containerImageRef) NewImageSource(ctx context.Context, _ *types.SystemC
11431174 return src , nil
11441175}
11451176
1177+ func prepareWinHeader (h * tar.Header ) {
1178+ if h .PAXRecords == nil {
1179+ h .PAXRecords = map [string ]string {}
1180+ }
1181+ if h .Typeflag == tar .TypeDir {
1182+ h .Mode |= 1 << 14
1183+ h .PAXRecords [keyFileAttr ] = "16"
1184+ }
1185+
1186+ if h .Typeflag == tar .TypeReg {
1187+ h .Mode |= 1 << 15
1188+ h .PAXRecords [keyFileAttr ] = "32"
1189+ }
1190+
1191+ if ! h .ModTime .IsZero () {
1192+ h .PAXRecords [keyCreationTime ] = fmt .Sprintf ("%d.%d" , h .ModTime .Unix (), h .ModTime .Nanosecond ())
1193+ }
1194+
1195+ h .Format = tar .FormatPAX
1196+ }
1197+
1198+ func addSecurityDescriptor (h * tar.Header ) {
1199+ if h .Typeflag == tar .TypeDir {
1200+ h .PAXRecords [keySDRaw ] = winSecurityDescriptorDirectory
1201+ }
1202+
1203+ if h .Typeflag == tar .TypeReg {
1204+ h .PAXRecords [keySDRaw ] = winSecurityDescriptorFile
1205+ }
1206+ }
1207+
1208+ // winMutator implements io.WriteCloser and transforms an incoming tar stream to fit the expected format of a Windows
1209+ // container image
1210+ type winMutator struct {
1211+ pw * io.PipeWriter
1212+ done chan error
1213+ }
1214+
1215+ func (w * winMutator ) Write (b []byte ) (int , error ) {
1216+ return w .pw .Write (b )
1217+ }
1218+
1219+ func (w * winMutator ) Close () error {
1220+ if err := w .pw .Close (); err != nil {
1221+ return fmt .Errorf ("error closing pipewriter" )
1222+ }
1223+ return <- w .done
1224+ }
1225+
1226+ func newWindowsMutator (outStream io.WriteCloser ) * winMutator {
1227+ pr , pw := io .Pipe ()
1228+ done := make (chan error )
1229+ go func () {
1230+ tarReader := tar .NewReader (pr )
1231+ tarWriter := tar .NewWriter (outStream )
1232+
1233+ err := func () error {
1234+ h := & tar.Header {
1235+ Name : "Hives" ,
1236+ Typeflag : tar .TypeDir ,
1237+ ModTime : time .Now (),
1238+ }
1239+ prepareWinHeader (h )
1240+ if err := tarWriter .WriteHeader (h ); err != nil {
1241+ return err
1242+ }
1243+
1244+ h = & tar.Header {
1245+ Name : "Files" ,
1246+ Typeflag : tar .TypeDir ,
1247+ ModTime : time .Now (),
1248+ }
1249+ prepareWinHeader (h )
1250+ if err := tarWriter .WriteHeader (h ); err != nil {
1251+ return err
1252+ }
1253+
1254+ for {
1255+ h , err := tarReader .Next ()
1256+ if err == io .EOF {
1257+ break
1258+ }
1259+ if err != nil {
1260+ return err
1261+ }
1262+ h .Name = filepath .Join ("Files" , h .Name )
1263+ if h .Linkname != "" {
1264+ h .Linkname = filepath .Join ("Files" , h .Linkname )
1265+ }
1266+ prepareWinHeader (h )
1267+ addSecurityDescriptor (h )
1268+ if err := tarWriter .WriteHeader (h ); err != nil {
1269+ return err
1270+ }
1271+ if h .Size > 0 {
1272+ if _ , err := io .Copy (tarWriter , tarReader ); err != nil {
1273+ return err
1274+ }
1275+ }
1276+ }
1277+ return tarWriter .Close ()
1278+ }()
1279+ done <- err
1280+ }()
1281+ return & winMutator {pw : pw , done : done }
1282+ }
1283+
11461284// layerWriter represents a pipeline of writers
11471285type layerWriter struct {
11481286 outputCounter * int64
@@ -1189,15 +1327,15 @@ func (l *layerWriter) TotalWritten() int64 {
11891327// newLayerWriter creates a writer pipeline which processes an input stream and ultimately writes to the given destination.
11901328// The write stream pipeline is as follows:
11911329// input -> filter -> compressor -> destination
1192- func newLayerWriter (destination io.Writer , compression archive.Compression , layerModTime , layerLatestModTime * time.Time , layerExclusions []copier.ConditionalRemovePath ) (* layerWriter , error ) {
1330+ func newLayerWriter (destination io.Writer , compression archive.Compression , layerModTime , layerLatestModTime * time.Time , layerExclusions []copier.ConditionalRemovePath , windows bool ) (* layerWriter , error ) {
11931331 layerWriteCounter := ioutils .NewWriteCounter (destination )
11941332 var multiWriter io.Writer
11951333 var destHasher digest.Digester
11961334 srcHasher := digest .Canonical .Digester ()
11971335 var closers []func () error
11981336
11991337 // If the input stream will not be different from the output stream, avoid rehashing
1200- if compression != archive .Uncompressed || layerModTime != nil || layerLatestModTime != nil || len (layerExclusions ) != 0 {
1338+ if windows || compression != archive .Uncompressed || layerModTime != nil || layerLatestModTime != nil || len (layerExclusions ) != 0 {
12011339 destHasher = digest .Canonical .Digester ()
12021340 multiWriter = io .MultiWriter (layerWriteCounter , destHasher .Hash ())
12031341 } else {
@@ -1212,9 +1350,17 @@ func newLayerWriter(destination io.Writer, compression archive.Compression, laye
12121350 closers = append (closers , compressor .Close )
12131351 compressorHasher := io .MultiWriter (compressor , srcHasher .Hash ())
12141352
1353+ // Apply Windows layer mutation if needed, before calculating the uncompressed hash
1354+ preCompressor := ioutils .NopWriteCloser (compressorHasher )
1355+ if windows {
1356+ // preCompressor.Close is owned by makeFilteredLayerWriteCloser
1357+ preCompressor = newWindowsMutator (preCompressor )
1358+ }
1359+
12151360 // Use specified timestamps in the layer, if we're doing that for history entries.
1216- filter := makeFilteredLayerWriteCloser (ioutils . NopWriteCloser ( compressorHasher ) , layerModTime , layerLatestModTime , layerExclusions )
1361+ filter := makeFilteredLayerWriteCloser (preCompressor , layerModTime , layerLatestModTime , layerExclusions )
12171362 closers = append (closers , filter .Close )
1363+
12181364 return & layerWriter {
12191365 outputCounter : & layerWriteCounter .Count ,
12201366 inputDigester : srcHasher ,
@@ -1720,6 +1866,7 @@ func (b *Builder) makeContainerImageRef(options CommitOptions) (*containerImageR
17201866 layerMountTargets : layerMountTargets ,
17211867 layerPullUps : layerPullUps ,
17221868 createdAnnotation : options .CreatedAnnotation ,
1869+ os : b .OCIv1 .OS ,
17231870 }
17241871 if ref .created != nil {
17251872 for i := range ref .preEmptyLayers {
0 commit comments