From fdb320cc253bcbedaf782db185531d4ebef36ef8 Mon Sep 17 00:00:00 2001 From: Nick Clyde Date: Sun, 23 Aug 2026 15:37:12 -0700 Subject: [PATCH 1/2] Build f3probe and f3brew on macOS f3probe and f3brew depended on Linux-only interfaces: O_DIRECT, posix_fadvise(), the BLKGETSIZE64/BLKSSZGET ioctls, and libudev for partition detection and USB resets. Add a macOS code path to libdevs.c: - open the device with F_NOCACHE instead of O_DIRECT; - flush with F_FULLFSYNC, falling back to fsync() since neither is supported on every device node (e.g. /dev/rdiskN returns ENOTTY); - query geometry with DKIOCGETBLOCKCOUNT and DKIOCGETBLOCKSIZE; - reject partition names (/dev/[r]diskNsM) by convention, since there is no udev to map a partition to its disk; - only support --reset-type=2 (RT_NONE); f3probe already always uses it, and f3brew reports how to pass it. f3fix still requires libparted, so "make extra" builds only f3probe and f3brew on Darwin. Link -ludev only on Linux, and locate the keg-only argp-standalone via its own Homebrew prefix, since it is not linked into $(brew --prefix) and the previous path never resolved. Extend the macOS CI job to build the extra targets and to run f3probe and f3brew against a RAM disk created with hdiutil. --- .github/workflows/test.yml | 21 +++++++- Makefile | 13 +++-- src/libdevs.c | 103 +++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b09bb1b..111763a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -97,7 +97,7 @@ jobs: steps: - uses: actions/checkout@v3 - run: brew install argp-standalone - - run: make + - run: make all extra - if: matrix.volume == 'relative-path' run: mkdir relative-path @@ -114,6 +114,25 @@ jobs: - run: build/f3read --help - run: ${{ matrix.asroot.sudo }}build/f3read -s 2 -e 4 -r 50000 ${{ matrix.volume }} + - run: build/f3probe -V + - run: build/f3probe --help + - run: build/f3brew -V + - run: build/f3brew --help + + - name: Create RAM disk for f3probe and f3brew tests + id: ramdisk + run: echo "dev=$(hdiutil attach -nomount ram://20480 | tr -d '[:space:]' | sed 's|^/dev/||')" >> $GITHUB_OUTPUT + + - run: sudo build/f3probe /dev/r${{ steps.ramdisk.outputs.dev }} + - run: sudo build/f3brew --reset-type=2 -h 0 -e 4 /dev/r${{ steps.ramdisk.outputs.dev }} + + - name: Cleanup RAM disk + if: always() + run: | + if [ -n "${{ steps.ramdisk.outputs.dev }}" ]; then + hdiutil detach ${{ steps.ramdisk.outputs.dev }} + fi + OpenBSD: strategy: fail-fast: false diff --git a/Makefile b/Makefile index 829eb5a..2188037 100644 --- a/Makefile +++ b/Makefile @@ -15,11 +15,16 @@ UNLINK = unlink ifndef OS OS = $(shell uname -s) endif -ifneq ($(OS), Linux) +ifeq ($(OS), Linux) + UDEV_LIB = -ludev +else ARGP = /usr/local ifeq ($(OS), Darwin) + # f3fix needs libparted, which is not available on macOS. + EXTRA_TARGETS = $(BUILD_DIR)/f3probe $(BUILD_DIR)/f3brew ifneq ($(shell command -v brew),) - ARGP = $(shell brew --prefix) + # argp-standalone is keg-only, so it isn't linked into the prefix. + ARGP = $(shell brew --prefix argp-standalone) endif endif CFLAGS += -I$(ARGP)/include @@ -64,10 +69,10 @@ $(BUILD_DIR)/f3read: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libfile.o $(BUILD_DIR) $(CC) -o $@ $^ $(LDFLAGS) -lm $(BUILD_DIR)/f3probe: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libflow.o $(BUILD_DIR)/libdevs.o $(BUILD_DIR)/libprobe.o $(BUILD_DIR)/f3probe.o - $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev + $(CC) -o $@ $^ $(LDFLAGS) -lm $(UDEV_LIB) $(BUILD_DIR)/f3brew: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/libflow.o $(BUILD_DIR)/libdevs.o $(BUILD_DIR)/f3brew.o - $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev + $(CC) -o $@ $^ $(LDFLAGS) -lm $(UDEV_LIB) $(BUILD_DIR)/f3fix: $(BUILD_DIR)/libutils.o $(BUILD_DIR)/f3fix.o $(CC) -o $@ $^ $(LDFLAGS) -lparted diff --git a/src/libdevs.c b/src/libdevs.c index 7348d8c..31a5c10 100644 --- a/src/libdevs.c +++ b/src/libdevs.c @@ -1,6 +1,9 @@ #define _GNU_SOURCE #define _POSIX_C_SOURCE 200809L #define _FILE_OFFSET_BITS 64 +#ifdef __APPLE__ +#define _DARWIN_C_SOURCE /* For F_NOCACHE and F_FULLFSYNC. */ +#endif #include #include @@ -16,9 +19,13 @@ #include #include #include +#ifdef __APPLE__ +#include +#else #include #include #include +#endif #include "libutils.h" #include "libdevs.h" @@ -463,16 +470,53 @@ static int bdev_write_blocks(struct device *dev, const char *buf, rc = write_all(bdev->fd, buf, length); if (rc) return rc; +#ifdef __APPLE__ + /* F_FULLFSYNC asks the drive to flush its own cache too; + * plain fsync() does not on macOS. Neither is guaranteed to be + * supported on a device node (e.g. /dev/rdiskN), so treat + * "not supported" as success; F_NOCACHE already bypasses the + * page cache and the writes were synchronous. + */ + if (fcntl(bdev->fd, F_FULLFSYNC) == 0) + return 0; + if (errno == ENOTSUP || errno == ENOTTY || errno == EINVAL || + errno == ENODEV) { + if (fsync(bdev->fd) == 0 || errno == ENOTSUP || + errno == ENOTTY || errno == EINVAL) + return 0; + } + return - errno; +#else rc = fsync(bdev->fd); if (rc) return rc; return posix_fadvise(bdev->fd, 0, 0, POSIX_FADV_DONTNEED); +#endif } +#ifdef __APPLE__ +static inline int bdev_open(const char *filename) +{ + int fd = open(filename, O_RDWR); + if (fd < 0) + return fd; + /* macOS has no O_DIRECT; F_NOCACHE is the closest equivalent. */ + if (fcntl(fd, F_NOCACHE, 1) < 0) { + int saved = errno; + close(fd); + errno = saved; + return -1; + } + return fd; +} +#else static inline int bdev_open(const char *filename) { return open(filename, O_RDWR | O_DIRECT); } +#endif + +#ifndef __APPLE__ static struct udev_device *map_dev_to_usb_dev(struct udev_device *dev) { @@ -815,6 +859,8 @@ static int bdev_usb_reset(struct device *dev) return 0; } +#endif /* !__APPLE__ */ + static int bdev_none_reset(struct device *dev) { UNUSED(dev); @@ -834,6 +880,7 @@ static const char *bdev_get_filename(struct device *dev) return dev_bdev(dev)->filename; } +#ifndef __APPLE__ static struct udev_device *map_partition_to_disk(struct udev_device *dev) { struct udev_device *disk_dev; @@ -849,6 +896,25 @@ static struct udev_device *map_partition_to_disk(struct udev_device *dev) return udev_device_ref(disk_dev); } +#else +/* Return true if @filename is of the form /dev/[r]disks[...]. */ +static bool darwin_is_partition_name(const char *filename) +{ + const char *p = strrchr(filename, '/'); + p = p ? p + 1 : filename; + if (*p == 'r') + p++; + if (strncmp(p, "disk", 4)) + return false; + p += 4; + if (*p < '0' || *p > '9') + return false; + while (*p >= '0' && *p <= '9') + p++; + return *p == 's' && p[1] >= '0' && p[1] <= '9'; +} +#endif /* !__APPLE__ */ + /* XXX This is borrowing from glibc. * A better solution would be to return proper errors, * so callers write their own messages. @@ -858,9 +924,11 @@ extern const char *__progname; struct device *create_block_device(const char *filename, enum reset_type rt) { struct block_device *bdev; +#ifndef __APPLE__ struct udev *udev; struct udev_device *fd_dev; const char *s; +#endif int block_size, block_order; bdev = malloc(sizeof(*bdev)); @@ -871,6 +939,19 @@ struct device *create_block_device(const char *filename, enum reset_type rt) if (!bdev->filename) goto bdev; +#ifdef __APPLE__ + /* macOS has no udev, so rely on the device naming convention to + * make sure that @filename is a whole disk, not a partition: + * /dev/diskN and /dev/rdiskN are disks, /dev/diskNsM are partitions. + */ + if (darwin_is_partition_name(filename)) { + fprintf(stderr, "Device `%s' looks like a partition.\n" + "You must run %s on the whole disk device, " + "for example /dev/rdiskN\n", filename, __progname); + goto filename; + } +#endif + bdev->fd = bdev_open(filename); if (bdev->fd < 0) { if (errno == EACCES && getuid()) { @@ -885,6 +966,25 @@ struct device *create_block_device(const char *filename, enum reset_type rt) goto filename; } +#ifdef __APPLE__ + /* Resets need Linux's USBDEVFS_RESET, so only RT_NONE is supported. */ + if (rt != RT_NONE) { + fprintf(stderr, "Device resets are not supported on macOS.\n" + "You must disable reset, run %s as follows:\n" + "%s --reset-type=%i %s\n", + __progname, __progname, RT_NONE, filename); + goto fd; + } + bdev->dev.reset = bdev_none_reset; + { + uint64_t block_count; + uint32_t bsize; + assert(!ioctl(bdev->fd, DKIOCGETBLOCKCOUNT, &block_count)); + assert(!ioctl(bdev->fd, DKIOCGETBLOCKSIZE, &bsize)); + block_size = bsize; + bdev->dev.size_byte = block_count * bsize; + } +#else /* Make sure that @bdev->fd is a disk, not a partition. */ udev = udev_new(); if (!udev) { @@ -949,6 +1049,7 @@ struct device *create_block_device(const char *filename, enum reset_type rt) assert(!ioctl(bdev->fd, BLKGETSIZE64, &bdev->dev.size_byte)); assert(!ioctl(bdev->fd, BLKSSZGET, &block_size)); +#endif block_order = ilog2(block_size); assert(block_size == (1 << block_order)); bdev->dev.block_order = block_order; @@ -960,10 +1061,12 @@ struct device *create_block_device(const char *filename, enum reset_type rt) return &bdev->dev; +#ifndef __APPLE__ fd_dev: udev_device_unref(fd_dev); udev: assert(!udev_unref(udev)); +#endif fd: assert(!close(bdev->fd)); filename: From bb9a7aeabe860b65ff7bd86435eecb14c129dec6 Mon Sep 17 00:00:00 2001 From: Nick Clyde Date: Sun, 23 Aug 2026 15:37:12 -0700 Subject: [PATCH 2/2] README: document f3probe and f3brew on macOS Also note that the Docker installation cannot be used on Mac since Docker Desktop does not expose the host's block devices to containers. --- README.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index e62dd66..4aa946f 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,12 @@ user:: .. warning:: This will destroy any previously stored data on your disk! +On Mac, use the raw whole-disk device (e.g. /dev/rdiskN, not /dev/diskNs1), +and unmount the disk right before probing since macOS remounts it +automatically:: + + $ diskutil unmountDisk /dev/diskN && sudo build/f3probe --destructive --time-ops /dev/rdiskN + Correcting capacity to actual size with f3fix --------------------------------------------- @@ -128,10 +134,10 @@ If you want to install f3write and f3read, run the following command:: Compile stable software on Apple Mac ------------------------------------ -f3write and f3read can be installed on Mac, but currently f3probe, f3fix, and -f3brew `require Linux <#the-extra-applications-for-linux>`__. To use them on -Mac, use the `Docker Installation <#docker>`__. For f3write and f3read, read -on. +f3write, f3read, f3probe, and f3brew can be built on Mac, but f3fix +`requires Linux <#the-extra-applications-for-linux>`__ because it depends on +libparted. Note that the `Docker Installation <#docker>`__ does not help on +Mac since Docker Desktop cannot expose the host's block devices to containers. Using HomeBrew ~~~~~~~~~~~~~~ @@ -200,6 +206,14 @@ for details. make ARGP=/opt/local +5) Optionally, build f3probe and f3brew:: + + make extra + + On Mac, f3probe and f3brew do not support resetting USB devices, so + f3brew must be run with :code:`--reset-type=2`. f3fix is not built + because it depends on libparted. + Docker ------ @@ -329,7 +343,8 @@ Compile the extra applications .. note:: - The extra applications are only compiled and tested on Linux - platform. + platform, except that f3probe and f3brew also build on + `Mac <#compile-stable-software-on-apple-mac>`__. - Please do not e-mail me saying that you want the extra applications to run on your platform; I already know that. - If you want the extra applications to run on your platform, help