diff --git a/Sources/ContainerizationEXT4/EXT4+Formatter.swift b/Sources/ContainerizationEXT4/EXT4+Formatter.swift index 03bd304c0..6d2cabd7c 100644 --- a/Sources/ContainerizationEXT4/EXT4+Formatter.swift +++ b/Sources/ContainerizationEXT4/EXT4+Formatter.swift @@ -1434,6 +1434,7 @@ extension EXT4 { case internalInconsistency(_ message: String) case invalidBlockSize(_ size: UInt32) case journalTooSmall(_ size: UInt64) + case journalTooLarge(_ size: UInt64) public var description: String { switch self { case .notDirectory(let path): @@ -1472,7 +1473,10 @@ extension EXT4 { return "invalid block size \(size): must be 1024, 2048, or 4096" case .journalTooSmall(let size): return "requested journal size \(size) bytes is too small; minimum is \(EXT4.MinJournalBlocks) blocks (JBD2_MIN_JOURNAL_BLOCKS)" + case .journalTooLarge(let size): + return "requested journal size \(size) bytes is too large" } + } } diff --git a/Sources/ContainerizationEXT4/EXT4+Journal.swift b/Sources/ContainerizationEXT4/EXT4+Journal.swift index 5e3511f6a..4c1179731 100644 --- a/Sources/ContainerizationEXT4/EXT4+Journal.swift +++ b/Sources/ContainerizationEXT4/EXT4+Journal.swift @@ -35,9 +35,16 @@ extension EXT4.Formatter { try self.seek(block: self.currentBlock + 1) } let journalStartBlock = self.currentBlock + let journalEndBlock = try calculateJournalEndBlock( + startBlock: journalStartBlock, + blockCount: journalBlocks + ) try writeJournalSuperblock(journalBlocks: journalBlocks, filesystemUUID: filesystemUUID) - try zeroJournalBlocks(count: journalBlocks - 1) - try setupJournalInode(startBlock: journalStartBlock, blockCount: journalBlocks) + try skipJournalBlocks(count: journalBlocks - 1) + try setupJournalInode( + startBlock: journalStartBlock, + endBlock: journalEndBlock + ) return journalBlocks } @@ -51,8 +58,10 @@ extension EXT4.Formatter { guard blocks >= EXT4.MinJournalBlocks else { throw EXT4.Formatter.Error.journalTooSmall(size) } - // Safe: any journal large enough to overflow UInt32 (>16 TiB at 4 KiB block size) - // would fail at the I/O layer before this conversion is reached. + // Safe: blocks is guaranteed to be at least EXT4.MinJournalBlocks and at most UInt32.max. + guard blocks <= UInt64(UInt32.max) else { + throw EXT4.Formatter.Error.journalTooLarge(size) + } return UInt32(blocks) } // Default sizing: scale with the usable content area, with a floor determined by @@ -124,26 +133,19 @@ extension EXT4.Formatter { try self.handle.write(contentsOf: buf) } - private func zeroJournalBlocks(count: UInt32) throws { + private func skipJournalBlocks(count: UInt32) throws { guard count > 0 else { return } - let chunkSize = 1.mib() - // Safe: both operands are UInt32, so their product peaks at ~17 TiB, which fits - // in Int64 (the width of Int on all 64-bit Apple platforms). - let totalBytes = Int(count) * Int(self.blockSize) - let zeroBuf = [UInt8](repeating: 0, count: min(Int(chunkSize), totalBytes)) - var remaining = totalBytes - while remaining > 0 { - let toWrite = min(zeroBuf.count, remaining) - try self.handle.write(contentsOf: zeroBuf[0.. UInt32 { + let (endBlock, overflow) = startBlock.addingReportingOverflow(blockCount) + guard !overflow else { + throw EXT4.Formatter.Error.journalTooLarge(UInt64(blockCount) * UInt64(self.blockSize)) + } + return endBlock + } + func journalInodeBlockBackup() -> ( UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, diff --git a/Tests/ContainerizationEXT4Tests/TestEXT4Journal.swift b/Tests/ContainerizationEXT4Tests/TestEXT4Journal.swift index 837032461..54694c518 100644 --- a/Tests/ContainerizationEXT4Tests/TestEXT4Journal.swift +++ b/Tests/ContainerizationEXT4Tests/TestEXT4Journal.swift @@ -163,4 +163,32 @@ struct JournalOverflowTests { try formatter.close() } } + + @Test func closeRejectsJournalBlockCountOverflowingUInt32() throws { + let path = FilePath( + FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: false).path) + defer { try? FileManager.default.removeItem(atPath: path.string) } + let formatter = try EXT4.Formatter( + path, + journal: .init(size: (UInt64(UInt32.max) + 1) * 4096) + ) + #expect(throws: EXT4.Formatter.Error.self) { + try formatter.close() + } + } + + @Test func closeRejectsJournalEndBlockOverflowingUInt32() throws { + let path = FilePath( + FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: false).path) + defer { try? FileManager.default.removeItem(atPath: path.string) } + let formatter = try EXT4.Formatter( + path, + journal: .init(size: UInt64(UInt32.max) * 4096) + ) + #expect(throws: EXT4.Formatter.Error.self) { + try formatter.close() + } + } }