Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Sources/ContainerizationArchive/ArchiveReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,16 @@ extension ArchiveReader {
}

private func setFileAttributes(fd: Int32, entry: WriteEntry) {
fchmod(fd, entry.permissions & 0o777)
// Ownership must be set before the permission mode: Darwin's fchown clears
// the set-user-ID and set-group-ID bits, even when chowning to the file's
// existing owner/group, so applying it after fchmod would silently drop
// those bits.
if let owner = entry.owner, let group = entry.group {
fchown(fd, owner, group)
}
// Mask to the POSIX permission field (0o7777: owner/group/other rwx plus
// set-user-ID, set-group-ID, and sticky), excluding unrelated file-type bits.
fchmod(fd, entry.permissions & 0o7777)
}

private static func copyDataReaderToFd(dataReader: ArchiveEntryReader, fileFd: Int32, memberPath: FilePath) throws {
Expand Down
53 changes: 53 additions & 0 deletions Tests/ContainerizationArchiveTests/ArchiveReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,59 @@ struct ArchiveReaderTests {
#expect((perms & permMask) == 0o755, "Permissions should be preserved")
}

@Test func preserveSpecialPermissionBits() throws {
// Extraction must preserve the sticky/set-user-ID/set-group-ID bits, and
// ownership must be applied before the permission mode: Darwin's fchown
// clears set-user-ID/set-group-ID even when chowning a file to its own
// existing owner/group. Use the current process's real uid/gid so the
// fchown below is permitted without requiring root.
let uid = getuid()
let gid = getgid()

let testDirectory = createTemporaryDirectory(baseName: "ArchiveReaderTests")!
let archiveURL = testDirectory.appendingPathComponent("special-perms.tar")
let archiver = try ArchiveWriter(format: .paxRestricted, filter: .none, file: archiveURL)

let dirEntry = WriteEntry()
dirEntry.path = "sticky-dir"
dirEntry.fileType = .directory
dirEntry.permissions = 0o1777
dirEntry.owner = uid
dirEntry.group = gid
dirEntry.size = 0
try archiver.writeEntry(entry: dirEntry, data: nil)

let fileEntry = WriteEntry()
fileEntry.path = "setid-file"
fileEntry.fileType = .regular
fileEntry.permissions = 0o6755
fileEntry.owner = uid
fileEntry.group = gid
let data = Data("setuid/setgid content".utf8)
fileEntry.size = numericCast(data.count)
try archiver.writeEntry(entry: fileEntry, data: data)

try archiver.finishEncoding()
defer { try? FileManager.default.removeItem(at: testDirectory) }

let extractDir = try createExtractionDirectory(name: "special-perms")
defer { try? FileManager.default.removeItem(at: extractDir.deletingLastPathComponent()) }

let reader = try ArchiveReader(format: .paxRestricted, filter: .none, file: archiveURL)
let rejectedPaths = try reader.extractContents(to: extractDir)
#expect(rejectedPaths.isEmpty)

let permMask: UInt16 = 0o7777

let dirAttrs = try FileManager.default.attributesOfItem(atPath: extractDir.appendingPathComponent("sticky-dir").path)
let dirPerms = (dirAttrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0
#expect((dirPerms & permMask) == 0o1777, "Sticky bit should be preserved on directories")

let fileAttrs = try FileManager.default.attributesOfItem(atPath: extractDir.appendingPathComponent("setid-file").path)
let filePerms = (fileAttrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0
#expect((filePerms & permMask) == 0o6755, "Set-ID bits should be preserved after ownership is applied")
}

// MARK: - Duplicate Entry Tests

@Test func duplicateRegularFiles() throws {
Expand Down