zlayer-agent 0.13.0

Container runtime agent using libcontainer/youki
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! Translate OCI Windows layer tar entries into the hcsshim staging-dir
//! on-disk format consumed by `HcsImportLayer`.
//!
//! OCI Windows base layers (e.g. `mcr.microsoft.com/windows/nanoserver:ltsc2022`)
//! distribute file content + NTFS metadata as ordinary tar entries with the
//! NTFS bits carried in PAX extended headers (`MSWINDOWS.rawsd`,
//! `MSWINDOWS.reparse`, `MSWINDOWS.eas`, `MSWINDOWS.fileattr`).
//!
//! `HcsImportLayer` reads back each staged `Files\…` entry via hcsshim's
//! `legacyLayerReader.Next`, which expects the file to start with a 4-byte
//! little-endian `uint32` `FileAttributes` header followed by a sequence of
//! `WIN32_STREAM_ID`-framed `BACKUP_*` records (`BACKUP_SECURITY_DATA`,
//! `BACKUP_REPARSE_DATA`, `BACKUP_EA_DATA`, `BACKUP_DATA`). Those framing
//! bytes are persisted **verbatim** — they are NOT applied through `BackupWrite`
//! / `BackupRead`, since the kernel-side importer re-reads them itself.
//!
//! This module synthesises that on-disk format from a tar entry's body + PAX
//! headers and writes it to a regular file at the staging-dir destination,
//! mirroring the behaviour of hcsshim's `legacyLayerWriter.Add` in
//! `internal/wclayer/legacy.go`.

#![cfg(target_os = "windows")]

use std::io::{self, Read, Write};
use std::path::Path;

use base64::Engine as _;

/// `WIN32_STREAM_ID::dwStreamId` values, per
/// `winnt.h` (`BACKUP_*`). The set we synthesise is a subset of the full
/// list — see the Microsoft docs for the rest (`BACKUP_LINK`,
/// `BACKUP_PROPERTY_DATA`, `BACKUP_OBJECT_ID`, `BACKUP_TXFS_DATA`,
/// `BACKUP_ALTERNATE_DATA`). Those streams are not carried in OCI Windows
/// layer PAX headers, so we never emit them.
pub(crate) const BACKUP_DATA: u32 = 0x0000_0001;
pub(crate) const BACKUP_EA_DATA: u32 = 0x0000_0002;
pub(crate) const BACKUP_SECURITY_DATA: u32 = 0x0000_0003;
pub(crate) const BACKUP_REPARSE_DATA: u32 = 0x0000_0008;

/// Write a single `WIN32_STREAM_ID` header into `writer`.
///
/// The `WIN32_STREAM_ID` C struct is 20 bytes fixed prefix:
/// `dwStreamId(u32) | dwStreamAttributes(u32) | Size(i64) | dwStreamNameSize(u32)`,
/// followed by `dwStreamNameSize` bytes of UTF-16LE stream name (only used for
/// named alternate-data-streams). All three default-stream IDs we emit
/// (`BACKUP_DATA`, `BACKUP_SECURITY_DATA`, `BACKUP_REPARSE_DATA`,
/// `BACKUP_EA_DATA`) have an empty name, so `dwStreamNameSize` is always 0
/// and the trailing wide-char buffer is empty.
pub(crate) fn write_stream_header(
    writer: &mut impl Write,
    stream_id: u32,
    attributes: u32,
    size: u64,
) -> io::Result<()> {
    writer.write_all(&stream_id.to_le_bytes())?;
    writer.write_all(&attributes.to_le_bytes())?;
    writer.write_all(&size.to_le_bytes())?;
    writer.write_all(&0u32.to_le_bytes())?; // dwStreamNameSize = 0
    Ok(())
}

/// `FILE_ATTRIBUTE_ARCHIVE` — hcsshim's effective default for staged files
/// when no `MSWINDOWS.fileattr` PAX header is present.
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x0000_0020;

/// PAX metadata extracted from a tar entry's extended headers.
///
/// Each blob value is the **decoded** payload as written by the OCI layer
/// publisher (Microsoft's image build pipeline base64-encodes the raw
/// security descriptor / reparse blob / EA blob into the PAX header
/// value). `file_attributes` is parsed from decimal ASCII per the
/// `MSWINDOWS.fileattr` convention used by go-winio's `backuptar`.
#[derive(Default)]
struct PaxMetadata {
    security_descriptor: Option<Vec<u8>>,
    reparse_data: Option<Vec<u8>>,
    extended_attributes: Option<Vec<u8>>,
    file_attributes: Option<u32>,
}

/// Pull the MSWINDOWS.* PAX extensions off a tar entry, base64-decoding
/// each blob into raw bytes and parsing `MSWINDOWS.fileattr` as decimal
/// ASCII.
fn collect_pax_metadata<R: Read>(entry: &mut tar::Entry<'_, R>) -> io::Result<PaxMetadata> {
    let mut out = PaxMetadata::default();
    let Some(pax) = entry.pax_extensions()? else {
        return Ok(out);
    };
    let engine = base64::engine::general_purpose::STANDARD;
    for ext in pax {
        let ext = ext?;
        let key = ext.key().unwrap_or("");
        let val = ext.value_bytes();
        match key {
            "MSWINDOWS.rawsd" => {
                out.security_descriptor = Some(engine.decode(val).map_err(|e| {
                    io::Error::other(format!("PAX MSWINDOWS.rawsd base64 decode: {e}"))
                })?);
            }
            "MSWINDOWS.reparse" => {
                out.reparse_data = Some(engine.decode(val).map_err(|e| {
                    io::Error::other(format!("PAX MSWINDOWS.reparse base64 decode: {e}"))
                })?);
            }
            "MSWINDOWS.eas" => {
                out.extended_attributes = Some(engine.decode(val).map_err(|e| {
                    io::Error::other(format!("PAX MSWINDOWS.eas base64 decode: {e}"))
                })?);
            }
            "MSWINDOWS.fileattr" => {
                // Decimal ASCII per go-winio's `backuptar.WriteTarFileFromBackupStream` /
                // hcsshim's `legacyLayerWriter.Add` — e.g. "32" for FILE_ATTRIBUTE_ARCHIVE.
                let s = std::str::from_utf8(val)
                    .map_err(|e| io::Error::other(format!("PAX MSWINDOWS.fileattr utf8: {e}")))?;
                let n = s
                    .trim()
                    .parse::<u32>()
                    .map_err(|e| io::Error::other(format!("PAX MSWINDOWS.fileattr parse: {e}")))?;
                out.file_attributes = Some(n);
            }
            _ => {}
        }
    }
    Ok(out)
}

/// Writes the hcsshim staging-dir on-disk format
/// `[4-byte LE FileAttributes][BackupStream-framed records]` to a regular
/// file at `dest`.
///
/// This is NOT a `BackupWrite`-applied file — the framing bytes are
/// persisted verbatim because the kernel-side `HcsImportLayer` re-reads
/// them via hcsshim's `legacyLayerReader.Next`, which itself reads the
/// 4-byte `FileAttributes` header and then decodes the trailing bytes as
/// raw `WIN32_STREAM_ID` records.
///
/// On-disk layout written:
/// 1. 4 bytes LE `uint32` — `MSWINDOWS.fileattr` PAX value if present,
///    else `FILE_ATTRIBUTE_ARCHIVE` (0x20) per hcsshim's effective default.
/// 2. `BACKUP_SECURITY_DATA` framed record — if the entry carries
///    `MSWINDOWS.rawsd`.
/// 3. `BACKUP_REPARSE_DATA` framed record — if the entry carries
///    `MSWINDOWS.reparse`. For reparse points the file body is
///    conventionally empty; we still emit the data record below if
///    `size > 0` so we don't drop data.
/// 4. `BACKUP_EA_DATA` framed record — if the entry carries `MSWINDOWS.eas`.
/// 5. `BACKUP_DATA` framed record followed by `body_size` body bytes.
///    Skipped when the body is empty (zero-byte files and reparse-point
///    placeholders).
///
/// Sparse-block streams (`BACKUP_SPARSE_BLOCK`) are not currently
/// synthesised — the OCI Windows layer publishers materialise sparse
/// files as dense bodies in the tar, so the dense `BACKUP_DATA` record
/// is correct. Add sparse handling here if a future image carries an
/// `MSWINDOWS.sparse` PAX header.
///
/// # Errors
///
/// Returns the OS error from `CreateFileW`, any write failure, or a
/// base64/utf8 decode error on a malformed PAX value.
pub fn write_oci_entry_to_backup_stream<R: Read>(
    entry: &mut tar::Entry<'_, R>,
    dest: &Path,
) -> io::Result<()> {
    // Collect PAX metadata BEFORE the body — `pax_extensions()` snapshots
    // the entry's GNU/PAX headers and is safe to call before reading the
    // body. The body itself is consumed by the BACKUP_DATA copy below.
    let pax = collect_pax_metadata(entry)?;
    let body_size = entry.size();

    // Regular file via `\\?\` long-path-aware CreateFileW; the framing bytes
    // are written verbatim, NOT through BackupWrite.
    let mut writer = crate::windows::layer::create_long_path_file(dest)?;

    // 4-byte LE FileAttributes header expected by legacyLayerReader.Next.
    let attrs = pax.file_attributes.unwrap_or(FILE_ATTRIBUTE_ARCHIVE);
    writer.write_all(&attrs.to_le_bytes())?;

    if let Some(sd) = pax.security_descriptor {
        write_stream_header(&mut writer, BACKUP_SECURITY_DATA, 0, sd.len() as u64)?;
        writer.write_all(&sd)?;
    }

    if let Some(rp) = pax.reparse_data {
        write_stream_header(&mut writer, BACKUP_REPARSE_DATA, 0, rp.len() as u64)?;
        writer.write_all(&rp)?;
    }

    if let Some(eas) = pax.extended_attributes {
        write_stream_header(&mut writer, BACKUP_EA_DATA, 0, eas.len() as u64)?;
        writer.write_all(&eas)?;
    }

    if body_size > 0 {
        write_stream_header(&mut writer, BACKUP_DATA, 0, body_size)?;
        io::copy(entry, &mut writer)?;
    }

    Ok(())
}

/// Write a plain host file at `src` into the diff-layer staging format at
/// `dest` — the COPY/ADD analogue of [`write_oci_entry_to_backup_stream`] for
/// files that carry NO PAX NTFS metadata.
///
/// A `COPY`/`ADD` payload is materialised from the build context (or a download)
/// as an ordinary host file: it has no security descriptor, reparse, or EA
/// stream. So the faithful `legacyLayerWriter.Add` framing is simply the 4-byte
/// little-endian `FILE_ATTRIBUTE_ARCHIVE` header followed by a single
/// `BACKUP_DATA` record carrying the body (omitted for zero-byte files). This
/// is the same on-disk shape [`write_oci_entry_to_backup_stream`] emits for a
/// metadata-free OCI entry, but sourced from a host path rather than a
/// `tar::Entry`.
///
/// Handing the raw host file straight to `HcsImportLayer` (i.e. without this
/// framing) fails with `0x80070002` because `legacyLayerReader.Next` cannot
/// parse the missing 4-byte attribute header + stream records.
///
/// # Errors
///
/// Returns the OS error from opening `src`, creating `dest`, or any write.
pub fn write_host_file_to_backup_stream(src: &Path, dest: &Path) -> io::Result<()> {
    let body_size = std::fs::metadata(src)?.len();
    let mut reader = std::fs::File::open(src)?;

    // `\\?\`-prefixed long-path-aware creation, mirroring the OCI-entry writer.
    let mut writer = crate::windows::layer::create_long_path_file(dest)?;

    // 4-byte LE FileAttributes header expected by legacyLayerReader.Next. A
    // host COPY has no PAX `MSWINDOWS.fileattr`, so the ARCHIVE default applies
    // (identical to the OCI-entry writer's fallback).
    writer.write_all(&FILE_ATTRIBUTE_ARCHIVE.to_le_bytes())?;

    if body_size > 0 {
        write_stream_header(&mut writer, BACKUP_DATA, 0, body_size)?;
        io::copy(&mut reader, &mut writer)?;
    }

    Ok(())
}

