Messages in this thread |  | | From | Hyunchul Lee <> | | Date | Wed, 6 May 2026 14:48:58 +0900 | | Subject | Re: [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path |
| |
2026년 5월 5일 (화) 오전 12:48, DaeMyung Kang <charsyam@gmail.com>님이 작성: > > 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@gmail.com> > --- > 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
|  |