Uh oh!
There was an error while loading. Please reload this page.
fix: memory over allocation due to last partial block group - #832
Conversation
There was a problem hiding this comment.
Thank you for the contribution!
Below are the test results across the size/content matrix, cross-checked with e2fsck:
Requested Size: 32KiB
Content: none
Size: FAIL
audit.imageSize == testCase.requested = false; audit.imageSize = 134217728; testCase.requested = 32768
e2fsck: PASS
----------------------------------------
Requested Size: 64MiB
Content: none
Size: FAIL
audit.imageSize == testCase.requested = false; audit.imageSize = 134217728; testCase.requested = 67108864
e2fsck: PASS
----------------------------------------
Requested Size: 128MiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 128MiB+4KiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 130MiB+8KiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 160MiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 256MiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 1GiB
Content: none
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 4GiB
Content: none
Size: FAIL
allocatedMiB <= ceilingMiB = false; allocatedMiB = 65; ceilingMiB = 32
e2fsck: PASS
----------------------------------------
Requested Size: 63x128MiB
Content: none
Size: FAIL
allocatedMiB <= ceilingMiB = false; allocatedMiB = 127; ceilingMiB = 32
e2fsck: PASS
----------------------------------------
Requested Size: 63x128MiB+4KiB
Content: none
Size: FAIL
allocatedMiB <= ceilingMiB = false; allocatedMiB = 129; ceilingMiB = 32
e2fsck: FAIL rc=4
Block bitmap differences: +(32768--32930)
Fix? no
/imgs/63x128MiB+4KiB.img: ********** WARNING: Filesystem still has errors **********
/imgs/63x128MiB+4KiB.img: 11/524288 files (0.0% non-contiguous), 32737/2064385 blocks
----------------------------------------
Requested Size: 8GiB
Content: none
Size: FAIL
allocatedMiB <= ceilingMiB = false; allocatedMiB = 129; ceilingMiB = 32
e2fsck: FAIL rc=4
Block bitmap differences: +(32768--32930)
Fix? no
/imgs/8GiB.img: ********** WARNING: Filesystem still has errors **********
/imgs/8GiB.img: 11/524288 files (0.0% non-contiguous), 32737/2097152 blocks
----------------------------------------
Requested Size: 16GiB
Content: none
Size: FAIL
allocatedMiB <= ceilingMiB = false; allocatedMiB = 258; ceilingMiB = 32
e2fsck: FAIL rc=4
Block bitmap differences: +(32768--65826)
Fix? no
/imgs/16GiB.img: ********** WARNING: Filesystem still has errors **********
/imgs/16GiB.img: 11/1048576 files (0.0% non-contiguous), 32737/4194304 blocks
----------------------------------------
Requested Size: 160MiB
Content: 10MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 128MiB
Content: 50MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 160MiB
Content: 120MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 160MiB
Content: 124MiB
Size: PASS
e2fsck: FAIL rc=4
Block bitmap differences: +(32768--32806)
Fix? no
/imgs/160MiB_124MiB-content.img: ********** WARNING: Filesystem still has errors **********
/imgs/160MiB_124MiB-content.img: 135/16384 files (0.0% non-contiguous), 32737/40960 blocks
----------------------------------------
Requested Size: 160MiB
Content: 126MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 256MiB
Content: 130MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 1GiB
Content: 200MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 300MiB
Content: 260MiB
Size: FAIL
audit.imageSize == testCase.requested = false; audit.imageSize = 402653184; testCase.requested = 314572800
e2fsck: PASS
----------------------------------------
Requested Size: 4GiB
Content: 260MiB
Size: PASS
e2fsck: PASS
----------------------------------------
Requested Size: 63x128MiB
Content: 500MiB
Size: PASS
e2fsck: FAIL rc=4
Block bitmap differences: +(131072--160417)
Fix? no
/imgs/63x128MiB_500MiB-content.img: ********** WARNING: Filesystem still has errors **********
/imgs/63x128MiB_500MiB-content.img: 511/516096 files (0.0% non-contiguous), 131041/2064384 blocks
----------------------------------------
Requested Size: 8GiB
Content: 300MiB
Size: PASS
e2fsck: FAIL rc=4
Block bitmap differences: +(98304--109730)
Fix? no
/imgs/8GiB_300MiB-content.img: ********** WARNING: Filesystem still has errors **********
/imgs/8GiB_300MiB-content.img: 311/524288 files (0.0% non-contiguous), 98273/2097152 blocks
----------------------------------------
Requested Size: 16GiB
Content: 1000MiB
Size: PASS
e2fsck: FAIL rc=4
Block bitmap differences: +(262144--321828)
Fix? no
/imgs/16GiB_1000MiB-content.img: ********** WARNING: Filesystem still has errors **********
/imgs/16GiB_1000MiB-content.img: 1011/1048576 files (0.0% non-contiguous), 262113/4194304 blocks
----------------------------------------
@dkovba thanks for the tests I will update my pr in few days and will include these failed tests in it too! |
98555ce to
16a7a7bCompareUpdates after issues discovered in new test cases as given by @dkovba
defining minimum inodes per group at line 41 This tries multiple candidate values for inodesPerGroup line 1093 it basically means smaller inode tables less metadata overhead
Now ext4 block bitmaps are per-group. Changes can be found at line 879 basically marking the packed metadata blocks that physically live inside a group as allocated in that group’s block bitmap also tested with e2fsck @dkovba please check according to your convience, thanks! Open to making any changes needed or I missed for this current pull request |
dkovba
left a comment
There was a problem hiding this comment.
Thank you for updating the PR!
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
dkovba
left a comment
There was a problem hiding this comment.
With the suggested implementation of the partial block support, we use significantly more physical space than on main and than mke2fs does.
With minimumInodesPerGroup: UInt32 = 896:
Requested Size: 16GiB
Content: none
Size: PASS
e2fsck: PASS
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: FAIL
physical <= mke2fs = false; physical = 30,457,856; mke2fs = 4,816,896
With the original block size derivation:
Requested Size: 16GiB
Content: none
Size: PASS
e2fsck: PASS
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: FAIL
physical <= mke2fs = false; physical = 269,647,872; mke2fs = 4,816,896
Uh oh!
There was an error while loading. Please reload this page.
16a7a7b to
b01028bCompareb01028b to
0594735Compare
Tested with letinc=Int(self.blockSize *self.inodeStrideKiB)/ Int(EXT4.InodeSize) // minimizes the number of blockGroups needed to its lowest valuewhere self.inodeStrideKiB = 512 Results Here is my script for tests import ContainerizationArchive
import Foundation
import SystemPackage
import Testing
@testableimport ContainerizationEXT4
structE2FsckImageCase:Sendable{letname:StringletimagePath:FilePathletrequestedSize:UInt64letcontentSize:UInt64init(
name:String,
imagePath:FilePath,
requestedSize:UInt64,
contentSize:UInt64=0){self.name = name
self.imagePath = imagePath
self.requestedSize = requestedSize
self.contentSize = contentSize
}}structE2FsckResult:Sendable{letexitCode:Int32letstdout:Stringletstderr:Stringvarpassed:Bool{
exitCode ==0}}structEXT4E2FsckHarness{lete2fsckPath:Stringletmke2fsPath:Stringinit(
e2fsckPath:String="/opt/homebrew/Cellar/e2fsprogs/1.47.4/sbin/e2fsck",
mke2fsPath:String="/opt/homebrew/Cellar/e2fsprogs/1.47.4/sbin/mke2fs"){self.e2fsckPath = e2fsckPath
self.mke2fsPath = mke2fsPath
}func createReferenceImage(requestedSize:UInt64, at imagePath:FilePath)throws{ifFileManager.default.fileExists(atPath: imagePath.string){tryFileManager.default.removeItem(at: imagePath.url)}FileManager.default.createFile(atPath: imagePath.string, contents:nil)lethandle=tryFileHandle(forWritingTo: imagePath.url)try handle.truncate(atOffset: requestedSize)try handle.close()letprocess=Process()
process.executableURL =URL(fileURLWithPath: mke2fsPath)
process.arguments =["-F","-q","-t","ext4", imagePath.string]letstderrPipe=Pipe()
process.standardError = stderrPipe
process.standardOutput =Pipe()try process.run()
process.waitUntilExit()guard process.terminationStatus ==0else{leterr=String(decoding: stderrPipe.fileHandleForReading.readDataToEndOfFile(), as:UTF8.self)structMkE2FsError:Swift.Error,CustomStringConvertible{letdescription:String}throwMkE2FsError(description:"mke2fs failed: \(err)")}}func createImage(for testCase:E2FsckImageCase)throws{letformatter=tryEXT4.Formatter(
testCase.imagePath,
minDiskSize: testCase.requestedSize
)if testCase.contentSize >0{letchunkSize=min(testCase.contentSize,1.mib())letchunk=Data(repeating:0x41, count:Int(chunkSize))letstream=RepeatingDataStream(chunk: chunk, totalBytes: testCase.contentSize)try formatter.create(
path:FilePath("/content.bin"),
mode:EXT4.Inode.Mode(.S_IFREG,0o644),
buf: stream
)}try formatter.close()}func runE2Fsck(on imagePath:FilePath)throws->E2FsckResult{letprocess=Process()
process.executableURL =URL(fileURLWithPath: e2fsckPath)
process.arguments =["-fn", imagePath.string]letstdoutPipe=Pipe()letstderrPipe=Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
try process.run()
process.waitUntilExit()letstdoutData= stdoutPipe.fileHandleForReading.readDataToEndOfFile()letstderrData= stderrPipe.fileHandleForReading.readDataToEndOfFile()returnE2FsckResult(
exitCode: process.terminationStatus,
stdout:String(decoding: stdoutData, as:UTF8.self),
stderr:String(decoding: stderrData, as:UTF8.self))}func deleteImage(at imagePath:FilePath)throws{ifFileManager.default.fileExists(atPath: imagePath.string){tryFileManager.default.removeItem(at: imagePath.url)}}func imageSize(at imagePath:FilePath)throws->UInt64{lethandle=tryFileHandle(forReadingFrom: imagePath.url)returntry handle.seekToEnd()}func allocatedMiB(at imagePath:FilePath)throws->UInt64{letext4=tryEXT4.EXT4Reader(blockDevice: imagePath)letsb= ext4.superBlock
letblocksCount=UInt64(sb.blocksCountLow) | (UInt64(sb.blocksCountHigh) << 32)letfreeBlocks=UInt64(sb.freeBlocksCountLow) | (UInt64(sb.freeBlocksCountHigh) << 32)letusedBlocks= blocksCount - freeBlocks
letusedBytes= usedBlocks * UInt64(sb.blockSize)return usedBytes /1024/1024}
// Physical (used) and logical (file) size for any existing ext4 image, ours or a reference.
func footprint(at imagePath:FilePath)throws->(logicalBytes:UInt64, physicalBytes:UInt64){letlogicalBytes=tryimageSize(at: imagePath)letext4=tryEXT4.EXT4Reader(blockDevice: imagePath)letsb= ext4.superBlock
letblocksCount=UInt64(sb.blocksCountLow) | (UInt64(sb.blocksCountHigh) << 32)letfreeBlocks=UInt64(sb.freeBlocksCountLow) | (UInt64(sb.freeBlocksCountHigh) << 32)letphysicalBytes=(blocksCount - freeBlocks)* UInt64(sb.blockSize)return(logicalBytes, physicalBytes)}}finalclassRepeatingDataStream:ReadableStream{privateletchunk:DataprivatelettotalBytes:UInt64privatevarwrittenBytes:UInt64=0privatevarchunkOffset:Int=0init(chunk:Data, totalBytes:UInt64){self.chunk = chunk
self.totalBytes = totalBytes
}func read(_ buffer:UnsafeMutablePointer<UInt8>, maxLength len:Int)->Int{guard writtenBytes < totalBytes else{return0}letremaining=Int(min(UInt64(len), totalBytes - writtenBytes))letbytes= chunk.withUnsafeBytes{ chunkBytes inguardlet base = chunkBytes.baseAddress?.assumingMemoryBound(to:UInt8.self)else{return0}varcopied=0while copied < remaining {letavailable= chunk.count - chunkOffset
lettoCopy=min(remaining - copied, available)
buffer.advanced(by: copied).update(from: base.advanced(by: chunkOffset), count: toCopy)
copied += toCopy
chunkOffset =(chunkOffset + toCopy)% chunk.count
}return copied
}
writtenBytes +=UInt64(bytes)return bytes
}}privatefunc formatBytes(_ bytes:UInt64)->String{letunits:[(UInt64,String)]=[(1.gib(),"GiB"),(1.mib(),"MiB"),(1.kib(),"KiB")]for(size, label)in units where bytes >= size && bytes % size ==0{return"\(bytes / size)\(label)"}return"\(bytes)B"}privatefunc formatCount(_ n:UInt64)->String{letformatter=NumberFormatter()
formatter.numberStyle =.decimal
return formatter.string(from:NSNumber(value: n))??"\(n)"}@Testfunc e2FsckMatrixComparesAgainstMke2fs()throws{letharness=EXT4E2FsckHarness()letcases:[(name:String, requested:UInt64, content:UInt64)]=[
// ("32KiB_none", 32.kib(), 0),
// ("64MiB_none", 64.mib(), 0),
("128MiB_none",128.mib(),0),("1GiB_none",1.gib(),0),("4GiB_none",4.gib(),0),("8GiB_none",8.gib(),0),("16GiB_none",16.gib(),0),]letroot=FileManager.default.temporaryDirectory
.appendingPathComponent("ext4-mke2fs-compare-\(UUID().uuidString)", isDirectory:true)tryFileManager.default.createDirectory(at: root, withIntermediateDirectories:true)defer{try?FileManager.default.removeItem(at: root)}fortestCasein cases {letourImage=FilePath(root.appendingPathComponent("\(testCase.name)-ours.img", isDirectory:false))letrefImage=FilePath(root.appendingPathComponent("\(testCase.name)-ref.img", isDirectory:false))try harness.createImage(
for:E2FsckImageCase(name: testCase.name, imagePath: ourImage, requestedSize: testCase.requested, contentSize: testCase.content))try harness.createReferenceImage(requestedSize: testCase.requested, at: refImage)letfsckResult=try harness.runE2Fsck(on: ourImage)letours=try harness.footprint(at: ourImage)letreference=try harness.footprint(at: refImage)letsizePass= ours.logicalBytes == testCase.requested
letfsckPass= fsckResult.passed
letlogicalPass= ours.logicalBytes == reference.logicalBytes
letphysicalPass= ours.physicalBytes <= reference.physicalBytes
print("Requested Size: \(formatBytes(testCase.requested))")print("Content: \(testCase.content >0?formatBytes(testCase.content):"none")")print("Size: \(sizePass ?"PASS":"FAIL")")print("e2fsck: \(fsckPass ?"PASS":"FAIL")")print("Our logical size: \(formatBytes(ours.logicalBytes)), mke2fs logical size: \(formatBytes(reference.logicalBytes))")print("Our physical size: \(formatBytes(ours.physicalBytes)), mke2fs physical size: \(formatBytes(reference.physicalBytes))")print("Logical Size vs mke2fs: \(logicalPass ?"PASS":"FAIL")")print("Physical Size vs mke2fs: \(physicalPass ?"PASS":"FAIL")")if !physicalPass {print("physical <= mke2fs = false; physical = \(formatCount(ours.physicalBytes)); mke2fs = \(formatCount(reference.physicalBytes))")}
#expect(sizePass)
#expect(fsckPass)
#expect(physicalPass,"physical <= mke2fs = false; physical = \(ours.physicalBytes); mke2fs = \(reference.physicalBytes)")}}@dkovba please check when it is convenient for you also please reply to comment |
c167cb3 to
3d888eeComparedkovba
commented
Aug 17, 2026
It seems this code gives allocated size, ignoring sparseness: Consider something like this: Test results for |
siddh34
commented
Aug 18, 2026
@dkovba I will make some changes in few days. Thanks for pointing my tests in correct direction |
3d888ee to
e57324dComparesiddh34
commented
Aug 22, 2026
@dkovba I was able to reduce physical size across file allocation I want to ask you about the journal size you are use for testing containerization vs mke2fs (journalled)? letcases:[(name:String, requested:UInt64, content:UInt64, journalSize:UInt64?)]=[("128MiB_nojournal",128.mib(),0,nil),("1GiB_nojournal",1.gib(),0,nil),("4GiB_nojournal",4.gib(),0,nil),("8GiB_nojournal",8.gib(),0,nil),("16GiB_nojournal",16.gib(),0,nil),("128MiB_journal",128.mib(),0,4.mib()),("63x128MiB_journal",63*128.mib(),0,32.mib()),("63x128MiB+4KiB_journal",63*128.mib()+4.kib(),0,32.mib()),("1GiB_journal",1.gib(),0,32.mib()),("4GiB_journal",4.gib(),0,64.mib()),("8GiB_journal",8.gib(),0,128.mib()),("16GiB_journal",16.gib(),0,256.mib()),]This are the test cases I am testing them with. This test cases are passing, result is pasted as below Also I faced a issue with highlighted line, so I am commenting this superblock.journalUUID = filesystemUUIDas e2fsck was searching for external journal when we aren't writing external journal the error is as follow |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
1412537 to
02283ddCompare@dkovba I will keep the branch in sync with main until it get merged. Also open to making more changes for this PR if something I missed |
02283dd to
a1b6496CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
499337d to
af47d80CompareThere was a problem hiding this comment.
Another test case that fails to pass the e2fsck check:
Requested Size: 128MiB+4KiB
Content: 124MiB
Logical size: PASS
Physical size: PASS
e2fsck: FAIL rc=8
ext2fs_check_desc: Corrupt group descriptor: bad block for inode table
/opt/homebrew/opt/e2fsprogs/sbin/e2fsck: Group descriptors look bad... trying backup blocks...
Note: if several inode or block bitmap blocks or part
of the inode table require relocation, you may wish to try
running e2fsck with the '-b 8193' option first. The problem
may lie only with the primary block group descriptors, and
the backup block group descriptors may be OK.
Inode table for group 1 is not in group. (block 32295)
WARNING: SEVERE DATA LOSS POSSIBLE.
Relocate? no
Pass 1: Checking inodes, blocks, and sizes
Illegal block number passed to ext2fs_test_block_bitmap #32769 for in-use block map
...
Illegal block number passed to ext2fs_mark_block_bitmap #32806 for metadata block map
Error while scanning inodes (8192): Corrupt group descriptor: bad block for inode table
e2fsck: aborted
@dkovba thanks for pointing it out I will update the pull request by the EOD testing a few more possibilities like it |
There was a problem hiding this comment.
Another similar failure, which seems to be caused by a different issue:
Requested size: 128MiB+4KiB
Content: 125MiB+218*4KiB
Logical size: FAIL
audit.imageSize = 134225920; audit.blocksCount * 4.kib() = 134221824
audit.imageSize = 134225920; expectedSize = 134221824
Physical size: PASS
e2fsck: FAIL rc=8
ext2fs_check_desc: Corrupt group descriptor: bad block for inode bitmap
Note: if several inode or block bitmap blocks or part
of the inode table require relocation, you may wish to try
may lie only with the primary block group descriptors, and
the backup block group descriptors may be OK.
Inode bitmap for group 1 is not in group. (block 32769)
Relocate? no
Inode table for group 1 is not in group. (block 32770)
WARNING: SEVERE DATA LOSS POSSIBLE.
Relocate? no
Illegal block number passed to ext2fs_test_block_bitmap #32770 for in-use block map
...
Illegal block number passed to ext2fs_mark_block_bitmap #32779 for metadata block map
af47d80 to
14f1728Comparesiddh34
commented
Aug 29, 2026
@dkovba I have updated my local test suite for new changes also added new tests cases to the current tests. Please check according to your convenience local tests logs |
dkovba
left a comment
There was a problem hiding this comment.
And this one fails the logical size check:
Requested size: 128MiB+60KiB
Content: 125MiB+177*4KiB
Logical size: FAIL
audit.imageSize == audit.blocksCount * 4.kib() = false; audit.imageSize = 134279168; audit.blocksCount * 4.kib() = 134217728
Physical size: PASS
e2fsck: PASS
e2fsck (journalled): PASS
Description
This resolves issue #647
This is fix for memory over allocation due to last partial block
I have explicitly laid out metadata for extra/last groups
Modifications:
Packed-region math is computation starts at line 718
function to calculate blocks in last group at line 58
while loop here didn't made any sense so replaced it with if check line 914