Skip to content

fix: memory over allocation due to last partial block group - #832

Open
siddh34 wants to merge 1 commit into
apple:mainfrom
siddh34:fix/memoryOverAllocation
Open

fix: memory over allocation due to last partial block group#832
siddh34 wants to merge 1 commit into
apple:mainfrom
siddh34:fix/memoryOverAllocation

Conversation

@siddh34

@siddh34 siddh34 commented Aug 6, 2026

Copy link
Copy Markdown

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

@dkovba dkovba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
----------------------------------------

@siddh34

siddh34 commented Aug 7, 2026

Copy link
Copy Markdown
Author

@dkovba thanks for the tests I will update my pr in few days and will include these failed tests in it too!

@siddh34
siddh34 force-pushed the fix/memoryOverAllocation branch from 98555ce to 16a7a7b Compare August 11, 2026 03:07
@siddh34
siddh34 requested a review from dkovba August 11, 2026 03:36
@siddh34

siddh34 commented Aug 11, 2026

Copy link
Copy Markdown
Author

Updates after issues discovered in new test cases as given by @dkovba

  1. Reduced the allocated size by

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

  1. Bitmap fixes as follows

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

CASE: 32KiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/32KiB_none.img
  requested: 32768 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 64MiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/64MiB_none.img
  requested: 67108864 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 128MiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/128MiB_none.img
  requested: 134217728 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 128MiB+4KiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/128MiB+4KiB_none.img
  requested: 134221824 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 130MiB+8KiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/130MiB+8KiB_none.img
  requested: 136323072 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 160MiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/160MiB_none.img
  requested: 167772160 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 256MiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/256MiB_none.img
  requested: 268435456 bytes
  content: 0 bytes
  allocatedMiB: 0
  e2fsck rc: 0
CASE: 1GiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/1GiB_none.img
  requested: 1073741824 bytes
  content: 0 bytes
  allocatedMiB: 1
  e2fsck rc: 0
CASE: 4GiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/4GiB_none.img
  requested: 4294967296 bytes
  content: 0 bytes
  allocatedMiB: 7
  e2fsck rc: 0
CASE: 63x128MiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/63x128MiB_none.img
  requested: 8455716864 bytes
  content: 0 bytes
  allocatedMiB: 14
  e2fsck rc: 0
CASE: 63x128MiB+4KiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/63x128MiB+4KiB_none.img
  requested: 8455720960 bytes
  content: 0 bytes
  allocatedMiB: 14
  e2fsck rc: 0
CASE: 8GiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/8GiB_none.img
  requested: 8589934592 bytes
  content: 0 bytes
  allocatedMiB: 14
  e2fsck rc: 0
CASE: 16GiB_none
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/16GiB_none.img
  requested: 17179869184 bytes
  content: 0 bytes
  allocatedMiB: 29
  e2fsck rc: 0
CASE: 160MiB_10MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/160MiB_10MiB.img
  requested: 167772160 bytes
  content: 10485760 bytes
  allocatedMiB: 10
  e2fsck rc: 0
CASE: 128MiB_50MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/128MiB_50MiB.img
  requested: 134217728 bytes
  content: 52428800 bytes
  allocatedMiB: 50
  e2fsck rc: 0
CASE: 160MiB_120MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/160MiB_120MiB.img
  requested: 167772160 bytes
  content: 125829120 bytes
  allocatedMiB: 120
  e2fsck rc: 0
CASE: 160MiB_124MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/160MiB_124MiB.img
  requested: 167772160 bytes
  content: 130023424 bytes
  allocatedMiB: 124
  e2fsck rc: 0
CASE: 160MiB_126MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/160MiB_126MiB.img
  requested: 167772160 bytes
  content: 132120576 bytes
  allocatedMiB: 126
  e2fsck rc: 0
CASE: 256MiB_130MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/256MiB_130MiB.img
  requested: 268435456 bytes
  content: 136314880 bytes
  allocatedMiB: 130
  e2fsck rc: 0
CASE: 1GiB_200MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/1GiB_200MiB.img
  requested: 1073741824 bytes
  content: 209715200 bytes
  allocatedMiB: 201
  e2fsck rc: 0
CASE: 300MiB_260MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/300MiB_260MiB.img
  requested: 314572800 bytes
  content: 272629760 bytes
  allocatedMiB: 260
  e2fsck rc: 0
CASE: 4GiB_260MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/4GiB_260MiB.img
  requested: 4294967296 bytes
  content: 272629760 bytes
  allocatedMiB: 267
  e2fsck rc: 0
CASE: 63x128MiB_500MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/63x128MiB_500MiB.img
  requested: 8455716864 bytes
  content: 524288000 bytes
  allocatedMiB: 514
  e2fsck rc: 0
CASE: 8GiB_300MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/8GiB_300MiB.img
  requested: 8589934592 bytes
  content: 314572800 bytes
  allocatedMiB: 314
  e2fsck rc: 0
