Skip to content

Port f3probe, f3brew, and f3fix to macOS - #283

Open
sbrunner-atx wants to merge 1 commit into
AltraMayor:masterfrom
sbrunner-atx:macos-port
Open

Port f3probe, f3brew, and f3fix to macOS#283
sbrunner-atx wants to merge 1 commit into
AltraMayor:masterfrom
sbrunner-atx:macos-port

Conversation

@sbrunner-atx

Copy link
Copy Markdown
Contributor

What

Ports the three extra tools - f3probe, f3brew, and f3fix - to macOS, as invited by #8 and requested in #135.

How

The probing algorithm (libprobe.c) is already platform-independent; only the device-access layer needed work. All changes are behind APPLE guards:

  • open() with F_NOCACHE replaces O_DIRECT (plus _DARWIN_C_SOURCE so Darwin extensions remain visible under _POSIX_C_SOURCE)
  • DKIOCGETBLOCKCOUNT / DKIOCGETBLOCKSIZE replace BLKGETSIZE64 / BLKSSZGET
  • whole-disk detection by device-name convention (/dev/rdiskN vs /dev/rdiskNsM) replaces the udev devtype check
  • manual USB reset (used only by f3brew; f3probe uses RT_NONE) polls the device node instead of using a udev monitor
  • software USB reset reports EOPNOTSUPP on macOS

f3fix on macOS is a new libparted-free backend (f3fix_darwin.c) that writes the MBR directly - fixing a fake drive only ever needs a single MBR partition ending at the last good sector from f3probe. MBR only for now; GPT support could follow if there is demand.

Validation

On macOS 26.5 (Intel, MacBookPro16,1), clang + brew argp-standalone:

  • make extra builds all three tools with no warnings
  • f3probe --debug-unit-test: SUMMARY: Perfect!
  • Real hardware (SanDisk Ultra 128 GB USB 3.0, genuine):
$ sudo ./build/f3probe --destructive --time-ops /dev/rdisk7
Good news: The device /dev/rdisk7 is the real thing
Device geometry:
         *Usable* size: 114.60 GB (240328704 blocks)
        Announced size: 114.60 GB (240328704 blocks)
Probe time: 1 min 36 s

The Linux build is unchanged: gcc on Ubuntu compiles all targets with no new warnings, and the Linux code path is untouched.

Notes

  • Happy to add a macos-latest job to the GitHub Actions workflow in a follow-up commit so the port cannot regress.
  • Disclosure: developed with AI assistance (Claude); reviewed, built, and hardware-validated by the author.

@sbrunner-atx

Copy link
Copy Markdown
Contributor Author

For anyone who wants to try this on macOS without building manually, I have published a Homebrew tap pinned to this PR branch:

brew tap sbrunner-atx/f3
brew install sbrunner-atx/f3/f3-macos

Tap: https://github.com/sbrunner-atx/homebrew-f3 (README includes macOS usage notes: raw whole-disk device, unmount first, sudo). I will retire the tap once this is merged and the official formula picks up make extra.

Full f3probe hardware-validation transcript (genuine SanDisk Ultra 128 GB, macOS 26.5 Intel) available on request.

AltraMayor pushed a commit that referenced this pull request Aug 24, 2026
f3-drive-test-macos.sh drives a complete erase / f3write / f3read cycle
on macOS with diskutil, adding what ad-hoc use of the tools tends to
get wrong:

- refuses to run on internal disks or partition slices, and asks for
  explicit confirmation naming the drive label before erasing
- records the USB location ID and device serial number next to a
  user-chosen drive label, so logs stay attached to the physical drive
  even though macOS reassigns diskN numbers by connection order
- unmounts and remounts between write and read so f3read measures the
  drive instead of the page cache
- optional cooldown (--cooldown SEC, or --replug for a physical
  unplug) between write and read, and repeated read passes (--reads N),
  for diagnosing drives that drop off the bus under sustained I/O,
  e.g. from overheating: compare failure time across back-to-back
  passes
- per-drive timestamped logs and a summary table of all read passes

Only needs f3write/f3read (PATH or ../build), so it works on macOS with
the stock tools, independent of the extra-tools port in #283.

This grew out of diagnosing a batch of refurbished 128 GB drives where
one unit passed a full 114 GB write but could not sustain more than
about 90 seconds of reading before disconnecting - a failure mode that
a single write/read pass with default settings does not isolate.
The probing algorithm (libprobe.c) is platform-independent; only the
device-access layer in libdevs.c was Linux-specific. This commit adds
an __APPLE__ backend:

