* [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path
@ 2026-05-04 15:48 DaeMyung Kang
2026-05-06 5:48 ` Hyunchul Lee
2026-05-06 9:24 ` [PATCH v2] " DaeMyung Kang
0 siblings, 2 replies; 3+ messages in thread
From: DaeMyung Kang @ 2026-05-04 15:48 UTC (permalink / raw)
To: Namjae Jeon, Hyunchul Lee; +Cc: linux-fsdevel, linux-kernel
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];
s_rl->length += s_rl[1].length;
merge_cnt = 1;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path
2026-05-04 15:48 [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path DaeMyung Kang
@ 2026-05-06 5:48 ` Hyunchul Lee
2026-05-06 9:24 ` [PATCH v2] " DaeMyung Kang
1 sibling, 0 replies; 3+ messages in thread
From: Hyunchul Lee @ 2026-05-06 5:48 UTC (permalink / raw)
To: DaeMyung Kang; +Cc: Namjae Jeon, linux-fsdevel, linux-kernel
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
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v2] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path
2026-05-04 15:48 [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path DaeMyung Kang
2026-05-06 5:48 ` Hyunchul Lee
@ 2026-05-06 9:24 ` DaeMyung Kang
1 sibling, 0 replies; 3+ messages in thread
From: DaeMyung Kang @ 2026-05-06 9:24 UTC (permalink / raw)
To: Namjae Jeon, Hyunchul Lee; +Cc: linux-fsdevel, linux-kernel, DaeMyung Kang
ntfs_rl_collapse_range() merges the run on the left of the collapsed
region with the run on its right when they are contiguous. The contiguous
check chooses a clamped index 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() 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 and the following
run are holes. In that case ntfs_rle_lcn_contiguous() returns true
because both checked entries are LCN_HOLE, so the merge path is entered
with @new_1st_cnt still 0. Such consecutive holes do not occur on a
well-formed runlist (NTFS keeps runlists coalesced in memory), so this
OOB path is only reachable from a crafted volume.
A normal runlist has no element to the left of vcn 0, so the left/right
merge is not valid when @new_1st_cnt is 0. Require @new_1st_cnt to be
positive before checking or performing the merge. This skips the merge
entirely in that case instead of clamping the merge target.
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")
Suggested-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: DaeMyung Kang <charsyam@gmail.com>
---
Changes since v1:
- Skip the merge entirely when @new_1st_cnt == 0 instead of using a
clamped index, per review feedback. The clamped form silently
changed the operation's semantics from "merge the run left of the
collapsed region with the run right of it" to "coalesce two
right-side runs"; the new form closes a path that is only
reachable on malformed runlists. Behaviour on well-formed runlists
is unchanged.
fs/ntfs/runlist.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c
index da21dbeaaf66..e7de3d01257e 100644
--- a/fs/ntfs/runlist.c
+++ b/fs/ntfs/runlist.c
@@ -2056,10 +2056,11 @@ struct runlist_element *ntfs_rl_collapse_range(struct runlist_element *dst_rl, i
* consists of holes.
*/
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];
+ if (new_1st_cnt > 0 &&
+ ntfs_rle_lcn_contiguous(&new_rl[new_1st_cnt - 1],
+ &new_rl[new_1st_cnt])) {
+ /* Merge right and left. */
+ s_rl = &new_rl[new_1st_cnt - 1];
s_rl->length += s_rl[1].length;
merge_cnt = 1;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-05-06 9:24 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-04 15:48 [PATCH] ntfs: fix out-of-bounds write in ntfs_rl_collapse_range() merge path DaeMyung Kang
2026-05-06 5:48 ` Hyunchul Lee
2026-05-06 9:24 ` [PATCH v2] " DaeMyung Kang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox