Re: [PATCH 0/2] mm/memfd: add F_SEAL_PUNCH_HOLE
From: Andy Lutomirski
Date: Thu Oct 29 2026 - 22:11:45 EST
On Thu, Oct 29, 2026 at 5:26 PM Andy Lutomirski <luto@xxxxxxxxxxxxxx> wrote:
>
> On Thu, Oct 29, 2026 at 4:16 PM Samiullah Khawaja <skhawaja@xxxxxxxxxx> wrote:
> >
> > For live update either approach works. If pinned pages cannot be punched
> > or truncated, iommufd can rely on that directly and the seal is not
> > needed. I went with an opt-in seal only to avoid changing existing
> > behaviour. If folks are fine with blocking punch hole and truncate on
> > pinned pages, I can send patches for that instead.
>
> That needs opinions from other people :)
>
> My two cents are that blocking hole punch (or making hole punch fall
> back to writing zeros) seems entirely sensible, but I'm less confident
> I know what to do about truncate.
>
FWIW, I think a demonstration of weird kernel behavior is clearer than
musing based on first principles, so I had a friendly coding agent
write the lovely code at the end of this email. It prints:
Creating and mmapping memfd and registering it
memcpy "Hello, world!" to memfd
MADV_REMOVE the contents
The memfd now contains NULs!
memcpy "memcpy was here" to the memfd
Using io_uring to read the registered buffer
io_uring says: Hello, world!
WRITE_FIXED completed: 15 bytes.
IMO this is utterly disgusting. It makes logical sense based on how
the kernel works under the hood, but I think it makes no sense at all
as a matter of actual observable behavior.
Unsurprisingly, the behavior is identical if MADV_REMOVE is replaced
with ftruncate(0) + fallocate(size).
I can't shake the feeling that the problem here isn't so much that KHO
+ iommofd doesn't like this but that the behavior in question is
nonsensical.
My system's tmpfs does not appear to support FALLOC_FL_COLLAPSE_RANGE,
which is probably for the best :)
---
#define _GNU_SOURCE 1
#include <linux/io_uring.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
[[noreturn]] static void Fail(const char* operation) {
std::perror(operation);
std::exit(1);
}
static void* Map(int fd, size_t size, off_t offset) {
void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, offset);
if (p == MAP_FAILED) Fail("mmap");
return p;
}
// Single-threaded, one outstanding request, using the raw Linux io_uring ABI.
struct Ring {
int fd;
io_uring_params p{};
char* sq;
char* cq;
io_uring_sqe* sqes;
size_t sq_size, cq_size;
Ring() {
fd = static_cast<int>(syscall(SYS_io_uring_setup, 2, &p));
if (fd < 0) Fail("io_uring_setup");
sq_size = p.sq_off.array + p.sq_entries * sizeof(unsigned);
cq_size = p.cq_off.cqes + p.cq_entries * sizeof(io_uring_cqe);
if (p.features & IORING_FEAT_SINGLE_MMAP) {
if (cq_size > sq_size) sq_size = cq_size;
cq = sq = static_cast<char*>(Map(fd, sq_size, IORING_OFF_SQ_RING));
} else {
sq = static_cast<char*>(Map(fd, sq_size, IORING_OFF_SQ_RING));
cq = static_cast<char*>(Map(fd, cq_size, IORING_OFF_CQ_RING));
}
sqes = static_cast<io_uring_sqe*>(Map(fd, p.sq_entries *
sizeof(*sqes), IORING_OFF_SQES));
}
~Ring() {
munmap(sqes, p.sq_entries * sizeof(*sqes));
if (cq != sq) munmap(cq, cq_size);
munmap(sq, sq_size);
close(fd);
}
int WriteFixed(int output, const char* buffer, unsigned length,
uint64_t offset) {
auto* tail = reinterpret_cast<unsigned*>(sq + p.sq_off.tail);
const unsigned next = *tail;
const unsigned slot = next & *reinterpret_cast<unsigned*>(sq +
p.sq_off.ring_mask);
io_uring_sqe& entry = sqes[slot];
entry = {};
entry.opcode = IORING_OP_WRITE_FIXED;
entry.fd = output;
entry.off = offset;
entry.addr = reinterpret_cast<uintptr_t>(buffer);
entry.len = length;
entry.buf_index = 0;
reinterpret_cast<unsigned*>(sq + p.sq_off.array)[slot] = slot;
__atomic_store_n(tail, next + 1, __ATOMIC_RELEASE);
long submitted;
do {
submitted = syscall(SYS_io_uring_enter, fd, 1, 0, 0, nullptr, 0);
} while (submitted < 0 && errno == EINTR);
if (submitted != 1) {
if (submitted >= 0) errno = EIO;
Fail("io_uring_enter submit");
}
auto* head = reinterpret_cast<unsigned*>(cq + p.cq_off.head);
auto* cq_tail = reinterpret_cast<unsigned*>(cq + p.cq_off.tail);
const unsigned current = *head;
while (__atomic_load_n(cq_tail, __ATOMIC_ACQUIRE) == current) {
if (syscall(SYS_io_uring_enter, fd, 0, 1,
IORING_ENTER_GETEVENTS, nullptr, 0) < 0
&& errno != EINTR) Fail("io_uring_enter wait");
}
auto* completions = reinterpret_cast<io_uring_cqe*>(cq + p.cq_off.cqes);
const int result = completions[current &
*reinterpret_cast<unsigned*>(cq + p.cq_off.ring_mask)].res;
__atomic_store_n(head, current + 1, __ATOMIC_RELEASE);
return result;
}
};
int main() {
printf("Creating and mmapping memfd and registering it\n");
constexpr size_t size = 16 * 1024;
const int fd = memfd_create("registered-buffer", MFD_CLOEXEC);
if (fd < 0) Fail("memfd_create");
if (fallocate(fd, 0, 0, size) < 0) Fail("fallocate");
auto* buffer = static_cast<char*>(Map(fd, size, 0));
Ring ring;
iovec iov{buffer, size};
if (syscall(SYS_io_uring_register, ring.fd,
IORING_REGISTER_BUFFERS, &iov, 1) < 0)
Fail("IORING_REGISTER_BUFFERS");
printf("memcpy \"Hello, world!\" to memfd\n");
constexpr char hello[] = "Hello, world!";
std::memcpy(buffer, hello, sizeof(hello));
printf("MADV_REMOVE the contents\n");
// MADV_REMOVE operates on whole pages. Remove the entire 16 KiB mapping.
if (madvise(buffer, size, MADV_REMOVE) < 0) Fail("MADV_REMOVE");
for (size_t i = 0; i < size; ++i) {
if (buffer[i] != '\0') {
std::fprintf(stderr, "MADV_REMOVE verification failed at
byte %zu\n", i);
return 1;
}
}
std::fprintf(stderr, "The memfd now contains NULs!\n");
printf("memcpy \"memcpy was here\" to the memfd\n");
constexpr char message[] = "memcpy was here";
std::memcpy(buffer, message, sizeof(message));
printf("Using io_uring to read the registered buffer\nio_uring says: ");
fflush(stdout);
const unsigned length = static_cast<unsigned>(std::strlen(message));
int result = ring.WriteFixed(STDOUT_FILENO, buffer, length, UINT64_MAX);
if (result < 0) {
Fail("WRITE_FIXED");
}
if (result != static_cast<int>(length)) {
std::fprintf(stderr, "Short WRITE_FIXED: %d of %u bytes\n",
result, length);
return 1;
}
std::fprintf(stderr, "\nWRITE_FIXED completed: %d bytes.\n", result);
if (syscall(SYS_io_uring_register, ring.fd,
IORING_UNREGISTER_BUFFERS, nullptr, 0) < 0)
Fail("IORING_UNREGISTER_BUFFERS");
munmap(buffer, size);
close(fd);
}
--
Andy Lutomirski
AMA Capital Management, LLC