From 8924d5e8f2811a58b21f280dffeeab5a306daaaf Mon Sep 17 00:00:00 2001 From: Martin Leitner-Ankerl Date: Thu, 23 Jul 2026 15:30:59 +0200 Subject: [PATCH] dedupe: clamp the source range to the file size to avoid EINVAL (#398) The kernel rejects the entire FIDEDUPERANGE ioctl with EINVAL when the source range extends past the end of the source (ioctl) file - the "off + len > i_size_read(src)" check in vfs_dedupe_file_range(). Extent lengths come from fiemap's fe_length, which is rounded up to the filesystem block size, so a file's final extent typically reports a length that overshoots the real end of file (a file that shrank since it was scanned does the same). Using that as the dedupe source range fails the whole batch with "Dedupe ioctl returns 22: Invalid argument" even though the data is genuinely deduplicable. Clamp the request to the source file's current size before the ioctl. If the source offset is now entirely past EOF, move the queued requests to the completed list and skip the doomed ioctl. A too-short destination is unaffected - the kernel reports that per-file via info->status. Fixes: https://github.com/markfasheh/duperemove/issues/398 Co-Authored-By: Claude Opus 4.8 --- dedupe.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/dedupe.c b/dedupe.c index 0ade18eeeccf..eb1f4e7a2761 100644 --- a/dedupe.c +++ b/dedupe.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -310,10 +311,63 @@ static void process_dedupes(struct dedupe_ctxt *ctxt, } } +/* + * The kernel rejects the *entire* FIDEDUPERANGE ioctl with EINVAL when the + * source range extends past the end of the source (ioctl) file, see the + * "off + len > i_size_read(src)" check in vfs_dedupe_file_range(). + * + * This happens more often than one might expect: extent lengths are recorded + * from fiemap's fe_length, which is rounded up to the filesystem block size, + * so a file's final extent typically reports a length that overshoots the real + * end of file. It can also happen if a file shrank since it was scanned. Either + * way, clamp our request to the file's current size so we dedupe what actually + * exists instead of failing the whole batch (a too-short *destination* file is + * fine, the kernel reports that per-file via info->status). + */ +static void clamp_len_to_ioctl_file(struct dedupe_ctxt *ctxt) +{ + struct stat st; + uint64_t src_size; + + if (fstat(ctxt->ioctl_file->fd, &st)) + return; /* Let the ioctl surface whatever the real error is */ + + src_size = st.st_size; + + if (ctxt->ioctl_file_off + ctxt->len <= src_size) + return; + + if (ctxt->ioctl_file_off >= src_size) { + /* + * The source range no longer exists at all. Move every queued + * request to the completed list so the caller cleans up without + * issuing a doomed ioctl. + */ + dprintf("Skipping dedupe: source offset %llu is past the end " + "of file \"%s\" (size %llu)\n", + (unsigned long long)ctxt->ioctl_file_off, + ctxt->ioctl_file->filename, + (unsigned long long)src_size); + list_splice_init(&ctxt->queued, &ctxt->completed); + ctxt->num_queued = 0; + return; + } + + dprintf("Clamping dedupe length for \"%s\" from %llu to %llu to fit " + "source file size %llu\n", ctxt->ioctl_file->filename, + (unsigned long long)ctxt->len, + (unsigned long long)(src_size - ctxt->ioctl_file_off), + (unsigned long long)src_size); + + ctxt->len = ctxt->orig_len = src_size - ctxt->ioctl_file_off; +} + int dedupe_extents(struct dedupe_ctxt *ctxt) { int ret = 0; + clamp_len_to_ioctl_file(ctxt); + while (!list_empty(&ctxt->queued)) { /* Convert the queued list into an actual request */ populate_dedupe_request(ctxt, ctxt->same);