Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fatgrow

Copyright (C) 2026 pacnpal — released under LGPL-3.0-or-later.

Native macOS FAT32 partition + filesystem resizer. Grows the slot in the MBR partition table and the FAT32 inside it, in one shot, without needing libparted, GParted Live, or a Linux VM. Bare-FAT32 grows are also supported on individual slices and raw image files.

CI License: LGPL-3.0-or-later


⚠️ Read this before running anything ⚠️

This tool directly rewrites partition tables and filesystem metadata. A wrong device path, an unplugged USB stick mid-write, or undetected hardware faults can destroy every byte on the target disk.

  • ALWAYS take a dd image of the target disk first. The non-negotiable safety gate. fatgrow refuses to write without --i-have-a-backup. The flag is honor-system; you have to actually take the backup.
  • The tool refuses /dev/disk0 (your boot disk) outright.
  • Aimed squarely at external/removable media. Internal disks work too in theory; the boot disk is the only hard refusal.
  • 1.0 covers FAT32 only. FAT16/12 grow, shrink, and GPT slot edits are in the roadmap but return clear errors today rather than guessing.

If any of the above scares you off, that's by design. Use at your own risk and don't skip the backup step.


What it does

Capability Status
Inspect MBR + FAT BPB (--info) done
FAT32 grow on a slice or image done
Combined MBR slot + FAT32 grow on disk done
Resume from interrupted grow done
Refuse dirty filesystems done
Refuse target ≤ current done
FAT16 grow planned 1.1
FAT12 grow not planned
Shrink (any FAT type) planned 1.2
GPT slot edit planned 1.3
Cluster-size change not planned

Requirements

Item Notes
macOS 13.x (Ventura), 14.x (Sonoma), 15.x (Sequoia) tested
Architecture universal binary: arm64 + x86_64
Xcode Command Line Tools only if building from source
sudo required for any operation on a real /dev/diskN
Backup space enough free space for a full dd of the target disk
External storage the target disk; do not point at internal disks

No Homebrew packages, no libparted, no dosfstools, no Python at runtime. Tests use python3 (os.truncate) and newfs_msdos / hdiutil / diskutil, all of which ship with macOS.

Install

Pre-built binary (recommended)

Download the universal binary from the releases page and unpack:

curl -LO https://github.com/pacnpal/fatgrow/releases/download/v1.0.0/fatgrow-1.0.0-macos-universal.tar.gz
curl -LO https://github.com/pacnpal/fatgrow/releases/download/v1.0.0/fatgrow-1.0.0-macos-universal.tar.gz.sha256
shasum -a 256 -c fatgrow-1.0.0-macos-universal.tar.gz.sha256
tar -xzf fatgrow-1.0.0-macos-universal.tar.gz
cd fatgrow-1.0.0-macos-universal
./fatgrow --version

Quarantine on download. Anything you download from a browser on macOS gets the com.apple.quarantine extended attribute, which blocks Gatekeeper-unsigned binaries from running. fatgrow is not code-signed or notarized (no Apple Developer ID). Strip the quarantine flag yourself:

xattr -d com.apple.quarantine ./fatgrow

If you'd rather keep the quarantine flag and click through the prompts, macOS will offer "Allow Anyway" once you try to run it under System Settings → Privacy & Security.

From source

git clone https://github.com/pacnpal/fatgrow.git
cd fatgrow
make                # native build for your CPU
# or:
make universal      # arm64 + x86_64 fat binary
sudo cp fatgrow /usr/local/bin/   # optional

macOS 15 (Sequoia) and later — required permission grants

macOS Sequoia tightened "Files and Folders" privacy controls. Even with sudo, raw access to removable media can require an explicit grant. On a fresh Sequoia install, the first run of fatgrow against an external disk may show:

"Terminal would like to access files on a removable volume."

Approve. The grant persists per-app. If you skip the prompt, fatgrow's read of /dev/rdiskN returns EPERM or EBUSY and the tool exits.

If the prompt never appears (typical for headless / SSH sessions), you may need to grant Full Disk Access to your terminal app:

System Settings → Privacy & Security → Full Disk Access
  → click + → add /Applications/Utilities/Terminal.app (or iTerm,
    Warp, Ghostty, whatever shell you're driving)
  → toggle ON
  → restart the terminal app

This is a per-terminal-app, per-user grant. fatgrow itself doesn't request permissions — it inherits them from the calling shell.

Usage

Inspect

./fatgrow --info path/to/image.img
sudo ./fatgrow --info /dev/disk4s1
sudo ./fatgrow --info /dev/disk4   # whole disk: prints MBR + probes FAT slots

/dev/diskN is silently rewritten internally to /dev/rdiskN for raw character-device access. /dev/disk0 (the boot disk) is refused outright.

Grow

# 1. ALWAYS take a backup first.
sudo dd if=/dev/rdisk4 of=$HOME/disk4-backup.img bs=4m status=progress
shasum -a 256 $HOME/disk4-backup.img > $HOME/disk4-backup.sha256

# 2. Print the grow plan, write nothing.
sudo ./fatgrow --grow --size max --dry-run /dev/disk4

# 3. Execute. The --i-have-a-backup gate is honor-system but mandatory.
sudo ./fatgrow --grow --size max --i-have-a-backup /dev/disk4

# 4. fatgrow itself runs unmount + fsck before exit, so no extra steps.

--size accepts max, raw bytes, or any K/Ki/M/Mi/G/Gi suffix:

sudo ./fatgrow --grow --size 5G    --i-have-a-backup /dev/disk4
sudo ./fatgrow --grow --size 4500M --i-have-a-backup /dev/disk4s1
sudo ./fatgrow --grow --size max   --i-have-a-backup whole-disk-image.img

Auto-detection

fatgrow inspects sector 0 of the target. If it parses as a FAT BPB, the target is treated as a bare FAT volume (slice or unpartitioned image). If not, fatgrow reads the MBR and operates in whole-disk mode. You can override slot selection with --part N (1..4).

Resume

If the grow is interrupted (^C, unplug, kernel panic, power loss), fatgrow leaves a checkpoint at /var/tmp/fatgrow-<volid>.ckpt. To continue:

sudo ./fatgrow --grow --size max --i-have-a-backup --resume /dev/disk4

The checkpoint is updated every 64 MiB during the data-move phase. If you lose power mid-checkpoint and want to start over from your dd backup, restore the backup first, then re-run without --resume.

Other flags

  • --allow-dirty — proceed even if the FS isn't cleanly unmounted (FAT[1] ClnShutBit clear). Implies risk; only do this if you've already fsck'd.
  • --no-progress — suppress the move-loop progress bar.
  • -v / -d — verbose / debug logging.
  • --part N — force operating on MBR slot N (1–4). For a whole disk with a single FAT32 slot, omit and fatgrow auto-picks.

Sample successful run

sudo /usr/local/bin/fatgrow --grow --size max --i-have-a-backup /dev/disk4

MBR signature : 0xaa55 (valid)
Disk signature: 0x428c8b2d
Total sectors : 15924384

# Status Type      Start LBA      Sectors  ...
1   *   0x0c          32256        6322176 ...   FAT32 (LBA)
2       0x12             63          32193 ...   Diagnostic

Target slot: 1  (start LBA 32256, current size 6322176 sec, type 0x0c)
New slot size: 15892128 sectors (7.58 GiB); +9569952 sectors absorbed

==== grow plan ====
total_sectors           6297984 ->        15892128
fat_sz (sec)               6139 ->           15505
clusters                 785709 ->         1982635 (+1196926)
Data move required: 18732 sectors forward (3.00 GiB)

MBR slot 1 resized to 15892128 sectors and synced.
Checkpoint at /var/tmp/fatgrow-233c14d8.ckpt
[move] 100%  (3.00 GiB / 3.00 GiB)
Move completed in 368 seconds.

Done. Volume grown to 15892128 sectors (7.58 GiB); +1196926 clusters added.

Settling filesystem state (unmount + fsck)...
  /sbin/fsck_msdos -y /dev/rdisk4s1
** /dev/rdisk4s1
** Phase 1 - Preparing FAT
** Phase 2 - Checking Directories
** Phase 3 - Checking for Orphan Clusters
4934 files, 7499864 KiB free (1874966 clusters)

Whole-disk grow complete.
To mount: sudo diskutil mountDisk /dev/disk4

How it works

Briefly, since the algorithm is the part most likely to be wrong:

  1. Read the MBR. Find the FAT32 slot to grow.
  2. Compute the new geometry. Microsoft's fatgen103 §3.5 FAT-size formula gives new_fat_sz for the target total sectors.
  3. Refuse if the FS is dirty (FAT[1] ClnShutBit, mask 0x08000000), if any slice is mounted, or if the target ≤ current.
  4. Slot grow. Write the new MBR with updated sector_count and ending CHS triple. (CHS uses the modern 63 sec/track × 255 head geometry, or the 0xFE 0xFF 0xFF sentinel for LBAs out of CHS range.)
  5. Read the entire old FAT1 into RAM (capped at 256 MiB).
  6. Move every data sector from [old_first_data .. old_data_end] to [new_first_data .. new_data_end]. Processing high-LBA first prevents overlapping moves from clobbering unread data, since delta > 0 means each destination is past every still-unread source.
  7. Persist a checkpoint to /var/tmp/fatgrow-<volid>.ckpt every 64 MiB so an interrupted grow can resume.
  8. Write the new (extended) FAT1 and FAT2 — old contents at the start followed by zero-extension for the new free clusters.
  9. Patch the boot sector's tot_sec_32 and fat_sz_32, then mirror to the backup boot region (typically sec 6, 7, 8) per the FAT spec.
  10. Update FSInfo's free_count and nxt_free.
  11. Sync the device, delete the checkpoint.
  12. Cleanup: force-unmount any auto-mounted slices and run fsck_msdos -y so the user sees a guaranteed-clean post-state.

Cluster numbers do not change during a grow — only the cluster→LBA mapping shifts forward. So directory entries (which reference cluster numbers) and the FAT chain (which references cluster numbers) remain correct without any rewriting. This is the simplification that lets fatgrow do the job in ~2,000 lines instead of libparted's multi-thousand-line cluster-remapping algorithm.

Troubleshooting

Resource busy / Inappropriate ioctl for device

The kernel's diskarbitrationd is mid-arbitration after a previous I/O operation. fatgrow falls back to opening without exclusive lock and warns. If you want strict locking:

sudo diskutil unmountDisk force /dev/disk4
sleep 2
sudo ./fatgrow --grow ...

(NO WRITE) from fsck after grow

Some other process (often macOS auto-mount) holds the slice open. fatgrow 1.0 runs diskutil unmountDisk force + fsck_msdos -y automatically at the end of every successful grow, so this should not happen on the post-grow path. If you see it, manually:

sudo diskutil unmountDisk force /dev/disk4
sudo /sbin/fsck_msdos -y /dev/rdisk4s1
sudo diskutil mountDisk /dev/disk4

Filesystem appears "dirty" but data is intact

Mounted FAT volumes have boot_flags & 0x01 set on disk (the "currently-mounted" indicator). fsck_msdos of a mounted volume will always report dirty. Force-unmount and re-run fsck.

Kernel doesn't see the new partition size

After fatgrow exits, macOS re-arbitrates the disk and should pick up the new MBR. If diskutil list still shows the old size:

sudo diskutil unmountDisk /dev/disk4
sudo diskutil eject /dev/disk4
# physically unplug + replug the USB stick

"Permission denied" / EPERM on /dev/rdiskN

You're either missing sudo or you're on macOS 15+ without the Files & Folders / Full Disk Access grant. See the "macOS 15 permission grants" section above.

Tests

make smoke               # --info on FAT16 + FAT32 images
make test-fs-grow        # bare FAT32 grow on a 64 → 256 MiB image
make test-wholedisk-grow # combined MBR + FAT32 grow with file checksums
make test-safety         # 4 refusal scenarios
make test                # all of the above

test-wholedisk-grow builds a 256 MiB image with a 64 MiB FAT32 slot plus 192 MiB of trailing free space (mimicking the typical "restored small partclone image onto larger USB" scenario), writes ~20 MiB of test data including random binary files and subdirectories, runs the combined grow, then verifies:

  • All file SHA-256 checksums survive the grow.
  • macOS auto-mounts the post-grow slice cleanly.
  • fsck_msdos -n passes phases 1, 2, 3 with no errors.
  • Both FAT copies remain byte-identical.
  • The backup boot sector mirror at sector 6 matches the primary.

CI runs the full test suite with -Werror enforced on macOS-13 (x86_64) and macOS-14 (arm64) GitHub Actions runners. See .github/workflows/ci.yml.

Bug reports / questions

GitHub Issues. Please include:

  • macOS version (sw_vers).
  • The output of sudo fatgrow --info <target> (BEFORE attempting any destructive operation).
  • The exact command you ran and the full stderr.
  • Whether you have a backup. If you don't, stop and take one before any further investigation.

License

GNU Lesser General Public License v3.0 or later.

The LGPL-3.0 incorporates the GPL-3.0 by reference; both texts are shipped in this repository (LICENSE and COPYING).

Acknowledgements / prior art

  • Microsoft FAT32 File System Specification ("fatgen103") — the canonical reference used to verify FAT-size and BPB layout details.
  • dosfstools (fsck.fat, mkfs.fat) — provided ground truth for the FAT[1] flag-bit mask values.
  • GNU parted / libparted — fatgrow does not use libparted, but libparted's libparted/fs/r/fat/resize.c was studied to understand the more general cluster-remapping algorithm. fatgrow uses the simpler "memmove + extend FAT" approach because it keeps sec_per_clus constant.

fatgrow is a clean-room reimplementation written in 2026. No upstream parted code was copied.

About

Native macOS FAT32 partition + filesystem resizer. No libparted, no Linux live USB needed.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages