Impact
An attacker who can supply a container image layer for a victim to unpack — via a malicious or compromised registry, a poisoned base image, or a pull request that changes which image gets fetched — can crash the process performing the unpack with no privileges beyond getting the victim to pull or run the crafted image reference.
The trigger is a single tar entry carrying an extended-attribute (xattr) name longer than 255 bytes. When ContainerizationEXT4 writes that entry into the ext4 rootfs it is building, an unchecked UInt8 conversion of the name length traps and aborts the process.
Details
EXT4.FileXattrsState.write() writes each xattr entry's name length into a single on-disk byte via an unchecked UInt8 conversion:
// Sources/ContainerizationEXT4/EXT4+Xattrs.swift
out.append(UInt8(attribute.name.count)) // traps if attribute.name.count > 255
Swift's plain UInt8.init(_:) traps at runtime (aborting the process) when the source value does not fit in 8 bits.
attribute.name is the xattr name taken from a tar entry's PAX extended header (SCHILY.xattr.*) with no length limit, after ExtendedAttribute.compressName() strips a known namespace prefix, it flows into EXT4.Formatter.create(...), which calls state.writeInlineAttributes(...) / state.writeBlockAttributes(...), both reaching the conversion above with no length validation along the path.
255 bytes is the maximum length of a single xattr name on a real Linux filesystem, so a layer produced by tarring an actual directory tree can never contain a name this long. The tar format itself imposes no such limit, so a deliberately crafted archive (one that was never the tar of a real filesystem) can contain one.
Affected code
EXT4.FileXattrsState.write() in Sources/ContainerizationEXT4/EXT4+Xattrs.swift — fixed to reject a name whose length is 0 or exceeds UInt8.max (255), throwing EXT4.FileXattrsState.Error.invalidXAttr instead of trapping.
- The reachable callers
writeInlineAttributes(...) / writeBlockAttributes(...), invoked from EXT4.Formatter.create(...). Any consumer of ContainerizationEXT4 that formats an ext4 filesystem from untrusted tar/OCI layer input reaches the same conversion.
Mitigations
- Upgrade to the version of
apple/containerization containing the fix.
- Do not unpack, pull, or run container images from untrusted sources — the registry, base image, or CI step that supplies a layer is part of the trust boundary, not just the resulting container's contents.
- In CI/CD, avoid pulling or running an image reference that a pull request or other external input can influence on a runner shared with other jobs, since the crash disrupts every concurrent image operation on that host.
Verifying whether you are affected
Inspect a layer tar for any xattr name that would overflow the single length byte before unpacking it. The only reliable check enumerates xattrs through the same library the unpacker uses (ContainerizationArchive, a libarchive wrapper) and applies the same prefix compression (EXT4.ExtendedAttribute.compressName) that feeds the conversion. This catches both PAX xattr encodings libarchive recognizes (SCHILY.xattr. and LIBARCHIVE.xattr., the latter with URL-encoded names) and measures the exact post-compression length that the vulnerable UInt8(...) sees. A plain tarfile/tar scan for long SCHILY.xattr. keys is only an approximation — it misses LIBARCHIVE.xattr.-encoded names and measures the raw, pre-compression length.
Create a Swift project directory and populate it with the following files:
// Sources/main.swift
import Foundation
import ContainerizationArchive
import ContainerizationEXT4
for path in CommandLine.arguments.dropFirst() {
let reader = try ArchiveReader(file: URL(fileURLWithPath: path)) // auto-detects gzip/zstd/etc.
for (entry, _) in reader {
for (rawName, _) in entry.xattrs {
let name = EXT4.ExtendedAttribute.compressName(rawName).str
if name.isEmpty || name.utf8.count > 255 {
print("\(path): \(entry.path ?? "?"): xattr '\(rawName)' -> \(name.utf8.count) bytes")
}
}
}
}
// Package.swift
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "xattr-check",
platforms: [.macOS("15.0")],
dependencies: [
.package(url: "https://github.com/apple/containerization.git", branch: "main")
],
targets: [
.executableTarget(
name: "xattr-check",
dependencies: [
.product(name: "ContainerizationArchive", package: "containerization"),
.product(name: "ContainerizationEXT4", package: "containerization"),
]
)
]
)
Run the test program against each layer tar file:
swift run xattr-check layer.tar [more-layers.tar ...]
Any output indicates an entry that would trigger the trap on an affected version.
References
Fix: #898, commit 2a331164e9e42fec6192978a153831a0eb9e7ab4
Impact
An attacker who can supply a container image layer for a victim to unpack — via a malicious or compromised registry, a poisoned base image, or a pull request that changes which image gets fetched — can crash the process performing the unpack with no privileges beyond getting the victim to pull or run the crafted image reference.
The trigger is a single tar entry carrying an extended-attribute (xattr) name longer than 255 bytes. When
ContainerizationEXT4writes that entry into the ext4 rootfs it is building, an uncheckedUInt8conversion of the name length traps and aborts the process.Details
EXT4.FileXattrsState.write()writes each xattr entry's name length into a single on-disk byte via an uncheckedUInt8conversion:Swift's plain
UInt8.init(_:)traps at runtime (aborting the process) when the source value does not fit in 8 bits.attribute.nameis the xattr name taken from a tar entry's PAX extended header (SCHILY.xattr.*) with no length limit, afterExtendedAttribute.compressName()strips a known namespace prefix, it flows intoEXT4.Formatter.create(...), which callsstate.writeInlineAttributes(...)/state.writeBlockAttributes(...), both reaching the conversion above with no length validation along the path.255 bytes is the maximum length of a single xattr name on a real Linux filesystem, so a layer produced by tarring an actual directory tree can never contain a name this long. The tar format itself imposes no such limit, so a deliberately crafted archive (one that was never the tar of a real filesystem) can contain one.
Affected code
EXT4.FileXattrsState.write()inSources/ContainerizationEXT4/EXT4+Xattrs.swift— fixed to reject a name whose length is0or exceedsUInt8.max(255), throwingEXT4.FileXattrsState.Error.invalidXAttrinstead of trapping.writeInlineAttributes(...)/writeBlockAttributes(...), invoked fromEXT4.Formatter.create(...). Any consumer ofContainerizationEXT4that formats an ext4 filesystem from untrusted tar/OCI layer input reaches the same conversion.Mitigations
apple/containerizationcontaining the fix.Verifying whether you are affected
Inspect a layer tar for any xattr name that would overflow the single length byte before unpacking it. The only reliable check enumerates xattrs through the same library the unpacker uses (
ContainerizationArchive, a libarchive wrapper) and applies the same prefix compression (EXT4.ExtendedAttribute.compressName) that feeds the conversion. This catches both PAX xattr encodings libarchive recognizes (SCHILY.xattr.andLIBARCHIVE.xattr., the latter with URL-encoded names) and measures the exact post-compression length that the vulnerableUInt8(...)sees. A plaintarfile/tarscan for longSCHILY.xattr.keys is only an approximation — it missesLIBARCHIVE.xattr.-encoded names and measures the raw, pre-compression length.Create a Swift project directory and populate it with the following files:
Run the test program against each layer tar file:
Any output indicates an entry that would trigger the trap on an affected version.
References
Fix: #898, commit 2a331164e9e42fec6192978a153831a0eb9e7ab4