FreeBSD fspacectl(2): Deallocate File Ranges Without Assuming Hole Punching
Use FreeBSD fspacectl(2) with explicit offsets, inspect partial progress, and distinguish zeroing a range from guaranteed storage reclamation.
FreeBSD’s fspacectl(2) is a file-descriptor API for space-management operations at explicit byte ranges. Its current operation, SPACECTL_DEALLOC, zeros a requested region. If the filesystem supports hole punching, it may also deallocate filesystem space in that region. Those are related effects, but they are not the same guarantee: a successful call does not promise that every filesystem will return physical blocks to its free-space pool.
The system call appeared in FreeBSD 14.0. Check the manual for the exact release you deploy, because support and storage behavior depend on both the operating system and the filesystem underneath the descriptor.
The request is an offset and a length
Include <fcntl.h> for the declaration and struct spacectl_range. The request contains r_offset and r_len; it does not depend on or change the file descriptor’s current seek position. SPACECTL_DEALLOC requires a nonnegative offset and positive length, and the offset-plus-length must not exceed OFF_MAX. The flags argument must currently be zero.
The following intentionally destructive example is for a disposable regular file whose allocated contents you are willing to zero. fd must refer to a file opened with write permission. For these values, make the test file larger than the request end (r_offset + r_len, or 5 MiB), and preserve any data you need. This illustrative sample has not been compiled or run on FreeBSD; validate it in a disposable test environment before integrating it.
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <unistd.h>
int
deallocate_test_range(int fd)
{
const struct spacectl_range request = {
.r_offset = 1024 * 1024,
.r_len = 4 * 1024 * 1024,
};
struct spacectl_range remainder = request;
if (fspacectl(fd, SPACECTL_DEALLOC, &request, 0, &remainder) == -1) {
int saved_errno = errno;
fprintf(stderr, "range reported as unprocessed: offset=%jd length=%jd\n",
(intmax_t)remainder.r_offset, (intmax_t)remainder.r_len);
errno = saved_errno;
perror("fspacectl");
errno = saved_errno;
return -1;
}
if (remainder.r_len != 0) {
fprintf(stderr, "unprocessed range: offset=%jd length=%jd\n",
(intmax_t)remainder.r_offset, (intmax_t)remainder.r_len);
}
return 0;
}
The result range is useful operational evidence. When there is no unprocessed portion after successful completion, rmsr->r_len is zero and rmsr->r_offset reflects the request offset plus the bytes zeroed before end-of-file. If a portion remains, do not blindly replay the original range: preserve the reported remainder and decide whether a retry is appropriate for the file and error condition.
Filesystem support determines reclamation
The API’s zeroing semantics are the portable part of the request. Space deallocation is conditional on filesystem hole-punch support. A filesystem may preserve the logical file size while representing the range sparsely; another may not release blocks. Measure the result with filesystem-specific tools and tests rather than inferring it from a zero return code or from du alone.
This distinction also separates fspacectl from device-level discard or ZFS pool TRIM. The call operates on a file range; it does not directly issue an NVMe deallocate command, trim an entire pool, or promise that a storage device erased old physical data. Do not use it as a secure-erasure primitive.
Failure handling belongs in the caller
The manual documents EBADF for an invalid descriptor or one without write access; EFAULT for an invalid request or result pointer; EINVAL for invalid commands, ranges, overflow, or flags; EINTR if a signal was caught during execution; ENODEV when the descriptor’s file does not support the call; and ENOSPC, EIO, EINTEGRITY, ENOTCAPABLE, and ESPIPE for other documented failure conditions. Keep the original requested range, result range, errno, filesystem type, and FreeBSD release in diagnostic logs. A failure is not proof that no data changed, so inspect the output range if your application supplied rmsr and then test the target release’s behavior before automating retries.
For capacity-sensitive workloads, test a representative file on every filesystem type and mount configuration in scope. Record logical file size, allocated blocks, and application-visible reads before and after the operation. Confirm that the actual consumer benefits; the API’s name alone does not establish reclaim behavior.
Related:
- FreeBSD’s VFS Layer: How Multiple Filesystems Share One Interface
- FreeBSD ZFS TRIM Operations: Reclaim Free Space Without Surprises
Sources: