Hi,

sometimes I see writing to msdosfs fail with EINVAL. This seems to occur 
when writing a file reaches the end of the partition.  But it only happens 
if all bits in the pm_inusemap array are actually used, i.e. if 
pm_maxcluster % 32 == 31 . This means that for the number N of data 
clusters in the partition the condition N % 32 == 30 is true, as the first 
two cluster numbers have special meaning and do not represent data 
clusters on disk. For partition sizes that do not fulfill this condition, 
there is always a bit set at the end of the pm_inusemap that prevents 
chainlength() from overrunning the size of the partition.

Reproducer:

# dd if=/dev/zero bs=1k seek=20019 of=msdos.img count=1
1+0 records in
1+0 records out
1024 bytes transferred in 0.000 secs (52258229 bytes/sec)
# VND=$(vnconfig msdos.img ) && echo $VND
vnd0
# newfs_msdos ${VND}c
/dev/rvnd0c: 39928 sectors in 9982 FAT16 clusters (2048 bytes/cluster)
bps=512 spc=4 res=1 nft=2 rde=512 sec=40040 mid=0xf0 spf=39 spt=63 hds=1 hid=0
# mount_msdos /dev/${VND}c /mnt
# dd if=/dev/zero bs=1k of=/mnt/fff
dd: /mnt/fff: Invalid argument
12829+0 records in
12828+0 records out
13135872 bytes transferred in 1.884 secs (6969515 bytes/sec)
# df -h /mnt
Filesystem     Size    Used   Avail Capacity  Mounted on
/dev/vnd0c    19.5M   12.5M    7.0M    65%    /mnt
# umount /mnt/
# vnconfig -u ${VND}

There is this fix in FreeBSD:

  commit 097a1d5fbb7990980f8f806c6878537c964adf32
  Author: kib <k...@freebsd.org>
  Date:   Fri Oct 28 11:34:32 2016 +0000

  Ensure that cluster allocations never allocate clusters outside the
  volume limits.  In particular:
  - Assert that usemap_alloc() and usemap_free() cluster number argument
    is valid.
  - In chainlength(), return 0 if cluster start is after the max cluster.
  - In chainlength(), cut the calculated cluster chain length at the max
    cluster.
  - For true paranoia, after the pm_inusemap is calculated in
    fillinusemap(), reset all bits in the array for clusters after the
    max cluster, as in-use.

However, the for loop added to the end of fillinusemap() by that commit is 
a no-op because a) the bits have already been set by the for loop at the 
start of the function and b) the boundary conditions in the for loop mix 
cluster numbers and indexes to pm_inusemap causing the loop to never be 
executed.

Therefore, I omit that for loop alltogether but adapted the rest of the
FreeBSD fix to OpenBSD. This seems to fix the issue.

Are there any heavy users of msdosfs who can give this a test? Or any
comments or oks?

Cheers,
Stefan


diff --git a/sys/msdosfs/msdosfs_fat.c b/sys/msdosfs/msdosfs_fat.c
index c15b6257d43..0ab1463ea67 100644
--- a/sys/msdosfs/msdosfs_fat.c
+++ b/sys/msdosfs/msdosfs_fat.c
@@ -409,6 +409,7 @@ updatefats(struct msdosfsmount *pmp, struct buf *bp, 
uint32_t fatbn)
 static __inline void
 usemap_alloc(struct msdosfsmount *pmp, uint32_t cn)
 {
+       KASSERT(cn <= pmp->pm_maxcluster);
 
        pmp->pm_inusemap[cn / N_INUSEBITS] |= 1 << (cn % N_INUSEBITS);
        pmp->pm_freeclustercount--;
@@ -417,6 +418,7 @@ usemap_alloc(struct msdosfsmount *pmp, uint32_t cn)
 static __inline void
 usemap_free(struct msdosfsmount *pmp, uint32_t cn)
 {
+       KASSERT(cn <= pmp->pm_maxcluster);
 
        pmp->pm_freeclustercount++;
        pmp->pm_inusemap[cn / N_INUSEBITS] &= ~(1 << (cn % N_INUSEBITS));
@@ -644,6 +646,8 @@ chainlength(struct msdosfsmount *pmp, uint32_t start, 
uint32_t count)
        u_int map;
        uint32_t len;
 
+       if (start > pmp->pm_maxcluster)
+           return (0);
        max_idx = pmp->pm_maxcluster / N_INUSEBITS;
        idx = start / N_INUSEBITS;
        start %= N_INUSEBITS;
@@ -651,11 +655,15 @@ chainlength(struct msdosfsmount *pmp, uint32_t start, 
uint32_t count)
        map &= ~((1 << start) - 1);
        if (map) {
                len = ffs(map) - 1 - start;
-               return (len > count ? count : len);
+               len = MIN(len, count);
+               len = MIN(len, pmp->pm_maxcluster - start + 1);
+               return (len);
        }
        len = N_INUSEBITS - start;
-       if (len >= count)
-               return (count);
+       if (len >= count) {
+               len = MIN(count, pmp->pm_maxcluster - start + 1);
+               return (len);
+       }
        while (++idx <= max_idx) {
                if (len >= count)
                        break;
@@ -665,7 +673,9 @@ chainlength(struct msdosfsmount *pmp, uint32_t start, 
uint32_t count)
                }
                len += N_INUSEBITS;
        }
-       return (len > count ? count : len);
+       len = MIN(len, count);
+       len = MIN(len, pmp->pm_maxcluster - start + 1);
+       return (len);
 }
 
 /*

Reply via email to