DEV Community

Hiroshi Toyama
Hiroshi Toyama

Posted on

Editing an ext4 Partition Directly from macOS (No Linux VM Required)

I lost the private key for a Raspberry Pi and needed to add a new public key to /root/.ssh/authorized_keys. The Pi's SD card was plugged into a Mac via a USB reader, so diskutil list could see the partitions fine — but the root filesystem was ext4, which macOS cannot mount. No spare Linux box, no VM set up, just Docker Desktop (which, on Apple Silicon, only gives you a Linux kernel inside its VM — it doesn't let a container touch a host block device like /dev/disk5s3). This is the path that worked without either of those.

The trick: e2fsprogs is userspace

e2fsck, debugfs, and friends from e2fsprogs don't rely on the kernel's ext4 driver at all — they implement the ext2/3/4 on-disk format entirely in userspace and just do read()/write()/lseek() on whatever path you give them. That means they work identically whether the target is a regular file, a disk image, or — critically — a raw device node on macOS.

brew install e2fsprogs
# keg-only, so call it by full path or add it to PATH
DEBUGFS=/opt/homebrew/opt/e2fsprogs/sbin/debugfs
Enter fullscreen mode Exit fullscreen mode

diskutil list showed the target as disk5, with the Linux root filesystem on the third slice:

/dev/disk5 (external, physical):
   1: Windows_FAT_32   ...   disk5s1
   2: Linux_Swap       ...   disk5s2
   3: Linux            ...   disk5s3
Enter fullscreen mode Exit fullscreen mode

Read-only exploration works immediately, no mount required:

$DEBUGFS -R "ls -l /root/.ssh" /dev/rdisk5s3
$DEBUGFS -R "dump /root/.ssh/authorized_keys ./authorized_keys.bak" /dev/rdisk5s3
Enter fullscreen mode Exit fullscreen mode

ls -l, stat, and dump give you inode numbers, permissions, timestamps, and file contents without ever mounting the partition. For a simple edit — appending one line to an existing file — I dumped the file, merged in the new key locally, and wrote it back with debugfs -w:

$DEBUGFS -w -R "rm /root/.ssh/authorized_keys" \
            -R "write ./authorized_keys.merged /root/.ssh/authorized_keys" \
            -R "sif /root/.ssh/authorized_keys mode 0100600" \
            -R "sif /root/.ssh/authorized_keys uid 0" \
            -R "sif /root/.ssh/authorized_keys gid 0" \
            /dev/rdisk5s3
Enter fullscreen mode Exit fullscreen mode

That's the whole mechanism. Two things went wrong on the way there, and both are worth knowing before you try this on a filesystem you actually care about.

Gotcha 1: chained -R commands don't share a working directory

debugfs -w accepts multiple -R flags to run several commands in one invocation. I assumed cd would set the working directory for the rest of the chain, the way it would in an interactive debugfs session:

$DEBUGFS -w -R "cd /root/.ssh" -R "write ./authorized_keys.merged authorized_keys" /dev/rdisk5s3
Enter fullscreen mode Exit fullscreen mode

It silently wrote the file to /authorized_keys (filesystem root) instead of /root/.ssh/authorized_keys. The cd had no effect across the command boundary. Lesson: when scripting debugfs -w with multiple -R flags, always give write/rm/sif a full path. Don't rely on cd persisting — verify with ls -l after every mutating step instead of trusting the command succeeded as intended.

Gotcha 2: raw device + journal replay can undo your own edits

The bigger issue: after the edit, I ran a routine consistency check:

e2fsck -y -f /dev/rdisk5s3
Enter fullscreen mode Exit fullscreen mode

This printed recovering journal, then unable to set superblock flags on /dev/rdisk5s3 and aborted with errors still present. A subsequent read of the file showed its inode had been reverted to an unrelated, long-deleted file's metadata — wrong owner, dtime set, zero links. My freshly-written authorized_keys was gone, even though the data block itself was untouched.

Here's what actually happened. debugfs -w writes directly to blocks and bypasses the ext4 journal entirely. A live ext4 filesystem almost always has a non-empty journal — this is completely normal, not a sign of an unclean shutdown. As long as nothing tries to replay that journal, an empty or stale journal is harmless. But e2fsck -y does replay it, and if a journaled transaction happens to touch the same inode table block or bitmap you just hand-edited, replay wins: it overwrites your change with whatever stale state was recorded in the journal.

The specific failure to write the superblock flags turned out to be a macOS quirk: /dev/rdiskN is the character (raw) device, and some of the small, oddly-sized writes e2fsprogs issues when finalizing (clearing the "needs recovery" flag, in this case) get rejected with EINVAL because macOS raw devices require I/O aligned to the device's sector size. Switching to the block device node fixed it — no code changes, just a different path:

e2fsck -y -f /dev/disk5s3   # buffered block device, not /dev/rdisk5s3
Enter fullscreen mode Exit fullscreen mode

Through the buffered device, the kernel's buffer cache absorbs the unaligned writes, and both the journal replay and the superblock update completed cleanly.

The correct order of operations

Putting the two gotchas together, the safe procedure for hand-editing a live ext4 image via debugfs -w is:

  1. Recover the journal first, before touching anything by hand: e2fsck -y -f /dev/diskNsM (buffered device). This flushes any pending journaled transactions so they can't later clobber your edits.
  2. Make your edits with debugfs -w, using absolute paths for every command, verifying each mutation with a read-only ls -l / stat immediately after.
  3. Verify with a read-only check: e2fsck -n -f /dev/diskNsM. If it comes back clean, you're done — resist the urge to run e2fsck -y again "just to be safe"; there's nothing left for it to fix, and re-running it adds risk for no benefit.
# 1. clean baseline
e2fsck -y -f /dev/disk5s3

# 2. edit (absolute paths only)
debugfs -w -R "write ./authorized_keys.merged /root/.ssh/authorized_keys" \
            -R "sif /root/.ssh/authorized_keys mode 0100600" \
            -R "sif /root/.ssh/authorized_keys uid 0" \
            -R "sif /root/.ssh/authorized_keys gid 0" \
            /dev/disk5s3

# 3. confirm, read-only, no further writes
e2fsck -n -f /dev/disk5s3
Enter fullscreen mode Exit fullscreen mode

Summary

  • e2fsprogs (debugfs, e2fsck) implements ext2/3/4 entirely in userspace, so it works against a raw macOS device node with no kernel ext4 driver and no Linux VM.
  • debugfs -w's chained -R commands do not share a working directory — always use absolute paths and verify after each mutation.
  • debugfs -w bypasses the journal. Running e2fsck -y afterward replays it, and can silently revert your hand-edits if they share a block with a pending journaled transaction. Recover the journal before you edit, not after.
  • On macOS, prefer the buffered block device (/dev/diskN) over the raw character device (/dev/rdiskN) for e2fsck/debugfs writes — the raw device's alignment requirements can reject small metadata writes with EINVAL.

Top comments (0)