Re: [PATCH] ntfs: bound the free-cluster bitmap scan to the volume

Hyunchul Lee <[email protected]> Mon, 27 Jul 2026 10:44:48 +0900
Newsgroups dev.linux.lists.ntfs,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel
Message-ID <CANFS6bYise8cD8AttfFMnLkmGRzwGxQriOWTavRZo7nCQwiRJA@mail.gmail.com>
2026=EB=85=84 7=EC=9B=94 27=EC=9D=BC (=EC=9B=94) =EC=98=A4=EC=A0=84 8:29, B=
ryam Vargas via B4 Relay
<[email protected]>=EB=8B=98=EC=9D=B4 =EC=9E=91=
=EC=84=B1:
>
> From: Bryam Vargas <[email protected]>
>
> vol->lcn_empty_bits_per_page is sized from vol->nr_clusters at mount, but
> ntfs_cluster_alloc() bounds its scan of that array by the size of $Bitmap=
.
> Those are independent on-disk quantities and the mount-time check only
> rejects a $Bitmap that is too small, so an image whose $Bitmap covers mor=
e
> clusters than the volume has lets the scan index past the array.  A run
> whose LCN lies in that gap takes the allocator straight there, since the
> caller passes the file's own last LCN as its locality hint.  KASAN report=
s
> a slab out-of-bounds read when a file on such a volume is extended.
>
> Clamp the scan to what that array covers, mirroring the max_index
> calculation the mount-time scan already uses, and reject a decoded LCN
> at or beyond nr_clusters in the mapping pairs decoder.  Conforming
> volumes are unaffected.
>
> Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator=
")
> Cc: [email protected]
> Signed-off-by: Bryam Vargas <[email protected]>

Looks good to me.

Reviewed-by: Hyunchul Lee <[email protected]>

> ---
> Either hunk alone stops the access; they close it at different levels.  T=
he
> clamp bounds the index whatever the caller passes as a hint.  The decoder
> check stops a run pointing outside the volume from being accepted at all,
> which also covers ntfs_cluster_free_from_rl(), where the only test today =
is
> lcn < 0.  Happy to drop either one if you'd rather keep it minimal.
>
> Reproducer.  A volume whose $Bitmap covers more pages than nr_clusters
> requires, with one file's data run placed in the resulting gap:
>
>   truncate -s 13G img && mkntfs -F -f img
>   mount -i -t ntfs -o rw img /mnt && dd if=3D/dev/urandom of=3D/mnt/v.bin=
 \
>       bs=3D4096 count=3D16 && umount /mnt
>
>   # $Boot total_sectors (0x28) -> 1703936*8, i.e. nr_clusters =3D 1703936=
,
>   # just above mftmirr_lcn.  lcn_empty_bits_per_page then covers 52 pages
>   # while $Bitmap still spans 104, leaving indices 52..103 unbacked.
>   # v.bin's single data run (3-byte LCN delta) -> LCN 3000000, index 91.
>
>   mount -i -t ntfs -o rw img /mnt
>   dd if=3D/dev/zero bs=3D4096 count=3D8 >> /mnt/v.bin
>
> mount -i matters: without it the ntfs-3g helper takes the volume and this
> code never runs.
>
> A/B on v7.2-rc1 with KASAN, one arm per boot:
>
>   unpatched, crafted image:
>     BUG: KASAN: slab-out-of-bounds in ntfs_cluster_alloc+0x249e/0x2db0 [n=
tfs]
>     Read of size 4 at addr ffff8881029dad6c by task kworker/u32:6/96
>     Workqueue: writeback wb_workfn (flush-7:0)
>      ntfs_cluster_alloc+0x249e/0x2db0 [ntfs]
>      ntfs_attr_map_cluster+0x444/0xf50 [ntfs]
>      __ntfs_write_iomap_begin+0x8b7/0x22c0 [ntfs]
>      ntfs_writeback_range+0xd3/0x130 [ntfs]
>     Allocated by task 2255:
>      __kvmalloc_node_noprof+0x1cc/0x5d0
>      ntfs_fill_super+0x16c0/0x6fd0 [ntfs]
>     The buggy address is located 156 bytes to the right of
>      allocated 208-byte region [ffff8881029dac00, ffff8881029dacd0)
>
>   52 pages * sizeof(unsigned int) is 208 bytes, and index 91 is offset 36=
4.
>
>   clamp only:    clean; the volume mounts and the append completes
>   decoder only:  clean; the run is refused
>   both:          clean; the append returns EIO and the decoder logs
>                  "LCN >=3D nr_clusters in mapping pairs array"
>   pristine mkntfs volume, unpatched, same operations: clean
>
> The write side takes the same LCN.  A kprobe caught
> __ntfs_bitmap_set_bits_in_run() with start_bit=3D3000000 count=3D16, whic=
h lands
> on the same array through ntfs_set_lcn_empty_bits(); KASAN reports once p=
er
> boot, so only the read above splatted.
> ---
>  fs/ntfs/lcnalloc.c | 7 ++++++-
>  fs/ntfs/runlist.c  | 7 +++++++
>  2 files changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c
> index 835a041023a2..aa2e017a4384 100644
> --- a/fs/ntfs/lcnalloc.c
> +++ b/fs/ntfs/lcnalloc.c
> @@ -298,7 +298,12 @@ struct runlist_element *ntfs_cluster_alloc(struct nt=
fs_volume *vol, const s64 st
>         clusters =3D count;
>         rlpos =3D rlsize =3D 0;
>         mapping =3D lcnbmp_vi->i_mapping;
> -       i_size =3D i_size_read(lcnbmp_vi);
> +       /*
> +        * lcn_empty_bits_per_page is sized from nr_clusters, but $Bitmap=
 can
> +        * cover more clusters than that; bound the scan by the array.
> +        */
> +       i_size =3D min_t(s64, i_size_read(lcnbmp_vi),
> +                      ((s64)vol->nr_clusters + 7) >> 3);
>         while (1) {
>                 ntfs_debug("Start of outer while loop: done_zones 0x%x, s=
earch_zone %i, pass %i, zone_start 0x%llx, zone_end 0x%llx, bmp_initial_pos=
 0x%llx, bmp_pos 0x%llx, rlpos %i, rlsize %i.",
>                                 done_zones, search_zone, pass,
> diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c
> index cbb6576cf725..74f94b4b6c5b 100644
> --- a/fs/ntfs/runlist.c
> +++ b/fs/ntfs/runlist.c
> @@ -880,6 +880,13 @@ struct runlist_element *ntfs_mapping_pairs_decompres=
s(const struct ntfs_volume *
>                                         ntfs_error(vol->sb, "lcn =3D=3D -=
1");
>                         }
>  #endif
> +                       /* Check lcn is within the volume. */
> +                       if (unlikely(lcn >=3D (s64)vol->nr_clusters)) {
> +                               ntfs_error(vol->sb,
> +                                               "LCN >=3D nr_clusters in =
mapping pairs array.");
> +                               goto err_out;
> +                       }
> +
>                         /* Check lcn is not below -1. */
>                         if (unlikely(lcn < -1)) {
>                                 ntfs_error(vol->sb, "Invalid s64 < -1 in =
mapping pairs array.");
>
> ---
> base-commit: 4235cb24ec1e8e96843f3671ba4da2a6ccca2c7b
> change-id: 20260726-b4-disp-9cd2a8a2-007c3a970c87
>
> Best regards,
> --
> Bryam Vargas <[email protected]>
>
>


--=20
Thanks,
Hyunchul