ntfs_rl_collapse_range() merges the run on the left of the collapsed region with the run on its right when both are holes. The contiguous check correctly clamps the index to 1 when @new_1st_cnt is 0: i = new_1st_cnt == 0 ? 1 : new_1st_cnt; if (ntfs_rle_lcn_contiguous(&new_rl[i - 1], &new_rl[i])) { but the merge itself uses the unclamped value: s_rl = &new_rl[new_1st_cnt - 1]; s_rl->length += s_rl[1].length; When @new_1st_cnt is 0 this computes &new_rl[-1] and writes 8 bytes before the kvcalloc'd runlist buffer. The path is reachable through fallocate(FALLOC_FL_COLLAPSE_RANGE) starting at vcn 0 against an attribute whose first run after the collapsed region is a hole. In that case ntfs_rle_lcn_contiguous() returns true because both sides are LCN_HOLE, so the merge path is entered with @new_1st_cnt still 0. Use the same clamped index for the merge as for the contiguous check so the write always lands inside the buffer. The out-of-bounds write can corrupt an adjacent slab object. On a non-KASAN kernel, it is reachable after a crafted NTFS volume has been mounted read-write with the legacy fs/ntfs driver, by a local user that has write access to the crafted file. Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator") Signed-off-by: DaeMyung Kang --- Trigger conditions / reproducer notes: - subsystem: fs/ntfs (legacy NTFS runlist collapse path) - introduced by: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator"), first released in v7.1-rc1 - affected kernels: v7.1-rc1 through current mainline; also present in any branch that includes 11ccc9107dc4. Pre-v7.1-rc1 kernels do not contain this collapse-range code path. - local: yes - remote: no direct remote trigger - authentication required: no - special privileges required: yes to mount the crafted NTFS volume read-write in the normal case. After a privileged read-write mount, no additional privilege is needed for the fallocate trigger if the caller has write permission on the crafted file. - filesystem requirement: the legacy fs/ntfs driver must be mounted read-write against a crafted NTFS volume. - repeatable: yes, the crafted runlist geometry and collapse range make the sequence deterministic. Reproducer summary used during verification: 1. Prepare an NTFS volume containing a file whose runlist makes @new_1st_cnt equal to 0 and places a hole as the first run after the collapsed region. 2. Mount the volume read-write with the legacy fs/ntfs driver. 3. Call fallocate(FALLOC_FL_COLLAPSE_RANGE) on the crafted file, starting at file offset 0. 4. Repeat with different crafted runlist sizes to place the temporary runlist allocation in different kmalloc caches. Observed result without this fix: - KASAN reports a slab out-of-bounds write in ntfs_rl_collapse_range(). - The write updates the runlist element immediately before the kvcalloc'd temporary runlist buffer. - On a non-KASAN kernel, the same trigger corrupts adjacent kmalloc objects. - The crafted runlist geometry can steer the temporary runlist allocation into different kmalloc caches. - Impact: local kernel heap corruption, reachable only after a privileged read-write mount of a crafted NTFS volume. Practical impact in the field is bounded by that mount requirement. Verification with the fix applied: - "make M=fs/ntfs modules" succeeds. - The KASAN QEMU reproducer no longer reports an out-of-bounds write. - The non-KASAN QEMU observer object is no longer corrupted. fs/ntfs/runlist.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index da21dbeaaf66..e09bc86a7276 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -2058,8 +2058,12 @@ struct runlist_element *ntfs_rl_collapse_range(struct runlist_element *dst_rl, i merge_cnt = 0; i = new_1st_cnt == 0 ? 1 : new_1st_cnt; if (ntfs_rle_lcn_contiguous(&new_rl[i - 1], &new_rl[i])) { - /* Merge right and left */ - s_rl = &new_rl[new_1st_cnt - 1]; + /* Merge right and left. + * + * Use the clamped @i; new_1st_cnt - 1 would index + * new_rl[-1] when @new_1st_cnt == 0. + */ + s_rl = &new_rl[i - 1]; s_rl->length += s_rl[1].length; merge_cnt = 1; } -- 2.43.0