- open() with F_NOCACHE replaces O_DIRECT (with _DARWIN_C_SOURCE so
  the Darwin extensions stay visible despite _POSIX_C_SOURCE)
- DKIOCGETBLOCKCOUNT/DKIOCGETBLOCKSIZE replace BLKGETSIZE64/BLKSSZGET
- whole-disk detection by device-name convention (/dev/rdiskN vs
  /dev/rdiskNsM) replaces the udev devtype check
- manual USB reset (used by f3brew only; f3probe uses RT_NONE) is
  implemented by polling the device node instead of a udev monitor
- software USB reset (USBDEVFS_RESET) reports EOPNOTSUPP on macOS
- posix_fadvise(DONTNEED) is skipped; F_NOCACHE already bypasses the
  page cache

f3fix on macOS is a new libparted-free backend (f3fix_darwin.c) that
writes the MBR directly: fixing a fake drive only ever needs a single
MBR partition ending at the last good sector reported by f3probe.
MBR only for now; GPT is unnecessary for this use case.

The Linux build is unchanged: all __APPLE__ additions are guarded and
the Linux path compiles the same objects with no new warnings.

Tested on macOS 26.5 (Intel, MacBookPro16,1): make extra builds all
three tools with clang and brew argp-standalone, and
f3probe --debug-unit-test reports Perfect.

Closes AltraMayor#8. Closes AltraMayor#135.
@sbrunner-atx

Copy link
Copy Markdown
Contributor Author

Rebased onto current master; builds clean on macOS with -Wall -Wextra -pedantic (f3write, f3read, f3probe, f3brew, f3fix).

I see #285 covers much of the same ground for f3probe and f3brew. Rather than have you compare two black boxes, here is my honest read:

  • Build f3probe and f3brew on macOS #285 has a macOS CI job and this PR does not. That is the better approach, and I would rather it win than duplicate it.
  • Build f3probe and f3brew on macOS #285 fixes the keg-only argp-standalone lookup (brew --prefix argp-standalone). This PR does not. Mine builds on my Intel Mac only because /usr/local/include/argp.h happens to exist there; on Apple Silicon, where keg-only formulae are not linked into the prefix, it would fail. That fix is correct and needed either way.
  • What this PR has that Build f3probe and f3brew on macOS #285 does not is f3fix. Build f3probe and f3brew on macOS #285 leaves it out because of libparted. I replaced libparted with a small MBR writer (src/f3fix_darwin.c), so f3fix builds on macOS with no new dependency.

Testing was on Intel macOS against five physical SanDisk Ultra 128 GB drives, three good and two defective. The bad ones accept a full-capacity write and then drop off the USB bus 37 to 100 seconds into read-back. f3probe reported the good ones correctly: usable size = announced size, 114.60 GB.

One gap I should name: the two defective drives went back to the seller before I thought to run f3probe against them, so I have no transcript of the port's error paths under a mid-probe disconnect. If that matters to you, say so and I will reproduce it the next time a drive fails on me.

Happy to rebase the f3fix part on top of #285 so it lands separately, if that is easier for you to review.

@AltraMayor

Copy link
Copy Markdown
Owner

Hi @sbrunner-atx,

Given your effort to port all three extra tools to macOS, I'm assuming that you're willing to join forces to get your code merged.

My main concern with the proposed code at this point is the widespread of #if[n]def __APPLE__. This makes maintenance harder and will be a nightmare when someone decides to port the extra tools to another platform. I propose isolating the platform-dependent code in separate files. For example, src/libdevs.c could include the platform-dependent code using the following preprocessor directives:

#if defined(__linux__)
#include "linux_libdevs.c"
#elif defined(__APPLE__)
#include "darwin_libdevs.c"
#else
#error "Platform not supported"
#endif

I'm open to evaluating other ways to organize the code if you have alternative approaches to propose.

If you choose to move forward with this work, keep this pull request open and create another pull request following the new approach to organize the code and only port f3probe. Once we work together to review and eventually merge this new pull request, it should be easier later to have a pull request to port f3brew and another to port f3fix.

Finally, we should invite the developers of pull requests #279 and #285 to collaborate with us. They can help by reviewing code, testing, and, perhaps, even writing some of the code. Their help would be important because I'm not a Mac developer and don't have a Mac.

If we all pull this off, we may motivate other developers to port the extra tools to their platforms. So many more users will benefit from the extra tools on their platform.

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