Files
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

233 lines
7.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import ContainerizationArchive
import ContainerizationEXT4
import Foundation
import SystemPackage
import Testing
struct EXT4WhiteoutTests {
private func makeTempFileURL(prefix: String) throws -> URL {
let base = FileManager.default.temporaryDirectory
let url = base.appendingPathComponent("\(prefix)-\(UUID().uuidString)")
return url
}
private func writeLayerWithOpaqueWhiteout(to url: URL) throws {
let writer = try ArchiveWriter(
format: .pax,
filter: .gzip,
file: url
)
let ts = Date()
let entry = WriteEntry()
entry.modificationDate = ts
entry.creationDate = ts
entry.owner = 0
entry.group = 0
entry.fileType = .directory
entry.permissions = 0o755
entry.path = "usr"
try writer.writeEntry(entry: entry, data: nil)
entry.path = "usr/local"
try writer.writeEntry(entry: entry, data: nil)
entry.path = "usr/local/bin"
try writer.writeEntry(entry: entry, data: nil)
entry.fileType = .regular
entry.permissions = 0o644
let fooData = Data("hello\n".utf8)
entry.path = "usr/local/bin/foo"
entry.size = Int64(fooData.count)
try writer.writeEntry(entry: entry, data: fooData)
entry.fileType = .regular
entry.permissions = 0o000
entry.size = 0
entry.path = "usr//.wh..wh..opq"
try writer.writeEntry(entry: entry, data: nil)
try writer.finishEncoding()
}
private func withFormatter<T>(
prefix: String = "ext4-whiteout",
blockSize: UInt32 = 4096,
minDiskSize: UInt64 = 16.mib(),
_ body: (EXT4.Formatter, FilePath) async throws -> T
) async throws -> T {
let imageURL = try makeTempFileURL(prefix: prefix)
let imagePath = FilePath(imageURL.path)
defer {
try? FileManager.default.removeItem(at: imageURL)
}
let formatter = try EXT4.Formatter(
imagePath,
blockSize: blockSize,
minDiskSize: minDiskSize
)
let result = try await body(formatter, imagePath)
return result
}
@Test
func unpack_with_opaque_whiteout_path_does_not_stack_overflow_and_cleans_directory() async throws {
let layerURL = try makeTempFileURL(prefix: "ext4-wh-layer")
defer {
try? FileManager.default.removeItem(at: layerURL)
}
try writeLayerWithOpaqueWhiteout(to: layerURL)
try await withFormatter { formatter, imagePath in
try await formatter.unpack(
source: FilePath(layerURL.path).url,
format: .pax,
compression: .gzip,
progress: nil
)
try formatter.close()
let reader = try EXT4.EXT4Reader(blockDevice: FilePath(imagePath.description))
#expect(reader.exists(FilePath("/usr/local/bin")) == false)
#expect(reader.exists(FilePath("/usr/local/bin/foo")) == false)
}
}
@Test
func directoryWhiteout_from_wh_opq_path_with_repeated_slashes_terminates() async throws {
try await withFormatter { formatter, _ in
try formatter.create(
path: FilePath("/usr"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/usr/local"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/usr/local/bin"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/usr/local/bin/foo"),
mode: EXT4.Inode.Mode(.S_IFREG, 0o644)
)
try formatter.create(
path: FilePath("/usr/local/bin/bar"),
mode: EXT4.Inode.Mode(.S_IFREG, 0o644)
)
let whiteoutEntry = FilePath("//usr//.wh..wh..opq")
let directoryToWhiteout = whiteoutEntry.dir
let normalized = directoryToWhiteout.lexicallyNormalized()
#expect(normalized == FilePath("/usr"))
try formatter.unlink(path: directoryToWhiteout, directoryWhiteout: true)
}
}
/// Test the exact recursion attack sequence:
/// create /_d
/// create symlink / -> /_
/// create /_
/// create symlink / -> /_
///
/// This creates a recursive symlink structure that can cause infinite recursion
/// during directory traversal operations.
@Test
func recursion_attack_sequence_does_not_cause_infinite_recursion() async throws {
await #expect(throws: EXT4.Formatter.Error.unsupportedFiletype) {
try await withFormatter { formatter, _ in
// Step 1: create /_d
try formatter.create(
path: FilePath("/_d"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
// Step 2: create symlink / -> /_
try formatter.create(
path: FilePath("/"),
link: FilePath("/_"),
mode: EXT4.Inode.Mode(.S_IFLNK, 0o777)
)
try formatter.create(
path: FilePath("/_"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/"),
link: FilePath("/_"),
mode: EXT4.Inode.Mode(.S_IFLNK, 0o777)
)
}
}
}
@Test
func file_whiteouts_and_directory_whiteouts_interact_correctly() async throws {
try await withFormatter { formatter, imagePath in
// Lowerlayer content
try formatter.create(
path: FilePath("/opt"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/opt/app"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/opt/app/cache"),
mode: EXT4.Inode.Mode(.S_IFDIR, 0o755)
)
try formatter.create(
path: FilePath("/opt/app/cache/file"),
mode: EXT4.Inode.Mode(.S_IFREG, 0o644)
)
try formatter.unlink(path: FilePath("/opt/app/cache/file"))
try formatter.unlink(
path: FilePath("/opt/app/cache"),
directoryWhiteout: true
)
try formatter.close()
let reader = try EXT4.EXT4Reader(blockDevice: FilePath(imagePath.description))
#expect(reader.exists(FilePath("/opt")))
#expect(reader.exists(FilePath("/opt/app")))
#expect(reader.exists(FilePath("/opt/app/cache")))
#expect(reader.exists(FilePath("/opt/app/cache/file")) == false)
}
}
}