CASE: 16GiB_1000MiB
  image: /var/folders/y4/rkj6lthx2vg9ls8cbyyy8nb00000gn/T/ext4-e2fsck-matrix-92C353EF-EC99-40C4-8630-94E6E7CA3F46/16GiB_1000MiB.img
  requested: 17179869184 bytes
  content: 1048576000 bytes
  allocatedMiB: 1029
  e2fsck rc: 0
========== E2FSCK SUMMARY ==========
All 25 cases passed.

@dkovba please check according to your convience, thanks!

Open to making any changes needed or I missed for this current pull request

@dkovba dkovba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for updating the PR!

Comment thread Sources/ContainerizationEXT4/EXT4+Formatter.swift Outdated
Comment thread Tests/ContainerizationEXT4Tests/TestEXT4Format.swift
Comment thread Tests/ContainerizationEXT4Tests/TestEXT4Format.swift
Comment thread Tests/ContainerizationEXT4Tests/TestEXT4Format.swift
Comment thread Tests/ContainerizationEXT4Tests/TestEXT4Format.swift
Comment thread Sources/ContainerizationEXT4/EXT4+Formatter.swift Outdated

@dkovba dkovba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

// minimizes the number of blockGroups needed to its lowest value
for ipg in stride(from: inc, through: Int(self.maxInodesPerGroup), by: inc) {
let start = Int(self.minimumInodesPerGroup) // inodesPerGroup
let step = Int(self.minimumInodesPerGroup)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the original block size derivation here? With minimumInodesPerGroup: UInt32 = 896, we may run out of inodes.

↳ 256MiB image, 8000 files:
↳ write: Could not allocate inode in ext2 filesystem 
↳ created == fileCount → false
↳   created → 1781
↳   fileCount → 8000

@siddh34 siddh34 Aug 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay got it, Would not change the original block size derivation

the idea was to keep packed-metadata inode tables small for sparse & large images

this would break the allocatedSize <= ceilingMIB

shall I remove the check for allocatedSize <= ceilingMIB?

@siddh34
siddh34 force-pushed the fix/memoryOverAllocation branch from 16a7a7b to b01028b Compare August 15, 2026 11:36
@siddh34
siddh34 requested a review from dkovba August 15, 2026 11:41
@siddh34
siddh34 force-pushed the fix/memoryOverAllocation branch from b01028b to 0594735 Compare August 15, 2026 11:42
@siddh34

siddh34 commented Aug 15, 2026

Copy link
Copy Markdown
Author

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

Tested with

let inc = Int(self.blockSize * self.inodeStrideKiB) / Int(EXT4.InodeSize)  // minimizes the number of blockGroups needed to its lowest value

where self.inodeStrideKiB = 512

Results

Requested Size: 128MiB
Content: none
Size: PASS
e2fsck: PASS
Our logical size: 128MiB, mke2fs logical size: 128MiB
Our physical size: 2072KiB, mke2fs physical size: 14011KiB
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: PASS
Requested Size: 1GiB
Content: none
Size: PASS
e2fsck: PASS
Our logical size: 1GiB, mke2fs logical size: 1GiB
Our physical size: 16464KiB, mke2fs physical size: 52076KiB
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: PASS
Requested Size: 4GiB
Content: none
Size: PASS
e2fsck: PASS
Our logical size: 4GiB, mke2fs logical size: 4GiB
Our physical size: 65808KiB, mke2fs physical size: 148792KiB
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: PASS
Requested Size: 8GiB
Content: none
Size: PASS
e2fsck: PASS
Our logical size: 8GiB, mke2fs logical size: 8GiB
Our physical size: 131600KiB, mke2fs physical size: 234052KiB
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: PASS
Requested Size: 16GiB
Content: none
Size: PASS
e2fsck: PASS
Our logical size: 16GiB, mke2fs logical size: 16GiB
Our physical size: 263184KiB, mke2fs physical size: 439460KiB
Logical Size vs mke2fs: PASS
Physical Size vs mke2fs: PASS

Here is my script for tests

import ContainerizationArchive
import Foundation
import SystemPackage
import Testing

@testable import ContainerizationEXT4

struct E2FsckImageCase: Sendable {
    let name: String
    let imagePath: FilePath
    let requestedSize: UInt64
    let contentSize: UInt64

    init(
        name: String,
        imagePath: FilePath,
        requestedSize: UInt64,
        contentSize: UInt64 = 0
    ) {
        self.name = name
        self.imagePath = imagePath
        self.requestedSize = requestedSize
        self.contentSize = contentSize
    }
}

struct E2FsckResult: Sendable {
    let exitCode: Int32
    let stdout: String
    let stderr: String

    var passed: Bool {
        exitCode == 0
    }
}

struct EXT4E2FsckHarness {
    let e2fsckPath: String
    let mke2fsPath: String

    init(
        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 {
        if FileManager.default.fileExists(atPath: imagePath.string) {
            try FileManager.default.removeItem(at: imagePath.url)
        }
        FileManager.default.createFile(atPath: imagePath.string, contents: nil)
        let handle = try FileHandle(forWritingTo: imagePath.url)
        try handle.truncate(atOffset: requestedSize)
        try handle.close()

        let process = Process()
        process.executableURL = URL(fileURLWithPath: mke2fsPath)
        process.arguments = ["-F", "-q", "-t", "ext4", imagePath.string]
        let stderrPipe = Pipe()
        process.standardError = stderrPipe
        process.standardOutput = Pipe()
        try process.run()
        process.waitUntilExit()
        guard process.terminationStatus == 0 else {
            let err = String(decoding: stderrPipe.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
            struct MkE2FsError: Swift.Error, CustomStringConvertible {
                let description: String
            }
            throw MkE2FsError(description: "mke2fs failed: \(err)")
        }
    }

    func createImage(for testCase: E2FsckImageCase) throws {
        let formatter = try EXT4.Formatter(
            testCase.imagePath,
            minDiskSize: testCase.requestedSize
        )

        if testCase.contentSize > 0 {
            let chunkSize = min(testCase.contentSize, 1.mib())
            let chunk = Data(repeating: 0x41, count: Int(chunkSize))
            let stream = 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 {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: e2fsckPath)
        process.arguments = ["-fn", imagePath.string]

        let stdoutPipe = Pipe()
        let stderrPipe = Pipe()
        process.standardOutput = stdoutPipe
        process.standardError = stderrPipe

        try process.run()
        process.waitUntilExit()

        let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
        let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()

        return E2FsckResult(
            exitCode: process.terminationStatus,
            stdout: String(decoding: stdoutData, as: UTF8.self),
            stderr: String(decoding: stderrData, as: UTF8.self)
        )
    }

    func deleteImage(at imagePath: FilePath) throws {
        if FileManager.default.fileExists(atPath: imagePath.string) {
            try FileManager.default.removeItem(at: imagePath.url)
        }
    }

    func imageSize(at imagePath: FilePath) throws -> UInt64 {
        let handle = try FileHandle(forReadingFrom: imagePath.url)
        return try handle.seekToEnd()
    }

    func allocatedMiB(at imagePath: FilePath) throws -> UInt64 {
        let ext4 = try EXT4.EXT4Reader(blockDevice: imagePath)
        let sb = ext4.superBlock
        let blocksCount = UInt64(sb.blocksCountLow) | (UInt64(sb.blocksCountHigh) << 32)
        let freeBlocks = UInt64(sb.freeBlocksCountLow) | (UInt64(sb.freeBlocksCountHigh) << 32)
        let usedBlocks = blocksCount - freeBlocks
        let usedBytes = 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) {
        let logicalBytes = try imageSize(at: imagePath)
        let ext4 = try EXT4.EXT4Reader(blockDevice: imagePath)
        let sb = ext4.superBlock
        let blocksCount = UInt64(sb.blocksCountLow) | (UInt64(sb.blocksCountHigh) << 32)
        let freeBlocks = UInt64(sb.freeBlocksCountLow) | (UInt64(sb.freeBlocksCountHigh) << 32)
        let physicalBytes = (blocksCount - freeBlocks) * UInt64(sb.blockSize)
        return (logicalBytes, physicalBytes)
    }
}

final class RepeatingDataStream: ReadableStream {
    private let chunk: Data
    private let totalBytes: UInt64
    private var writtenBytes: UInt64 = 0
    private var chunkOffset: Int = 0

    init(chunk: Data, totalBytes: UInt64) {
        self.chunk = chunk
        self.totalBytes = totalBytes
    }

    func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int {
        guard writtenBytes < totalBytes else {
            return 0
        }

        let remaining = Int(min(UInt64(len), totalBytes - writtenBytes))
        let bytes = chunk.withUnsafeBytes { chunkBytes in
            guard let base = chunkBytes.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                return 0
            }

            var copied = 0
            while copied < remaining {
                let available = chunk.count - chunkOffset
                let toCopy = 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
    }
}

private func formatBytes(_ bytes: UInt64) -> String {
    let units: [(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"
}

private func formatCount(_ n: UInt64) -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    return formatter.string(from: NSNumber(value: n)) ?? "\(n)"
}

@Test func e2FsckMatrixComparesAgainstMke2fs() throws {
    let harness = EXT4E2FsckHarness()

    let cases: [(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),
    ]

    let root = FileManager.default.temporaryDirectory
        .appendingPathComponent("ext4-mke2fs-compare-\(UUID().uuidString)", isDirectory: true)
    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
    defer { try? FileManager.default.removeItem(at: root) }

    for testCase in cases {
        let ourImage = FilePath(root.appendingPathComponent("\(testCase.name)-ours.img", isDirectory: false))
        let refImage = 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)

        let fsckResult = try harness.runE2Fsck(on: ourImage)
        let ours = try harness.footprint(at: ourImage)
        let reference = try harness.footprint(at: refImage)

        let sizePass = ours.logicalBytes == testCase.requested
        let fsckPass = fsckResult.passed
        let logicalPass = ours.logicalBytes == reference.logicalBytes
        let physicalPass = 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

@siddh34
siddh34 force-pushed the fix/memoryOverAllocation branch from 0594735 to c167cb3 Compare August 15, 2026 15:25
@siddh34
siddh34 force-pushed the fix/memoryOverAllocation branch from c167cb3 to 3d888ee Compare August 15, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants