Re: [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path
From: Hyunchul Lee
Date: Wed May 06 2026 - 01:49:29 EST
2026년 5월 5일 (화) 오전 12:48, DaeMyung Kang <charsyam@xxxxxxxxx>님이 작성:
>
> 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 <charsyam@xxxxxxxxx>
> ---
> 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];
When new_1st_cnt is 0, this merge should never succeed on a
normal runlist. Therefore, instead of clamping, it is safer to skip
the merge entirely when new_1st_cnt == 0.
> s_rl->length += s_rl[1].length;
> merge_cnt = 1;
> }
> --
> 2.43.0
--
Thanks,
Hyunchul