Skip to content

process_dedupes() can loop forever if a successful FIDEDUPERANGE reports zero bytes #415

Description

@matthiasgoergens

process_dedupes() in dedupe.c advances by info->bytes_deduped and re-queues the request whenever the ioctl succeeded and req_total has not yet reached orig_len:

req->req_loff  += info->bytes_deduped;
req->req_total += info->bytes_deduped;

if (info->status || req->req_total >= ctxt->orig_len) {
	... completed
} else {
	/* put us back on the queued list for another go around */

If a successful FIDEDUPERANGE reports zero bytes deduped, that re-queues a request with identical parameters and dedupe_extents() never terminates.

duperemove does not hit this today, and I have not seen it hang. set_aligned_same_length() rounds src_length down to fs_blocksize, and I traced a -dr run and a --dedupe-options=partial -b 4096 run over a mixed corpus: every request was block-aligned, or was a whole-file dedupe whose range reaches EOF. So this is defence in depth, not a live bug report.

It is worth guarding anyway, because the input is real rather than hypothetical.

The kernel side

vfs_dedupe_file_range() reports the requested length rather than the amount actually deduplicated. generic_remap_check_len() silently rounds an unaligned length down to a block multiple when the destination range is not at EOF, and the loop then records len where deduped holds the real figure.

That means a sub-block request away from EOF currently comes back as "success, N bytes" having deduplicated nothing at all. On btrfs, stock v7.2-rc5:

requested=2048  bytes_deduped=2048  status=0
requested=1     bytes_deduped=1     status=0

The kernel reports deduplicating one byte, on a filesystem with 4096-byte granularity.

This was fixed once — 4a57a8400075 — and reverted the next day in b926f2adb044 because generic/517 asserts the buggy figure as its expected output. Matthias Goergens has a revival of that fix in the works.

With the fix applied (same tree, same corpus, btrfs in a VM), those become:

requested=2048  bytes_deduped=0  status=0
requested=1     bytes_deduped=0  status=0

and a plain rmlint --dedupe small.bin big.bin, where big begins with small's content, elicits it in ordinary use on its second call:

src_length=100000            =>  bytes_deduped=98304, status=0
src_offset=98304, len=1696   =>  bytes_deduped=0,     status=0

rmlint survives because it happens to guard against zero. duperemove would re-queue that request forever.

Why fix it here regardless

Even once the kernel change lands, duperemove will keep running on kernels that do not have it — every LTS since 4.19 over-reports. A one-condition guard makes duperemove correct on both old and new kernels, and means it is not the reason the kernel side stays reverted.

PR follows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions