From 253c032d809dec15c6b293c791239c03d2091daf Mon Sep 17 00:00:00 2001 From: siddh34 Date: Sat, 12 Sep 2026 23:40:16 +0530 Subject: [PATCH 1/4] chore: refactor zeroJournalBlocks to skipJournalBlocks and optimize implementation --- .../ContainerizationEXT4/EXT4+Journal.swift | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/Sources/ContainerizationEXT4/EXT4+Journal.swift b/Sources/ContainerizationEXT4/EXT4+Journal.swift index 5e3511f6a..5fd16383a 100644 --- a/Sources/ContainerizationEXT4/EXT4+Journal.swift +++ b/Sources/ContainerizationEXT4/EXT4+Journal.swift @@ -36,7 +36,7 @@ extension EXT4.Formatter { } let journalStartBlock = self.currentBlock try writeJournalSuperblock(journalBlocks: journalBlocks, filesystemUUID: filesystemUUID) - try zeroJournalBlocks(count: journalBlocks - 1) + try skipJournalBlocks(count: journalBlocks - 1) try setupJournalInode(startBlock: journalStartBlock, blockCount: journalBlocks) return journalBlocks } @@ -124,19 +124,11 @@ 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.. Date: Sat, 12 Sep 2026 23:45:10 +0530 Subject: [PATCH 2/4] feat: add journalTooLarge error case and calculateJournalEndBlock method --- .../ContainerizationEXT4/EXT4+Formatter.swift | 4 +++ .../ContainerizationEXT4/EXT4+Journal.swift | 25 ++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) 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 5fd16383a..e65a4d9fe 100644 --- a/Sources/ContainerizationEXT4/EXT4+Journal.swift +++ b/Sources/ContainerizationEXT4/EXT4+Journal.swift @@ -35,9 +35,17 @@ 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 skipJournalBlocks(count: journalBlocks - 1) - try setupJournalInode(startBlock: journalStartBlock, blockCount: journalBlocks) + try setupJournalInode( + startBlock: journalStartBlock, + blockCount: journalBlocks, + endBlock: journalEndBlock + ) return journalBlocks } @@ -131,7 +139,7 @@ extension EXT4.Formatter { try self.handle.seek(toOffset: self.pos + totalBytes) } - private func setupJournalInode(startBlock: UInt32, blockCount: UInt32) throws { + private func setupJournalInode(startBlock: UInt32, blockCount: UInt32, endBlock: UInt32) throws { var journalInode = EXT4.Inode() journalInode.mode = EXT4.Inode.Mode(.S_IFREG, 0o600) journalInode.uid = 0 @@ -154,14 +162,19 @@ extension EXT4.Formatter { // Journal is one contiguous allocation → numExtents = 1 → extent tree fits inline // in the inode, so writeExtents needs no extra disk I/O for extent index blocks. - // Safe: blockCount is at most UInt32.max and startBlock ≥ 0, so the addition could - // theoretically overflow — but zeroJournalBlocks would have already failed with an - // I/O error if the journal extended past the end of the filesystem image. - journalInode = try self.writeExtents(journalInode, (startBlock, startBlock + blockCount)) + journalInode = try self.writeExtents(journalInode, (startBlock, endBlock)) self.inodes[Int(EXT4.JournalInode) - 1].pointee = journalInode } + private func calculateJournalEndBlock(startBlock: UInt32, blockCount: UInt32) throws -> 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, From 4d774061e06776b3229f3696726cc6202729fd60 Mon Sep 17 00:00:00 2001 From: siddh34 Date: Sat, 12 Sep 2026 23:49:51 +0530 Subject: [PATCH 3/4] feat: add overflow checks for journal block count and end block in EXT4 formatter --- .../ContainerizationEXT4/EXT4+Journal.swift | 6 ++-- .../TestEXT4Journal.swift | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Sources/ContainerizationEXT4/EXT4+Journal.swift b/Sources/ContainerizationEXT4/EXT4+Journal.swift index e65a4d9fe..974adf08d 100644 --- a/Sources/ContainerizationEXT4/EXT4+Journal.swift +++ b/Sources/ContainerizationEXT4/EXT4+Journal.swift @@ -59,8 +59,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 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() + } + } } From 9e323b050ba7d3328d1419057d30c928773362d1 Mon Sep 17 00:00:00 2001 From: siddh34 Date: Fri, 2 Oct 2026 15:35:07 +0530 Subject: [PATCH 4/4] feat: deriving block count as diff between start block & end block --- Sources/ContainerizationEXT4/EXT4+Journal.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ContainerizationEXT4/EXT4+Journal.swift b/Sources/ContainerizationEXT4/EXT4+Journal.swift index 974adf08d..4c1179731 100644 --- a/Sources/ContainerizationEXT4/EXT4+Journal.swift +++ b/Sources/ContainerizationEXT4/EXT4+Journal.swift @@ -43,7 +43,6 @@ extension EXT4.Formatter { try skipJournalBlocks(count: journalBlocks - 1) try setupJournalInode( startBlock: journalStartBlock, - blockCount: journalBlocks, endBlock: journalEndBlock ) return journalBlocks @@ -141,11 +140,12 @@ extension EXT4.Formatter { try self.handle.seek(toOffset: self.pos + totalBytes) } - private func setupJournalInode(startBlock: UInt32, blockCount: UInt32, endBlock: UInt32) throws { + private func setupJournalInode(startBlock: UInt32, endBlock: UInt32) throws { var journalInode = EXT4.Inode() journalInode.mode = EXT4.Inode.Mode(.S_IFREG, 0o600) journalInode.uid = 0 journalInode.gid = 0 + let blockCount = endBlock - startBlock let size = UInt64(blockCount) * UInt64(self.blockSize) journalInode.sizeLow = size.lo journalInode.sizeHigh = size.hi