/// Base-layer counterpart to [`write_oci_entry_to_backup_stream`].
///
/// hcsshim's `baseLayerWriter` (see `internal/wclayer/baselayerwriter.go`)
/// produces REAL NTFS files for base (parent-less) layers — the `BackupStream`
/// records are APPLIED via `BackupWrite` to the destination file rather than
/// persisted verbatim. The on-disk format is therefore:
///
/// 1. NO 4-byte `FileAttributes` header (the diff-layer staging-format prefix
///    is absent — the kernel walks the file metadata directly).
/// 2. `BACKUP_SECURITY_DATA` / `BACKUP_REPARSE_DATA` / `BACKUP_EA_DATA` /
///    `BACKUP_DATA` records written through [`BackupStreamWriter`], which
///    funnels them into `BackupWrite()` — the kernel stamps the security
///    descriptor, reparse data, EAs, and body onto the file directly.
///
/// The same PAX collection logic is reused; the only differences relative to
/// the diff-layer writer are the destination (`BackupStreamWriter` instead of
/// a regular file) and the omission of the 4-byte attr header.
///
/// # Errors
///
/// Returns the OS error from `CreateFileW` / `BackupWrite`, any write
/// failure, or a base64/utf8 decode error on a malformed PAX value.
pub fn write_oci_entry_as_base_layer<R: Read>(
    entry: &mut tar::Entry<'_, R>,
    dest: &Path,
) -> io::Result<()> {
    let pax = collect_pax_metadata(entry)?;
    let body_size = entry.size();

    // `BackupStreamWriter::create_new` materializes the empty file via the
    // long-path-aware helper, then re-opens it with FILE_FLAG_BACKUP_SEMANTICS
    // so subsequent writes flow through BackupWrite() — which stamps each
    // WIN32_STREAM_ID record onto the real NTFS file metadata.
    let mut writer = crate::windows::layer::BackupStreamWriter::create_new(dest)?;

    if let Some(sd) = pax.security_descriptor {
        write_stream_header(&mut writer, BACKUP_SECURITY_DATA, 0, sd.len() as u64)?;
        writer.write_all(&sd)?;
    }

    if let Some(rp) = pax.reparse_data {
        write_stream_header(&mut writer, BACKUP_REPARSE_DATA, 0, rp.len() as u64)?;
        writer.write_all(&rp)?;
    }

    if let Some(eas) = pax.extended_attributes {
        write_stream_header(&mut writer, BACKUP_EA_DATA, 0, eas.len() as u64)?;
        writer.write_all(&eas)?;
    }

    if body_size > 0 {
        write_stream_header(&mut writer, BACKUP_DATA, 0, body_size)?;
        io::copy(entry, &mut writer)?;
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
//
// The header serialisation can be exercised on any platform — we test it
// against a captured buffer rather than a real file handle. The
// `write_oci_entry_to_backup_stream` flow writes to a real `\\?\…`
// long-path-aware file, so it only runs on Windows hosts; the end-to-end
// flow is also exercised by the integration tests in
// `windows_hcs_hyperv_e2e.rs`.

#[cfg(test)]
mod tests {
    use super::*;

    /// Helper writer that captures the bytes the framing code emits, so
    /// we can golden-test the `WIN32_STREAM_ID` header layout without
    /// going through `BackupWrite`.
    struct Capture {
        buf: Vec<u8>,
    }

    impl Write for Capture {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            self.buf.extend_from_slice(buf);
            Ok(buf.len())
        }
        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }

    #[test]
    fn header_layout_is_20_bytes_le_with_zero_name_size() {
        let mut cap = Capture { buf: Vec::new() };
        write_stream_header(&mut cap, BACKUP_DATA, 0, 0xAA_BB_CC_DD).unwrap();
        assert_eq!(cap.buf.len(), 20, "WIN32_STREAM_ID prefix is 20 bytes");

        // dwStreamId
        assert_eq!(&cap.buf[0..4], &1u32.to_le_bytes());
        // dwStreamAttributes
        assert_eq!(&cap.buf[4..8], &0u32.to_le_bytes());
        // Size (LARGE_INTEGER, little-endian u64)
        assert_eq!(&cap.buf[8..16], &0xAA_BB_CC_DDu64.to_le_bytes());
        // dwStreamNameSize
        assert_eq!(&cap.buf[16..20], &0u32.to_le_bytes());
    }

    #[test]
    fn header_encodes_security_stream_id() {
        let mut cap = Capture { buf: Vec::new() };
        write_stream_header(&mut cap, BACKUP_SECURITY_DATA, 0, 16).unwrap();
        assert_eq!(&cap.buf[0..4], &3u32.to_le_bytes());
        assert_eq!(&cap.buf[8..16], &16u64.to_le_bytes());
    }

    #[test]
    fn header_encodes_reparse_stream_id() {
        let mut cap = Capture { buf: Vec::new() };
        write_stream_header(&mut cap, BACKUP_REPARSE_DATA, 0, 24).unwrap();
        assert_eq!(&cap.buf[0..4], &8u32.to_le_bytes());
        assert_eq!(&cap.buf[8..16], &24u64.to_le_bytes());
    }

    #[test]
    fn header_encodes_ea_stream_id() {
        let mut cap = Capture { buf: Vec::new() };
        write_stream_header(&mut cap, BACKUP_EA_DATA, 0, 7).unwrap();
        assert_eq!(&cap.buf[0..4], &2u32.to_le_bytes());
        assert_eq!(&cap.buf[8..16], &7u64.to_le_bytes());
    }

    /// Build a minimal tar with a PAX `MSWINDOWS.fileattr=32` extension
    /// followed by a regular `Files/test.txt` entry whose body is `b"hi"`,
    /// then drive `write_oci_entry_to_backup_stream` against a temp file
    /// and assert the on-disk layout matches the hcsshim staging-dir format
    /// `[4-byte LE FileAttributes][BACKUP_DATA WIN32_STREAM_ID][body]`.
    #[test]
    fn write_oci_entry_writes_attr_header_then_body_for_minimal_file() {
        // 1. Synthesise the tar in memory.
        let mut tar_bytes: Vec<u8> = Vec::new();
        {
            let mut builder = tar::Builder::new(&mut tar_bytes);
            // PAX extended header: MSWINDOWS.fileattr = "32" (0x20 archive).
            builder
                .append_pax_extensions([("MSWINDOWS.fileattr", b"32".as_slice())])
                .unwrap();
            // Regular file entry.
            let body: &[u8] = b"hi";
            let mut header = tar::Header::new_ustar();
            header.set_path("Files/test.txt").unwrap();
            header.set_size(body.len() as u64);
            header.set_entry_type(tar::EntryType::Regular);
            header.set_mode(0o644);
            header.set_cksum();
            builder.append(&header, body).unwrap();
            builder.finish().unwrap();
        }

        // 2. Walk the tar to the regular file entry and run the writer.
        let dir = tempfile::tempdir().unwrap();
        let dest = dir.path().join("test.txt");
        {
            let mut archive = tar::Archive::new(io::Cursor::new(&tar_bytes));
            let mut entries = archive.entries().unwrap();
            let mut entry = loop {
                let e = entries
                    .next()
                    .expect("expected a regular file entry after the PAX header")
                    .unwrap();
                if e.header().entry_type() == tar::EntryType::Regular {
                    break e;
                }
            };
            write_oci_entry_to_backup_stream(&mut entry, &dest).unwrap();
        }

        // 3. Assert the on-disk layout.
        let out = std::fs::read(&dest).unwrap();
        assert_eq!(
            out.len(),
            4 + 20 + 2,
            "attr(4) + WIN32_STREAM_ID(20) + body(2)"
        );
        // 4-byte FileAttributes header = 0x20 LE.
        assert_eq!(&out[0..4], &[0x20, 0x00, 0x00, 0x00]);
        // BACKUP_DATA WIN32_STREAM_ID prefix: id=1, attrs=0, size=2, nameSize=0.
        assert_eq!(&out[4..8], &1u32.to_le_bytes());
        assert_eq!(&out[8..12], &0u32.to_le_bytes());
        assert_eq!(&out[12..20], &2u64.to_le_bytes());
        assert_eq!(&out[20..24], &0u32.to_le_bytes());
        // Body bytes.
        assert_eq!(&out[24..26], b"hi");
    }
}