Skip to content

Commit 16a7a7b

Browse files
committed
fix: memory over allocation due to last partial block group
1 parent 5427fd2 commit 16a7a7b

2 files changed

Lines changed: 197 additions & 57 deletions

File tree

Sources/ContainerizationEXT4/EXT4+Formatter.swift

Lines changed: 89 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ extension EXT4 {
3838
blockSize * 8 // limited by inode bitmap
3939
}
4040

41+
private let minimumInodesPerGroup: UInt32 = 896
42+
4143
private var groupsPerDescriptorBlock: UInt32 {
4244
blockSize / groupDescriptorSize
4345
}
@@ -55,6 +57,11 @@ extension EXT4 {
5557
((groupCount - 1) / groupsPerDescriptorBlock + 1) * 32
5658
}
5759

60+
private var blocksInLastGroup: UInt32 {
61+
let remainder = blockCount % blocksPerGroup
62+
return remainder == 0 ? blocksPerGroup : remainder
63+
}
64+
5865
/// Initializes an ext4 filesystem formatter.
5966
///
6067
/// This constructor creates an instance of the ext4 formatter designed to format a block device
@@ -685,11 +692,6 @@ extension EXT4 {
685692
if newSize < contentRequiredSize {
686693
newSize = contentRequiredSize
687694
}
688-
// number of blocks needed for group descriptors
689-
let groupDescriptorBlockCount: UInt32 = (blockGroupSize.blockGroups - 1) / self.groupsPerDescriptorBlock + 1
690-
guard groupDescriptorBlockCount <= self.groupDescriptorBlocks else {
691-
throw Error.insufficientSpaceForGroupDescriptorBlocks
692-
}
693695

694696
var totalBlocks: UInt32 = 0
695697
var totalInodes: UInt32 = 0
@@ -700,20 +702,33 @@ extension EXT4 {
700702
if newSize < minGroups * blocksPerGroup * blockSize {
701703
newSize = UInt64(minGroups * blocksPerGroup * blockSize)
702704
}
703-
let totalGroups = (((newSize / UInt64(self.blockSize)) - 1) / UInt64(self.blocksPerGroup)) + 1
704705

705-
// If the provided disk size is not aligned to a blockgroup boundary, it needs to
706-
// be expanded to the next blockgroup boundary.
707-
// Example:
708-
// Provided disk size: 2 GB + 100MB: 2148 MB
709-
// BlockSize: 4096
710-
// Blockgroup size: 32768 blocks: 128MB
711-
// Number of blocks: 549888
712-
// Number of blockgroups = 549888 / 32768 = 16.78125
713-
// Aligned disk size = 557056 blocks = 17 blockgroups: 2176 MB
714-
if newSize < totalGroups * blocksPerGroup * blockSize {
715-
newSize = UInt64(totalGroups * blocksPerGroup * blockSize)
706+
// Preserve the requested filesystem size exactly when possible.
707+
// Any trailing partial group is kept as-is; we do not round up to a full
708+
// block-group boundary just to place that group's metadata.
709+
//
710+
// For groups beyond blockGroupSize.blockGroups, metadata is packed into a
711+
// reserved region starting at dataBlocks:
712+
// - inode table: inodeTableSizePerGroup blocks
713+
// - block bitmap: 1 block
714+
// - inode bitmap: 1 block
715+
//
716+
// This keeps descriptor pointers in-bounds even when the last group is tiny
717+
// (for example, 128 MiB + 4 KiB), while still preserving exact-size images
718+
// for larger partial tails (for example, 160 MiB).
719+
720+
let fsBlocks: UInt64 = (newSize + UInt64(self.blockSize) - 1) / UInt64(self.blockSize) // round up to block boundary
721+
let totalGroups = ((fsBlocks - 1) / UInt64(self.blocksPerGroup)) + 1 // round up to group boundary
722+
let groupDescriptorBlockCount: UInt32 = (UInt32(totalGroups) - 1) / self.groupsPerDescriptorBlock + 1 // round up to descriptor block boundary
723+
guard groupDescriptorBlockCount <= self.groupDescriptorBlocks else {
724+
throw Error.insufficientSpaceForGroupDescriptorBlocks
716725
}
726+
let extraGroupCount = UInt64(UInt32(totalGroups) - blockGroupSize.blockGroups) // count of groups beyond blockGroupSize.blockGroups that require packed metadata layout
727+
let packedMetadataStart = UInt64(dataBlocks) // start block (inclusive) of packed metadata region for extra groups
728+
let packedMetadataBlocks = extraGroupCount * UInt64(inodeTableSizePerGroup + 2) // each extra group has inodeTableSizePerGroup blocks for the inode table, plus 1 block for the block bitmap and 1 block for the inode bitmap
729+
let packedMetadataEnd = UInt32(packedMetadataStart + packedMetadataBlocks) // end block (exclusive) of packed metadata region for extra groups
730+
let reservedDataBlocks = max(dataBlocks, packedMetadataEnd) // exclusive upper bound of reserved blocks (data/metadata), used for bitmap marking
731+
717732
// Snapshot groupDescriptorBlocks before self.size potentially changes: the bitmap
718733
// loop uses this to identify which GDT slots were physically reserved at init time,
719734
// so it can mark any unused slots as free without accidentally freeing content blocks
@@ -739,13 +754,13 @@ extension EXT4 {
739754
var blocks: UInt32 = 0
740755
// blocks bitmap
741756
var bitmap: [UInt8] = .init(repeating: 0, count: self.blockSize * 2) // 1 for blocks, 1 for inodes
742-
if (group + 1) * UInt32(self.blocksPerGroup) <= dataBlocks { // fully allocated group
757+
if (group + 1) * UInt32(self.blocksPerGroup) <= reservedDataBlocks { // fully allocated group
743758
for i in 0..<(self.blockSize) {
744759
bitmap[Int(i)] = 0xff // mark as allocated
745760
}
746761
blocks = UInt32(self.blocksPerGroup)
747-
} else if group * UInt32(self.blocksPerGroup) < dataBlocks { // partially allocated group
748-
for i in 0..<dataBlocks - group * UInt32(self.blocksPerGroup) {
762+
} else if group * UInt32(self.blocksPerGroup) < reservedDataBlocks { // partially allocated group
763+
for i in 0..<reservedDataBlocks - group * UInt32(self.blocksPerGroup) {
749764
bitmap[Int(i / 8)] |= 1 << (i % 8)
750765
blocks += 1
751766
}
@@ -766,17 +781,13 @@ extension EXT4 {
766781
}
767782
}
768783

769-
// last blockGroup if not aligned with total size should be marked as allocated
770-
let remainingBlocks = diskBlocks % self.blocksPerGroup
771-
if group == totalGroups - 1 && remainingBlocks != 0 && self.size / self.blockSize < self.blocksPerGroup {
772-
for i in remainingBlocks..<self.blocksPerGroup {
784+
// The true last group may be smaller than blocksPerGroup; blocks beyond
785+
// groupBlockCount don't physically exist and must be marked used.
786+
let groupBlockCount = group == totalGroups.lo - 1 ? blocksInLastGroup : self.blocksPerGroup
787+
if groupBlockCount < self.blocksPerGroup {
788+
for i in groupBlockCount..<self.blocksPerGroup {
773789
bitmap[Int(i / 8)] |= 1 << (i % 8)
774790
}
775-
if remainingBlocks < self.size / self.blockSize {
776-
for i in remainingBlocks..<self.size / self.blockSize {
777-
bitmap[Int(i / 8)] &= ~(1 << (i % 8))
778-
}
779-
}
780791
}
781792

782793
// mark deleted blocks as free
@@ -819,7 +830,7 @@ extension EXT4 {
819830
let blockBitmap = UInt64(bitmapOffset + 2 * group)
820831
let inodeBitmap = UInt64(bitmapOffset + 2 * group + 1)
821832
let inodeTable = inodeTableOffset + UInt64(group * inodeTableSizePerGroup)
822-
let freeBlocksCount = UInt32(self.blocksPerGroup - blocks)
833+
let freeBlocksCount = UInt32(groupBlockCount - blocks)
823834
let freeInodesCount = UInt32(blockGroupSize.inodesPerGroup - inodes)
824835
groupDescriptors.append(
825836
// low bits
@@ -843,21 +854,48 @@ extension EXT4 {
843854

844855
// Since the bitmaps for unoccupied block groups are the same, there is no need
845856
// to allocate separate memory or storage for each individual bitmap.
846-
var blockBitmap: [UInt8] = .init(repeating: 0, count: Int(self.blocksPerGroup) / 8)
857+
let blockBitmap: [UInt8] = .init(repeating: 0, count: Int(self.blocksPerGroup) / 8)
847858
var inodeBitmap: [UInt8] = .init(repeating: 0xff, count: Int(self.blocksPerGroup) / 8)
848-
for i in 0..<inodeTableSizePerGroup + 2 {
849-
blockBitmap[Int(i) / 8] |= 1 << (i % 8)
850-
}
851859
for i in 0..<UInt16(blockGroupSize.inodesPerGroup) {
852860
inodeBitmap[Int(i) / 8] &= ~(1 << (i % 8))
853861
}
862+
var packedMetadataCursor = packedMetadataStart
854863
for group in blockGroupSize.blockGroups..<totalGroups.lo {
855-
let blocksInGroup = UInt32(self.blocksPerGroup)
856-
let blockBitmapOffset = UInt64(group * self.blocksPerGroup + inodeTableSizePerGroup)
857-
let inodeBitmapOffset = UInt64(group * self.blocksPerGroup + inodeTableSizePerGroup + 1)
858-
let inodeTableOffset = UInt64(self.blocksPerGroup) * group
859-
let freeBlocksCount = UInt32(blocksInGroup - inodeTableSizePerGroup - 2)
864+
let groupStart = UInt64(group) * UInt64(self.blocksPerGroup)
865+
let blocksInGroup = UInt32(min(UInt64(self.blocksPerGroup), fsBlocks - groupStart))
866+
let groupEnd = groupStart + UInt64(blocksInGroup)
867+
868+
var groupBlockBitmap = blockBitmap
869+
if blocksInGroup < self.blocksPerGroup {
870+
for i in blocksInGroup..<self.blocksPerGroup {
871+
groupBlockBitmap[Int(i / 8)] |= 1 << (i % 8)
872+
}
873+
}
874+
875+
let packedStart = max(groupStart, packedMetadataStart)
876+
let packedEnd = min(groupEnd, UInt64(packedMetadataEnd))
877+
var usedBlocksInGroup: UInt32 = 0
878+
879+
if packedEnd > packedStart {
880+
let localStart = UInt32(packedStart - groupStart)
881+
let localEnd = UInt32(packedEnd - groupStart)
882+
883+
for i in localStart..<localEnd {
884+
groupBlockBitmap[Int(i / 8)] |= 1 << (i % 8)
885+
}
886+
887+
usedBlocksInGroup += localEnd - localStart
888+
}
889+
890+
let inodeTableOffset = packedMetadataCursor
891+
let blockBitmapOffset = inodeTableOffset + UInt64(inodeTableSizePerGroup)
892+
let inodeBitmapOffset = blockBitmapOffset + 1
893+
packedMetadataCursor += UInt64(inodeTableSizePerGroup + 2)
894+
totalBlocks += usedBlocksInGroup
895+
let freeBlocksCount = blocksInGroup - usedBlocksInGroup
860896
let freeInodesCount = UInt32(blockGroupSize.inodesPerGroup)
897+
let zeroInodeTable = [UInt8](repeating: 0, count: Int(inodeTableSizePerGroup) * Int(self.blockSize))
898+
861899
groupDescriptors.append(
862900
// low bits
863901
GroupDescriptor(
@@ -874,9 +912,11 @@ extension EXT4 {
874912
itableUnusedLow: 0x0000,
875913
checksum: 0x0000
876914
))
877-
totalBlocks += (inodeTableSizePerGroup + 2)
878-
try self.seek(block: group * self.blocksPerGroup + inodeTableSizePerGroup)
879-
try self.handle.write(contentsOf: blockBitmap)
915+
try self.seek(block: UInt32(inodeTableOffset))
916+
try self.handle.write(contentsOf: zeroInodeTable)
917+
918+
try self.seek(block: UInt32(blockBitmapOffset))
919+
try self.handle.write(contentsOf: groupBlockBitmap)
880920
try self.handle.write(contentsOf: inodeBitmap)
881921
}
882922

@@ -890,10 +930,9 @@ extension EXT4 {
890930
// write superblock
891931
try self.seek(block: 0)
892932
try self.handle.write(contentsOf: Array<UInt8>.init(repeating: 0, count: 1024))
893-
894933
let computedInodes = totalGroups * blockGroupSize.inodesPerGroup
895-
var blocksCount = totalGroups * self.blocksPerGroup
896-
while blocksCount < totalBlocks {
934+
var blocksCount = (newSize + UInt64(self.blockSize) - 1) / UInt64(self.blockSize)
935+
if blocksCount < totalBlocks {
897936
blocksCount = UInt64(totalBlocks)
898937
}
899938
let totalFreeBlocks: UInt64
@@ -1053,15 +1092,16 @@ extension EXT4 {
10531092

10541093
var groups: UInt32 = UInt32.max
10551094
var inodesPerGroup: UInt32 = 0
1056-
let inc = Int(self.blockSize * 512) / Int(EXT4.InodeSize) // inodesPerGroup
1057-
// minimizes the number of blockGroups needed to its lowest value
1058-
for ipg in stride(from: inc, through: Int(self.maxInodesPerGroup), by: inc) {
1095+
let start = Int(self.minimumInodesPerGroup) // inodesPerGroup
1096+
let step = Int(self.minimumInodesPerGroup)
1097+
for ipg in stride(from: start, through: Int(self.maxInodesPerGroup), by: step) {
10591098
let g = groupCount(blocks, inodes, UInt32(ipg))
1060-
if g < groups {
1099+
if g < groups || (g == groups && UInt32(ipg) < inodesPerGroup) {
10611100
groups = g
10621101
inodesPerGroup = UInt32(ipg)
10631102
}
10641103
}
1104+
10651105
return (groups, inodesPerGroup)
10661106
}
10671107

0 commit comments

Comments
 (0)