Last active
July 28, 2026 22:33
-
-
Save patrickdk77/503c31f8dd206e65b635d6c1e06257a2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| commit 92bd9d9772d851f71a6c9bc83f6825eba969744d (HEAD -> fulldedupe) | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Tue Jul 28 12:26:58 2026 -0400 | |
| Return a negative error from the writepage callback | |
| zfs_putpage() reports failures as positive ZFS errnos: it assigns | |
| EDQUOT directly, and passes on the values from | |
| zfs_enter_verify_zp(), dmu_tx_assign() and zil_commit_flags(). | |
| zpl_putpage() handed that straight back to the kernel, but it is | |
| the writepage callback for both .writepage and .writepages, and | |
| those must return a negative error or zero. | |
| The writeback loop notices and complains. Running the ENOSPC | |
| soak in fstests generic/269 produced: | |
| WARNING: CPU: 3 PID: 7320 at mm/page-writeback.c:2568 | |
| writeback_iter+0x267/0x2d0 | |
| __writeback_single_inode | |
| writeback_single_inode | |
| write_inode_now | |
| zfs_clone_range | |
| which is reached because zfs_clone_range() flushes the source | |
| through write_inode_now() before cloning. Only tests that run the | |
| filesystem out of space hit it, since that is the only way | |
| zfs_putpage() returns non-zero. The bad value was also recorded as | |
| the mapping's writeback error, so the error reported to userspace | |
| was wrong too, not just the log message. | |
| zpl_writepages() was already inconsistent with itself here: the | |
| zil_commit_flags() failure it returns is negated, while the value | |
| from write_cache_pages() was not. Negate in zpl_putpage(), as every | |
| other wrapper in this file does. | |
| generic/269 fails with that warning before this change and passes | |
| without it after, and so does generic/083. Across a full fstests | |
| auto run on five pool topologies (single disk ashift 9 and 12, | |
| 3-disk stripe, mirror, raidz1) both tests pass on every topology | |
| and no kernel warning is reported anywhere, against five in the | |
| same run before the fix. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit b472eba4e9465a567683200c5f59ae98b7f00650 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Tue Jul 28 00:11:25 2026 -0400 | |
| zvol: give up the clone txg wait when the pool suspends | |
| zvol_clone_range() holds RL_WRITER on the destination range, | |
| RL_READER on the source range, and zv_suspend_lock across its clone | |
| loop. When dmu_read_l0_bps() reports EAGAIN for a block dirtied in | |
| the open txg and zfs_bclone_wait_dirty is set, the loop waits for the | |
| next txg with a plain txg_wait_synced(). If the pool suspends during | |
| that wait it never returns, so the thread parks unkillably while | |
| holding all three locks and blocks every other I/O to both zvols. | |
| zfs_clone_range_locked() already handles this: it passes | |
| TXG_WAIT_SUSPEND when the pool failmode is continue, then maps | |
| ESHUTDOWN to EIO so the loop can break out and drop its locks. Do the | |
| same here. | |
| Found by inspection while auditing range lock use across the tree, | |
| prompted by an unrelated clone hang. It was not reproduced, since | |
| provoking it needs a pool suspend during a clone of a block dirtied | |
| in the current txg. The change is build tested and the ZPL clone | |
| tests (generic/733, 154, 430, 434) still pass, but note those cover | |
| zfs_clone_range(), not this zvol path. | |
| znode: assert the range lock is empty when a znode is recycled | |
| zfs_rangelock_init() for z_rangelock runs in the znode slab | |
| constructor, not per allocation, so the lock outlives any single | |
| znode and is handed to whatever file next reuses the object. Nothing | |
| checks that its tree is empty when the znode is freed. A range left | |
| locked by any znode, an xattr znode, a directory, a since-deleted | |
| file, therefore survives kmem_cache_free() and reappears in the tree | |
| of an unrelated file, where it blocks that file forever with no | |
| thread holding anything and no stack to point at. | |
| Assert the tree is empty in zfs_inode_free() before the object goes | |
| back to the cache, and again in zfs_inode_alloc() after it comes | |
| out. Under ZFS_DEBUG this turns a leak into a failure at the znode | |
| that leaked it, instead of an unexplained hang later on a different | |
| file. The assertions compile out of release builds. | |
| This was written while chasing exactly that symptom: an FICLONERANGE | |
| on a file blocked in zfs_rangelock_enter() for hours while no task in | |
| the system held a ZFS lock. An audit of every zfs_rangelock_enter() | |
| call site found no unbalanced path, which is what pointed at the | |
| recycled slab object rather than the clone path. | |
| The assertions have not fired so far: 24 runs of the xattr, clone and | |
| fiemap tests, plus a full xfstests auto sweep, all clean. They stand | |
| as a net for the class rather than a fix for a known leak. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit b12826d169695c75a29227704282ec8ce3e549c4 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 21:12:26 2026 -0400 | |
| Cover holes and overridden BPs in the clone pending-free check | |
| f74a384ae2 made dmu_read_l0_bps() return EAGAIN for a source block with | |
| a pending free, so a clone cannot add a BRT reference to a block about | |
| to be freed. Review on #18853 pointed out two cases that check missed; | |
| handle both the way dbuf_read_hole() already does. | |
| - A hole (or absent BP) has nothing to free. The check ran | |
| unconditionally, so a hole lying inside a pending free range | |
| returned EAGAIN where the block should just be cloned as a hole. | |
| dnode_free_range() records a range whether or not the blocks in it | |
| are holes, so this also covers a block never written inside a | |
| pending punch range. With zfs_bclone_wait_dirty=1 that costs an | |
| extra txg wait and resolves on the retry; with it unset | |
| copy_file_range falls back to a plain copy, but FICLONE has nowhere | |
| to fall back to and fails outright where master cloned the hole. | |
| Guard the check with !BP_IS_HOLE(bp) and let a NULL bp reach the | |
| existing hole handling below. | |
| - When the head dirty record already overrode the BP (dr_brtwrite -- | |
| cloning a clone made in the same txg), the overridden pointer was | |
| returned with no check at all. A punch landing in a txg after a | |
| still-unsynced clone leaves that override in place: dbuf_free_range() | |
| only unoverrides records from its own txg and skips cloned dbufs as | |
| DB_NOFILL, so cloning the clone captures the overridden pointer with | |
| the newer free pending against it and double frees it through that | |
| branch. Only frees after the override count against it, so use | |
| dnode_block_freed_after(dn, db->db_blkid, dr->dr_txg). | |
| Verified on a debug build with fstests generic/733 and with a FICLONE of | |
| a sparse file in a pending free range, which fails before this change | |
| (EAGAIN) and succeeds after; block cloning and BRT space accounting are | |
| unaffected. | |
| Suggested-by: mkhllr | |
| Refs: https://github.com/openzfs/zfs/pull/18853 | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 2e3f588b6df2153437a1232f220cabe2616d7ea6 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 20:08:44 2026 -0400 | |
| Don't assert a valid spill bp when the spill write failed | |
| dbuf_write_ready() asserts that a DMU_SPILL_BLKID dbuf has a non-hole | |
| block pointer matching the dnode's spill blkptr. When the write failed | |
| -- e.g. DVA allocation returns ENOSPC because the pool's only vdev is | |
| failing -- the spill bp is left a hole (no DVA) and the ZIO still runs | |
| its ready callback carrying io_error. The regular-block path above | |
| already tolerates a hole bp (it guards the type/level asserts on | |
| BP_GET_BIRTH != 0), but the spill-only assert does not, so a debug | |
| build panics. | |
| generic/753 (log recovery under repeated dm-error disk failures + xattr | |
| fsstress) reproduces it: the moment dm-error returns EIO, several | |
| in-flight spill writes reach dbuf_write_ready with hole bps and | |
| io_error == ENOSPC (pool not yet suspended), tripping the assert. The | |
| failed txg is retried once the pool resumes, so the hole is transient; | |
| on a non-debug build the assert is compiled out and the pool suspends | |
| and recovers normally. | |
| Only require a valid allocated spill bp when the write succeeded. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 9c653d9077d671b80c67f71db43723e7cb021d39 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 17:36:04 2026 -0400 | |
| Strip security capabilities on write | |
| zpl_iter_write() cleared set-ID mode bits (via | |
| zfs_clear_setid_bits_if_necessary()) but never removed the | |
| security.capability xattr, so writing to a file left its file | |
| capabilities intact (generic/093). The generic VFS write path drops | |
| them in file_remove_privs(), which ZFS bypasses. | |
| Call file_remove_privs() at the top of zpl_iter_write(). It needs | |
| the inode lock only when it has something to remove, so set SB_NOSEC | |
| on the superblock and gate the call on IS_NOSEC(): once a file is | |
| known to carry no set-ID bits or capabilities the write path skips | |
| both the lock and the call, leaving concurrent writes unserialized. | |
| S_NOSEC is cleared by the VFS on any security.* setxattr and is never | |
| set on a freshly instantiated inode, so a later setcap is still seen. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 63f44e0dc7f5b4e62128361a706bcc213e90bdbc | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 17:35:33 2026 -0400 | |
| Fix set-user-ID/set-group-ID clearing on write and chown | |
| ZFS open-codes set-ID clearing rather than going through the VFS | |
| file_remove_privs()/notify_change() path, and both copies diverged | |
| from should_remove_suid(): | |
| - zfs_clear_setid_bits_if_necessary() (write path) only cleared a | |
| set-ID bit when at least one execute bit was set, and cleared | |
| S_ISUID and S_ISGID together. A direct write to a file with no | |
| execute bits (-rwSr-Sr--) left set-user-ID in place (generic/355). | |
| - secpolicy_setid_clear() (chown/truncate path) cleared both | |
| S_ISUID and S_ISGID whenever either was set, stripping | |
| set-group-ID from a file with no group-execute bit -- a | |
| mandatory-locking marker that must be preserved (generic/193). | |
| Match should_remove_suid(): always clear S_ISUID, and clear S_ISGID | |
| only when S_IXGRP is set. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 962dbd343d10bd09710170aa85db5681db44ff35 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 16:21:52 2026 -0400 | |
| Zero the last partial page's tail past EOF when extending a file | |
| The tail of a file's last partial page, past EOF, can hold stale bytes | |
| that were never persisted, most easily via an mmap store past EOF, | |
| which the kernel lets land in the page but which zfs_putpage() clamps | |
| out of the writeback. Those bytes must read back as zero. | |
| When the file is later extended to cover them, by a write past EOF | |
| that leaves a gap, or by a truncate or fallocate that grows the file, | |
| ZFS left the stale bytes in the cached page. A subsequent read then | |
| returned them, and the next writeback (now clamped to the larger EOF) | |
| persisted them to disk. generic/363 (fsx -e, "pollute EOF") hits | |
| this; the bytes are visible past the file's real data in the page | |
| cache and, after writeback, on disk (confirmed with zdb). | |
| Zero the old partial-page tail before the file grows to expose it: | |
| - zfs_extend() (truncate-up and fallocate) already holds the whole- | |
| file range lock, so it calls zfs_zero_partial_page() directly. | |
| - zfs_write() only locks the range it writes, which does not cover | |
| the old EOF when the write leaves a gap; it takes a brief range | |
| lock over just the old last page and re-reads its tail from the | |
| DMU (a hole past the old EOF, hence zeros) before the main write. | |
| - zfs_clone_range() extends the destination as well and had the | |
| same gap. Its destination range lock starts at the clone offset, | |
| so like zfs_write() it does not cover the old EOF page; it now | |
| takes the same brief lock and re-read first. A clone placed past | |
| a partial last page would otherwise expose that page's stale | |
| tail. | |
| All three are gated on the page being cached, so the common path is | |
| unaffected. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit e58150f5e12bab92a6ae0edd9851eb42ae12b25e | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 15:01:21 2026 -0400 | |
| Honor RLIMIT_FSIZE and the maximum file size when extending a file | |
| A mode 0 fallocate (no FALLOC_FL_KEEP_SIZE) that extends the file must | |
| fail when the new size would exceed RLIMIT_FSIZE or the filesystem's | |
| maximum file size, exactly as a write() that extends the file does. | |
| zpl_fallocate_common() checked neither, so preallocating past | |
| RLIMIT_FSIZE silently succeeded instead of raising SIGXFSZ and | |
| returning EFBIG (generic/228). | |
| Call inode_newsize_ok(), the standard VFS size-limit check, before | |
| extending the file: it raises SIGXFSZ and returns EFBIG past | |
| RLIMIT_FSIZE, and returns EFBIG past the maximum file size, matching | |
| how ext4 and xfs gate a fallocate that grows the file. | |
| Two more paths grow a file and need the same check. A | |
| FALLOC_FL_ZERO_RANGE without FALLOC_FL_KEEP_SIZE reaches zfs_extend() | |
| by way of zfs_space() with no check at all. FICLONE, FICLONERANGE and | |
| copy_file_range() reach zfs_clone_range(), which calls | |
| zn_rlimit_fsize(); that is the real check only on FreeBSD, and expands | |
| to 0 on Linux. Nothing else covers either path: the VFS makes this | |
| check in generic_remap_file_range_prep(), which ZFS does not call. So | |
| ulimit -f did not bound a reflink on Linux at all, neither EFBIG nor | |
| SIGXFSZ. | |
| Apply inode_newsize_ok() to both, on the destination inode and under | |
| its lock. | |
| generic/514 covers the reflink case and passed over the bug. It fails | |
| only when the destination reaches the full size, and a clone that | |
| fails leaves the destination at zero bytes, which the test accepts. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 294f360792b25085f780b85a9adabf9506217018 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Mon Jul 27 14:47:08 2026 -0400 | |
| Fix data loss when fallocate races an O_DIRECT append write | |
| zpl_fallocate_common() extends a file (mode 0, no FALLOC_FL_KEEP_SIZE) | |
| by sampling the current i_size once and, when offset+len is larger, | |
| calling zfs_freesp(zp, offset+len, 0, ...), the len==0 "set the file | |
| size to off" idiom. | |
| An O_DIRECT append write updates the file size outside the inode lock | |
| (ZFS serializes direct writes with the range lock, not i_rwsem or | |
| inode_dio_wait), so it can grow the file past offset+len after | |
| fallocate sampled the old size but before zfs_freesp() runs. | |
| zfs_freesp() then treats the now-smaller offset+len as a truncation: | |
| it either clamps i_size back down in truncate_setsize(), hiding the | |
| write's data past the reported EOF, or, when the size check races the | |
| other way, calls zfs_trunc() and frees the block the write just | |
| added. generic/586 reproduces this: the appended block is on disk | |
| (zdb reports the larger dnode size) while stat() reports the smaller. | |
| Extend the file with zfs_extend() instead, which is grow-only: it does | |
| nothing when the file already reached the target size, so a concurrent | |
| write that grew it further is never truncated. zfs_extend() gains a | |
| 'log' argument so the fallocate path keeps the mtime/ctime and z_seq | |
| updates and the TX_TRUNCATE ZIL record that zfs_freesp() emitted (a | |
| following fsync stays crash-durable); the two existing callers in | |
| zfs_freesp() pass B_FALSE and log as before. | |
| The FALLOC_FL_KEEP_SIZE path samples the same size and had a second | |
| instance of the bug. It skips the request when offset is past EOF and | |
| otherwise clamps len to olen - offset, so at exactly EOF len becomes | |
| zero. zfs_space() reads a zero length as "to the end of the file" and | |
| zfs_freesp() turns that into zfs_trunc(): a size-changing operation on | |
| the path that promised to keep the size, and one that discards an | |
| O_DIRECT append that grew the file after olen was read. Skip the | |
| request at or past EOF instead. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 94e3ad71114a301a502d6398aefdc758a900efa0 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Sun Jul 26 22:10:23 2026 -0400 | |
| Fix self-deadlock when cloning a range within the same file | |
| zfs_clone_range() takes an RL_READER range lock on the source range | |
| and an RL_WRITER range lock on the destination range. When the | |
| source and destination are the same file (inzp == outzp) both locks | |
| are on the same znode's rangelock. If the destination write has to | |
| grow the file's block size, zfs_rangelock_enter() grows the writer | |
| lock to cover the whole file (see zfs_rlock.c), which then conflicts | |
| with the source reader held by the same thread. The result is an | |
| unrecoverable self-deadlock (the thread waits forever in | |
| zfs_rangelock_enter_impl(), D state, freed only by a reboot). | |
| Reproduced by fstests generic/564, whose copy_range probe does a | |
| sub-recordsize copy_file_range() within one file that extends it (so | |
| the destination block size grows). | |
| For a same-file clone, take a single writer lock spanning both ranges | |
| instead of a separate reader and writer. The source block pointers | |
| are read before zfs_clone_range_locked() reduces the lock, so the | |
| source stays covered, and a single lock cannot conflict with itself. | |
| Clones between different files are unchanged. | |
| zfs_dedupe_range() takes two locks on one znode the same way and does | |
| not need the same treatment, but only because it never extends the | |
| file. Both ranges are checked to lie within their file before the | |
| locks are taken, so end_size in zfs_rangelock_cb() is just z_size and | |
| its block growth test can never hold. A concurrent shrink cannot | |
| invalidate that, because the caller holds the inode lock, exclusively | |
| when the two files are the same, and the only size update outside it | |
| (an O_DIRECT append) only grows the file. Record the invariant next | |
| to the locks, so a later change that lets dedupe extend the file does | |
| not quietly reintroduce this deadlock. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit c5361000aec8edde6d8e75094a80d85f19d1d197 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Sun Jul 26 15:30:39 2026 -0400 | |
| Fix double free when cloning a block with a pending free | |
| dmu_read_l0_bps() captures the source block pointers for a clone | |
| (FICLONE / copy_file_range). For a block with no dirty record it | |
| used db->db_blkptr directly, without checking dnode_block_freed(). | |
| A block with a pending free -- e.g. a truncate that has recorded the | |
| block in the dnode's free ranges but has not yet synced -- still has | |
| a valid on-disk block pointer, so it could be cloned. The clone then | |
| registers a BRT reference to a block that is about to be returned to | |
| the allocator, and the block is freed twice: once for the pending | |
| free, and again when the clone reference is later released. | |
| On a debug build this trips the metaslab double-free verifier in the | |
| free bpobj drain (panic: "segment already in tree"); on a release | |
| build the space map is silently corrupted. | |
| The buffered read path already guards against this: dbuf_read_hole() | |
| treats a block with a pending free as a hole via dnode_block_freed(). | |
| Apply the same check in dmu_read_l0_bps() and return EAGAIN when the | |
| source block has a pending free. zfs_clone_range() already handles | |
| EAGAIN by waiting for the next TXG and retrying (or falling back to a | |
| copy), by which point the free has synced and the block reads as a | |
| hole. | |
| Reproduced with fstests generic/733, which panicked within a minute | |
| on a debug build and now runs cleanly across repeated runs; block | |
| cloning and BRT space accounting are unaffected (clone data compares | |
| identical, bclonesaved is correct, scrub is clean). | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| Closes #18842 | |
| commit 490936de578753f20c95d1bbb26fb35abc2a6c40 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Sun Jul 26 14:10:33 2026 -0400 | |
| Fix Direct I/O handling of blocks with a pending free | |
| Commit 9bf75b4b13 ("Fix reads for blocks freed after being cloned") | |
| taught the buffered read path, dbuf_read_hole(), to treat a block | |
| with a pending free as a hole: it consults dnode_block_freed(), or | |
| dnode_block_freed_after() when the block was overridden by a block | |
| clone or Direct I/O write so that only frees from TXGs after the | |
| override count. | |
| The Direct I/O read path in dmu_read_abd() was never updated to | |
| match. It issues a read from the block pointer whenever the bp is | |
| valid and the dbuf is not DB_CACHED, so a block freed by a not yet | |
| synced truncate (whose bp is still valid) returns stale data from | |
| the old block instead of zeros. | |
| Apply the same pending-free check to dmu_read_abd(). The DB_CACHED | |
| case is now handled first and always copies the dbuf's data, which | |
| already reflects any pending write or free, exactly as the buffered | |
| path returns db_buf for a cached dbuf. The pending-free check is | |
| consulted only on the uncached path that would otherwise read the | |
| stale bp. This is the Direct I/O counterpart to 9bf75b4b13. | |
| The Direct I/O write path had the matching gap. dmu_sync() disables | |
| nopwrite when the current block pointer could change before the TXG | |
| syncs, and it can change in two ways: the dbuf is dirty again, or | |
| the block is being freed (dnode_block_freed()). dmu_write_direct() | |
| tested only the first. A nopwrite that keeps the old bp because the | |
| data compares equal, while a pending truncate frees that very block, | |
| leaves the dnode pointing at freed space. Test both, as dmu_sync() | |
| does. | |
| fsx with O_DIRECT reproduced the read corruption within ~100k | |
| operations and now runs millions of operations cleanly. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 991b25565794297a3bb87a3c87e639629b1abb78 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Fri Jul 24 19:00:32 2026 -0400 | |
| zpl: report a short clone's real error instead of EINVAL | |
| FICLONE and FICLONERANGE cannot shorten a clone; the whole | |
| requested range must be cloned. zfs_clone_range() can still | |
| return a short clone: on partial progress it clears the | |
| underlying error and returns the bytes already cloned, which is | |
| correct for copy_file_range(). For FICLONE, | |
| zpl_remap_file_range() then turned that short result into EINVAL, | |
| discarding the real reason the clone stopped. | |
| The most visible case is ENOSPC. A clone that fills the pool | |
| partway through reported EINVAL ("Invalid argument") to userspace | |
| instead of ENOSPC ("No space left on device"); EDQUOT and EIO | |
| were hidden the same way. generic/333 and generic/334 expose it: | |
| they clone a file in a loop while writing to it, expect ENOSPC as | |
| the pool fills, and instead fail on the masked EINVAL. | |
| For the no-shorten path, retry the remaining range rather than | |
| converting the first short clone to EINVAL. Each retry either | |
| makes further progress or returns the real hard error, which then | |
| propagates to the caller; EINVAL is returned only when the clone | |
| genuinely cannot make progress. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 466bfa04fbb9c2e51318f225570bebe054a4b397 | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Fri Jul 24 15:41:12 2026 -0400 | |
| zfs_clone_range: allow a partial-tail clone past the dest EOF | |
| A clone (FICLONE/FICLONERANGE) whose length is not a multiple of | |
| the block size is only valid when the range reaches the end of | |
| both the source and the destination. zfs_clone_range() enforced | |
| that by comparing the length against z_size minus the offset for | |
| each file. | |
| When the destination offset is beyond the destination's current | |
| size (the clone extends the file, so the trailing partial block | |
| lands at the new EOF), that subtraction underflows (z_size and the | |
| offset are both uint64_t), so the term is always true and the | |
| clone is wrongly rejected with EINVAL. | |
| generic/144 hits this: it reflinks a partial-tail range into a | |
| file at an offset past its current EOF and expects it to succeed. | |
| Bounds-check each side (offset < size) before subtracting, so a | |
| clone that extends the destination past its current EOF is | |
| allowed while a genuine mid-file partial block is still rejected. | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
| commit 1c158bede18d1eed2fe5eb970dfe7e34d8f79a4e | |
| Author: Patrick Domack <patrickdk@patrickdk.com> | |
| Date: Wed Jul 22 03:27:55 2026 -0400 | |
| fideduperange: reject dedupe crossing destination EOF | |
| Dedupe silently shortened a request crossing the destination EOF | |
| under REMAP_FILE_CAN_SHORTEN, and the ioctl layer then reported the | |
| full requested length as deduped. xfs and btrfs reject this case | |
| with EINVAL via generic_remap_file_range_prep(); match them | |
| (caught by fstests generic/158). | |
| Signed-off-by: Patrick Domack <patrickdk@patrickdk.com> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment