Remove 2.18GB ISO limit on usb_f_mass_storage module

230 views
Skip to first unread message

David Bateman

unread,
Apr 12, 2023, 10:21:36 AM4/12/23
to USB armory
Below is a document (in Markdown) discussing the means I used to remove this limitation... I tried to respect the SCSI MMC-3 standard and the means I used should
avoid the issues discussed in this kernel thread.

Frankly, I don't have much hope of getting this patch accepted upstream..

# Using the Linux USB mass storage gadget for ISO file greater than 2.2GB

The linux mass storage gadget is capable of simulating CDROMs using an ISO backing file
either with [the g_mass_storage](https://linux-sunxi.org/USB_Gadget/Mass_storage)
legacy driver, or the [configfs f_mass_storage](https://wiki.tizen.org/USB/Linux_USB_Layers/Configfs_Composite_Gadget/Usage_eq._to_g_mass_storage.ko)
driver. However, the code of the linux driver includes the code block

```C
if (num_sectors >= 256*60*75) {
num_sectors = 256*60*75 - 1;
LINFO(curlun, "file too big: %s\n", filename);
LINFO(curlun, "using only first %d blocks\n",
(int) num_sectors);
}
```

that limits the maximum size of the ISO supported.. The reason for this is that
the CD Audio format specifies locations of the media in the format `Minutes-Seconds-Frames`.
or`MSF`. The `Minutes` field is specifies on 8-bits and is therefore limited to a
maximum value of 255, and there are 75 frames of 2048 bytes per seconds. The maximum
ISO size is therefore `255*60*75*2048` or 2.189 GB.

DVDROM devices can clearly support larger files than this and so we need to be able to
remove this restriction to support larger files..

## The store_cdrom_address function

The function in `storage_common.c` where the `Minutes` field is actually written
somewhere is `store_cdrom_address`. This function is called from the functions
`do_read_toc` and `do_read_header` in `f_mass_storage.c`.

[The SCSI MMC-3 standard](http://www.13thmonkey.org/documentation/SCSI/mmc3r10g.pdf)
defines the commands that the USB mass storage gadget needs to implement to simulate
a CDROM or DVDROM. The READ_TOC command seems to only be valid for MSF formats.
Examining the section of the standard for the READ_TOC command, the standard says


>"If the Track/Session Number field is not valid for the currently installed medium,
the command shall be terminated with CHECK CONDITION status and SK/ASC/ASCQ shall
be set to ILLEGAL REQUEST/INVALID FIELD IN CDB."

Therefore, the READ_TOC command should be rejected for files larger than 2,18GB. This
won't stop the media from working it will only prevent the audio IOCTL from functioning
on the media.

For the READ_HEADER command the situation seems to be slightly different. This
command is no longer part of the MMC-3 standard, but was part of the older
[SCSI-3 standard](http://www.13thmonkey.org/documentation/SCSI/x3_304_1997.pdf). If
the newer commands `GET_CONFIGURATION` and `READ_DISC_INFORMATION` are implemented
this this command seems never to be called. To avoid issues the solution seems
to be if MSF format is requested and the number of sectors exceeds the maximum CD
size, to return an `INVALID FIELD in CDB` error.

We can take the implementation of the `GET_CONFIGURATION` and `READ_DISC_INFORMATION`
from a [previous patch to the mass storage gadget](https://linuxehacking.ovh/2013/07/12/how-to-emulatore-dvd-rom-hardware-usb/).

For the 5.15 kernel, this gives a final patch

```diff
--- drivers/usb/gadet/function/f_mass_storage.c.old 2022-11-10 17:15:43.000000000 +0000
+++ drivers/usb/gadet/function/f_mass_storage.c 2023-04-11 17:19:44.830000399 +0000
@@ -200,7 +200,7 @@
 /*------------------------------------------------------------------------*/
 
 #define FSG_DRIVER_DESC "Mass Storage Function"
-#define FSG_DRIVER_VERSION "2009/09/11"
+#define FSG_DRIVER_VERSION "2023/04/05"
 
 static const char fsg_string_interface[] = "Mass Storage";
 
@@ -1139,7 +1139,8 @@
  curlun->sense_data = SS_INVALID_FIELD_IN_CDB;
  return -EINVAL;
  }
- if (lba >= curlun->num_sectors) {
+ if (lba >= curlun->num_sectors ||
+     (msf && curlun->num_sectors > CD_MAX_SECTORS)) {
  curlun->sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
  return -EINVAL;
  }
@@ -1150,6 +1151,61 @@
  return 8;
 }
 
+
+static int do_read_disc_information(struct fsg_common* common, struct fsg_buffhd * bh)
+{
+ struct fsg_lun *curlun = common->curlun;
+ if (common->cmnd[1] & ~0x02) { /* Mask away MSF */
+ curlun->sense_data = SS_INVALID_FIELD_IN_CDB;
+ return -EINVAL;
+ }
+ u8* outbuf = (u8*)bh->buf;
+ memset(outbuf,0,34);
+ outbuf[1] = 32;
+ outbuf[2] = 0xe; /* last session complete, disc finalized */
+ outbuf[3] = 1;   /* first track on disc */
+ outbuf[4] = 1;   /* # of sessions */
+ outbuf[5] = 1;   /* first track of last session */
+ outbuf[6] = 1;   /* last track of last session */
+ outbuf[7] = 0x20; /* unrestricted use */
+ outbuf[8] = 0x00; /* CD-ROM or DVD-ROM */
+ return 34;
+}
+
+static int do_get_configuration(struct fsg_common *common, struct fsg_buffhd *bh)
+{
+ struct fsg_lun *curlun = common->curlun;
+ if (common->cmnd[1] & ~0x02) { /* Mask away MSF */
+ curlun->sense_data = SS_INVALID_FIELD_IN_CDB;
+ return -EINVAL;
+ }
+ u8* buf = (u8*)bh->buf;
+ int cur;
+ if ( curlun->num_sectors > CD_MAX_SECTORS )
+ cur = MMC_PROFILE_DVD_ROM;
+ else
+ cur = MMC_PROFILE_CD_ROM;
+ memset(buf,0,40);
+ put_unaligned_be32(36,&buf[0]);
+ put_unaligned_be16(cur,&buf[6]);
+ buf[10] = 0x03;
+ buf[11] = 8;
+ put_unaligned_be16(MMC_PROFILE_DVD_ROM,&buf[12]);
+ buf[14] = ( cur == MMC_PROFILE_DVD_ROM );
+ put_unaligned_be16(MMC_PROFILE_CD_ROM,&buf[16]);
+ buf[18] = ( cur == MMC_PROFILE_CD_ROM );
+ put_unaligned_be16(1,&buf[20]);
+ buf[22] = 0x08 | 0x03;
+ buf[23] = 8;
+ put_unaligned_be32(1,&buf[24]);
+ buf[28] = 1;
+ put_unaligned_be16(3,&buf[32]);
+ buf[34] = 0x08 | 0x3;
+ buf[35] = 4;
+ buf[36] = 0x39;
+ return 40;
+}
+
 static int do_read_toc(struct fsg_common *common, struct fsg_buffhd *bh)
 {
  struct fsg_lun *curlun = common->curlun;
@@ -1158,7 +1214,8 @@
  u8 *buf = (u8 *)bh->buf;
 
  if ((common->cmnd[1] & ~0x02) != 0 || /* Mask away MSF */
- start_track > 1) {
+ start_track > 1 ||
+ curlun->num_sectors > CD_MAX_SECTORS) {
  curlun->sense_data = SS_INVALID_FIELD_IN_CDB;
  return -EINVAL;
  }
@@ -1997,6 +2054,20 @@
  reply = do_write(common);
  break;
 
+ case 0x51://READ_DISC_INFORMATION
+ common->data_size_from_cmnd = 0;
+ if (!common->curlun || !common->curlun->cdrom)
+ goto unknown_cmnd;
+ reply = do_read_disc_information(common,bh);
+ break;
+
+ case 0x46://GET_CONFIGURATION
+ common->data_size_from_cmnd = 0;
+ if (!common->curlun || !common->curlun->cdrom)
+ goto unknown_cmnd;
+ reply = do_get_configuration(common,bh);
+ break;
+
  /*
   * Some mandatory commands that we recognize but don't implement.
   * They don't mean much in this setting.  It's left as an exercise
--- drivers/usb/gadet/function/storage_common.h.old 2023-04-11 16:41:27.820000125 +0000
+++ drivers/usb/gadet/function/storage_common.h 2023-04-11 15:24:16.320000480 +0000
@@ -85,6 +85,42 @@
 #define SS_WRITE_ERROR 0x030c02
 #define SS_WRITE_PROTECTED 0x072700
 
+#define CD_MINS                       80 /* max. minutes per CD */
+#define CD_SECS                       60 /* seconds per minute */
+#define CD_FRAMES                     75 /* frames per second */
+#define CD_FRAMESIZE                2048 /* bytes per frame, "cooked" mode */
+#define CD_MAX_BYTES       (CD_MINS * CD_SECS * CD_FRAMES * CD_FRAMESIZE)
+#define CD_MAX_SECTORS     (CD_MAX_BYTES / 512)
+
+#define MMC_PROFILE_NONE                0x0000
+#define MMC_PROFILE_CD_ROM              0x0008
+#define MMC_PROFILE_CD_R                0x0009
+#define MMC_PROFILE_CD_RW               0x000A
+#define MMC_PROFILE_DVD_ROM             0x0010
+#define MMC_PROFILE_DVD_R_SR            0x0011
+#define MMC_PROFILE_DVD_RAM             0x0012
+#define MMC_PROFILE_DVD_RW_RO           0x0013
+#define MMC_PROFILE_DVD_RW_SR           0x0014
+#define MMC_PROFILE_DVD_R_DL_SR         0x0015
+#define MMC_PROFILE_DVD_R_DL_JR         0x0016
+#define MMC_PROFILE_DVD_RW_DL           0x0017
+#define MMC_PROFILE_DVD_DDR             0x0018
+#define MMC_PROFILE_DVD_PLUS_RW         0x001A
+#define MMC_PROFILE_DVD_PLUS_R          0x001B
+#define MMC_PROFILE_DVD_PLUS_RW_DL      0x002A
+#define MMC_PROFILE_DVD_PLUS_R_DL       0x002B
+#define MMC_PROFILE_BD_ROM              0x0040
+#define MMC_PROFILE_BD_R_SRM            0x0041
+#define MMC_PROFILE_BD_R_RRM            0x0042
+#define MMC_PROFILE_BD_RE               0x0043
+#define MMC_PROFILE_HDDVD_ROM           0x0050
+#define MMC_PROFILE_HDDVD_R             0x0051
+#define MMC_PROFILE_HDDVD_RAM           0x0052
+#define MMC_PROFILE_HDDVD_RW            0x0053
+#define MMC_PROFILE_HDDVD_R_DL          0x0058
+#define MMC_PROFILE_HDDVD_RW_DL         0x005A
+#define MMC_PROFILE_INVALID             0xFFFF
+
 #define SK(x) ((u8) ((x) >> 16)) /* Sense Key byte, etc. */
 #define ASC(x) ((u8) ((x) >> 8))
 #define ASCQ(x) ((u8) (x))
--- drivers/usb/gadet/function/storage_common.c.old 2023-04-11 17:53:14.720000638 +0000
+++ drivers/usb/gadet/function/storage_common.c 2023-04-11 16:42:34.770000133 +0000
@@ -243,12 +243,6 @@
  min_sectors = 1;
  if (curlun->cdrom) {
  min_sectors = 300; /* Smallest track is 300 frames */
- if (num_sectors >= 256*60*75) {
- num_sectors = 256*60*75 - 1;
- LINFO(curlun, "file too big: %s\n", filename);
- LINFO(curlun, "using only first %d blocks\n",
- (int) num_sectors);
- }
  }
  if (num_sectors < min_sectors) {
  LINFO(curlun, "file too small: %s\n", filename);
@@ -303,6 +297,8 @@
  addr /= 75;
  dest[2] = addr % 60; /* Seconds */
  addr /= 60;
+ if (addr > 255)
+ printk("store_cdrom_address: Addr overflow");
  dest[1] = addr; /* Minutes */
  dest[0] = 0; /* Reserved */
  } else {
```

In the following discussion we assume that this patch is saved in the file
`dvdrom.patch`. As this point we need to rebuild the `usb_f_mass_storage.ko`
kernel module. The modern way to do this would be with [DKMS](https://www.collabora.com/news-and-blog/blog/2021/05/05/quick-hack-patching-kernel-module-using-dkms/),
though the USB armory kernel headers don't include all of the files
necessary to allow this.

We are therefore required to recreate a minimal kernel source tree like for the
USB Armory, with exactly the same configuration and rebuild the module in this
source tree. If the script

```shell
! /bin/bash

src="."
patch="$(dirname $0)/dvdrom.patch"

kernelver=$(uname -r)
major=$(echo $kernelver | cut -d. -f1)
minor=$(echo $kernelver | cut -d. -f2)
subver=$(echo $kernelver | cut -d. -f3 | cut -d- -f1)
version="$major.$minor"    # recombine as needed

# Test if sufficent space available for the kernel build
[ "$(df --block-size=1M /mnt2 | tail -1 | xargs | cut -d' ' -f4)" -le 1500 ] \
  && { echo "Insufficient space for the build"; exit 1; }

if [ ! -f "$src/linux-$version.$subver.tar.xz" ]; then
  echo "Downloading kernel source $version.$subver for $kernelver"
  wget -P "$src" https://mirrors.edge.kernel.org/pub/linux/kernel/v$major.x/linux-$version.$subver.tar.xz
fi

if [ ! -d "$src/linux-$version.$subver" ]; then
  echo "Extracting original kernel source for $kernelver"
  tar -xJf "$src/linux-$version.$subver.tar.xz"

  echo "Patching usb_f_mass_storage driver"
  (cd "$src/linux-$version.$subver/drivers/usb/gadget/function"; patch) < $patch
fi  
 
if [ ! -f "$src/linux-$version.$subver/.config" ]; then
  echo "Downloading USB Armory kernel config usbarmory_linux-$version.defconfig"
  wget https://raw.githubusercontent.com/usbarmory/usbarmory/master/software/kernel_conf/usbarmory_linux-$version.defconfig -O "$src/linux-$version.$subver/.config"
 
  echo "Preparing kernel for the build"
  make -C "$src/linux-$version.$subver" olddefconfig
  make -C "$src/linux-$version.$subver" prepare
  make -C "$src/linux-$version.$subver" modules_prepare
fi

# Send build to log because we don't care about unresolved symbols
echo "Building usb_f_mass_storage.ko"
rm -f "$src/linux-$version.$subver/drivers/usb/gadget/function/usb_f_mass_storage.ko"
make -C "$src/linux-$version.$subver" M=drivers/usb/gadget/function usb_f_mass_storage.ko > "$src/build.log" 2>&1

# Test if build sucessfull
[ -f "$src/linux-$version.$subver/drivers/usb/gadget/function/usb_f_mass_storage.ko" ] \
  || { echo "### Build failed, see $src/build.log"; exit 1; }

# Package module for later use
cp "$src/linux-$version.$subver/drivers/usb/gadget/function/usb_f_mass_storage.ko" "$src/usb_f_mass_storage-$version.$subver.ko"

# Remove everything that was used in the build of the module
rm -fr "$src/linux-$version.$subver"
```

is saved as `script.sh` in the same directory as `dvdrom.patch`, then the
`usb_f_mass_storage.ko`kernel module can be rebuild as

```
chmod +x ./script.sh
./script.sh
```

To replace the existing module, you can then do

```
sudo cp usb_f_mass_storage-$(uname -r | cut -d- -f1).ko /lib/modules/$(uname -r)/kernel/drivers/usb/gadget/function/usb_f_mass_storage.ko
``̀

A reboot of the USB Armory is then necessary for this change to be taken into account.
Note that as the file `Module.symvers` of the kernel is only built for a full kernel build
and the USB Armory headers doesn't include this file, the modprobe on the usb_f_mass_storage
module will give a warning in the logs

```
[   XX.XXXXXX] usb_f_mass_storage: no symbol version for module_layout
```

This error can be ignored.

The above code is run during a standard build for the USB armory and can be called
specifically with the command

```
make module
```

Note that 1,4GB is needed in the home directory of the USB armory, otherwise something
like

```
make module /mnt
```

allows the module to be build elsewhere.

David Bateman

unread,
Apr 13, 2023, 4:42:05 AM4/13/23
to USB armory
Opps the patch had a couple of errors

```diff
-- drivers/usb/gadget/function/f_mass_storage.c.old 2022-11-10 17:15:43.000000000 +0000
+++ drivers/usb/gadget/function/f_mass_storage.c 2023-04-11 17:19:44.830000399 +0000
  curlun->sense_data = SS_INVALID_FIELD_IN_CDB;
  return -EINVAL;
  }
@@ -1997,6 +2054,20 @@
  reply = do_write(common);
  break;
 
+ case 0x51://READ_DISC_INFORMATION
+ common->data_size_from_cmnd = 0;
+ if (!common->curlun || !common->curlun->cdrom)
+ goto unknown_cmnd;
+ reply = do_read_disc_information(common,bh);
+ break;
+
+ case 0x46://GET_CONFIGURATION
+ common->data_size_from_cmnd = 0;
+ if (!common->curlun || !common->curlun->cdrom)
+ goto unknown_cmnd;
+ reply = do_get_configuration(common,bh);
+ break;
+
  /*
   * Some mandatory commands that we recognize but don't implement.
   * They don't mean much in this setting.  It's left as an exercise
--- drivers/usb/gadget/function/storage_common.h.old 2023-04-11 16:41:27.820000125 +0000
+++ drivers/usb/gadget/function/storage_common.h 2023-04-11 15:24:16.320000480 +0000
--- drivers/usb/gadget/function/storage_common.c.old 2023-04-11 17:53:14.720000638 +0000
+++ drivers/usb/gadget/function/storage_common.c 2023-04-11 16:42:34.770000133 +0000

@@ -243,12 +243,6 @@
  min_sectors = 1;
  if (curlun->cdrom) {
  min_sectors = 300; /* Smallest track is 300 frames */
- if (num_sectors >= 256*60*75) {
- num_sectors = 256*60*75 - 1;
- LINFO(curlun, "file too big: %s\n", filename);
- LINFO(curlun, "using only first %d blocks\n",
- (int) num_sectors);
- }
  }
  if (num_sectors < min_sectors) {
  LINFO(curlun, "file too small: %s\n", filename);
@@ -303,6 +297,8 @@
  addr /= 75;
  dest[2] = addr % 60; /* Seconds */
  addr /= 60;
+ if (addr > 255)
+ printk("store_cdrom_address: Addr overflow");
  dest[1] = addr; /* Minutes */
  dest[0] = 0; /* Reserved */
  } else {
```

Andrea Barisani

unread,
Apr 18, 2023, 4:13:14 AM4/18/23
to USB armory
Please be advised that our preferred way of developing UMS firmware on the USB armory is using our TamaGo framework.

In case this is compatible with your needs please check the following projects:



Cheers

David Bateman

unread,
Apr 19, 2023, 4:32:54 AM4/19/23
to USB armory
I need to present both a writable disk and ISO read-only image to the host. The ISO is stored in a file, so is a file backed storage. It didn't seem to me that the armory UMS device was actually capable of doing this, and so the easiest way to implement what I needed was based on the debian distribution, via their mass storage gadget (thus this patch to allow ISO file backed storage greater than 2,18GB).

I also need to be able to present different writable disks to he host on each connection and treat the files placed on the previous disk at the next boot.. Frankly a couple hundred lines of bash (or rather dash as we are talking about debian boot scripts) was easier to write than in TAMA..

Sure TAMA has lots of advantages particularly in the boot time and security, but the time to develop the code would be much longer. I'd be happy to talk more about my use case, but one on one.
Reply all
Reply to author
Forward
0 new messages