Kernel patch to fix FAT16 support to read a Korg D3200 drive

A problem exists with the Linux 2.6 kernel as supplied with RHEL4/CentOS4 when it comes to reading the "PC Drive" of a Korg D3200 digital multitracker. This partition is shared via USB and presents itself as a standard USB mass storage device. However, something is up with the formatting of the FAT16 filesystem on that partition.

Problem description

Here are the log entries that occur when the D3200 is connected and set to "USB Slave Mode":

kernel: usb 1-2: new full speed USB device using address 2
kernel: SCSI subsystem initialized
kernel: Initializing USB Mass Storage driver...
kernel: scsi0 : SCSI emulation for USB Mass Storage devices
kernel:   Vendor: ST340014  Model: A               Rev:  0 0
kernel:   Type:   Direct-Access                    ANSI SCSI revision: 02
kernel: USB Mass Storage device found at 2
kernel: usbcore: registered new driver usb-storage
kernel: USB Mass Storage support registered.
kernel: SCSI device sda: 78165360 512-byte hdwr sectors (40021 MB)
kernel: sda: assuming drive cache: write through
kernel:  sda: sda1 sda2 sda4
kernel: Attached scsi disk sda at scsi0, channel 0, id 0, lun 0

That's all well and good. But the problem is that when I try to mount the drive, it won't mount, and I get the following in syslog:

kernel: FAT: invalid first entry of FAT (0xffffff8 != 0xf00f800)
kernel: VFS: Can't find a valid FAT filesystem on dev sda4.

I'm no expert at this stuff, but it seems clear that what is at the start of the filesystem is not quite what the kernel is expecting. Continuing on with the presumption that the failing check is superfluous and the filesystem really is otherwise valid (because otherwise, I'm screwed), the quest now is to disable or add an exception to this check in the FAT driver source and recompile it. The following describes how to most easily accomplish this on a RHEL/CentOS/Fedora system, where the kernel source is distributed as an SRPM for the kernel package [1].

Download and prep the kernel source

The RHEL/CentOS Release Notes [2] say that one doesn't need an exploded kernel source tree in order to build a module, but you do kinda need the source tree if that's where the code for the module you're building is distributed, as the FAT driver is.

First, I downloaded the appropriate kernel.srpm and installed it into my RPM build area. If you have never built anything from SRPM before, you need to brush up a bit first [3]. At the very least, set up a ~/.rpmmacros file so that you can build RPMs as a non-root user, set up the necessary directory tree in a writeable location, then come back to this step. As an example, I'll use 2.6.9-34.EL, the current errata release for CentOS 4 Update 3.

localhost% wget http://mirror.centos.org/centos-4/4.3/os/SRPMS/kernel-2.6.9-34.EL.src.rpm
localhost% rpm -K kernel-2.6.9-34.EL.src.rpm
kernel-2.6.9-34.EL.src.rpm: (sha1) dsa sha1 md5 gpg OK
localhost% rpm -ivh kernel-2.6.9-34.EL.src.rpm
kernel ########################################### [100%]

Next, prep the source, to apply all patches and configuration options provided in the SRPM. You could simply extract the one needed file out of the main tarball, but it would be missing any other patches that your distribution may add. Running the rpmbuild prep process results in an exploded and patched source tree identical to that which the OS vendor uses to build the kernel binary they distribute.

localhost% rpmbuild --target=i686 -bp ~/rpmbuild/kernel-2.6.9/kernel-2.6.spec
Building target platforms: i686
Building for target i686
Executing(%prep): [...]
[...]
+ exit 0

Code to change

The source file that needs tweaking is linux-2.6.9/fs/fat/inode.c. The line that prints the above error message is located about 1000 lines down.

        if (FAT_FIRST_ENT(sb, media) == first) {
                /* all is as it should be */
        } else if (media == 0xf8 && FAT_FIRST_ENT(sb, 0xfe) == first) {
                /* bad, reported on pc9800 */
        } else if (media == 0xf0 && FAT_FIRST_ENT(sb, 0xf8) == first) {
                /* bad, reported with a MO disk on win95/me */
        } else if (first == 0) {
                /* bad, reported with a SmartMedia card */
        } else {
                if (!silent)
                        printk(KERN_ERR "FAT: invalid first entry of FAT "
                               "(0x%x != 0x%x)\n",
                               FAT_FIRST_ENT(sb, media), first);
                goto out_invalid;
        }

It would be easy enough to add another "else if" to check for the values returned from the Korg's filesystem, but I wondered if this had ever been addressed in later versions of the kernel. I grabbed the source to 2.6.16.9 and found that the above had been replaced with:

    /*      
     * The low byte of FAT's first entry must have same value with
     * media-field.  But in real world, too many devices is
     * writing wrong value.  So, removed that validity check.
     *  
     * if (FAT_FIRST_ENT(sb, media) != first)
     */            

In other words, the kernel used to try to be picky about this, but too much hardware doesn't live up to the spec, and it's easiest to just not bother checking than to add an exception for every non-compliant device. Simply remove the entire if/else block and be done with it.

Rebuild the module

If I was going to rebuild the entire kernel, I would need to diff out a patch file containing this change, and include that patch in the spec file, along with all the other things that must be done when rolling one's own kernel package. However, I'm lazy, and just want to rebuild this one little driver module, not the whole kernel. In order to do that, I need to edit the Makefile in the linux-2.6.9/fs/fat directory as follows:

#
# Makefile for the Linux fat filesystem support.
#

obj-m   := fat.o
fat-objs := cache.o dir.o file.o inode.o misc.o fatfs_syms.o
KDIR := /lib/modules/2.6.9-34.EL/build
PWD := $(shell pwd)
default:
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

Make sure that's a tab and not spaces on the last line, and that you have the correct kernel version in the KDIR line. If you're already running the kernel you're building the module for, you can use $(shell uname -r) in place of the hardcoded version number.

Now do a "make" to build the new FAT driver module.

localhost% setenv ARCH i386
localhost% make
make -C /lib/modules/2.6.9-34.EL/build \
    SUBDIRS=/home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat modules
make[1]: Entering directory `/usr/src/kernels/2.6.9-34.EL-i686'
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/cache.o
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/dir.o
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/file.o
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/inode.o
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/misc.o
  CC [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/fatfs_syms.o
  LD [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/fat.o
  Building modules, stage 2.
  MODPOST
  CC      /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/fat.mod.o
  LD [M]  /home/brad/rpmbuild/BUILD/kernel-2.6.9/linux-2.6.9/fs/fat/fat.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.9-34.EL-i686'

Installation

I simply renamed the original module, and copied the new one into its place. The old driver needs to be unloaded, and the new one loaded.

localhost# modprobe -r vfat
localhost# modprobe -r fat
localhost# mv /lib/modules/2.6.9-34.EL/kernel/fs/fat/fat.ko \
            /lib/modules/2.6.9-34.EL/kernel/fs/fat/_fat_orig.ko
localhost# cp fat.ko /lib/modules/2.6.9-34.EL/kernel/fs/fat/fat.ko
localhost# chown root:root !!$
localhost# chmod 744 !!$
localhost# modprobe fat

Results

After making this change, I am able to mount the D3200's shared drive and read and write to it just fine.

Footnotes

  1. Kernel 2.6 On Fedora-based Systems
  2. Red Hat Enterprise Linux AS 4 Release Notes
  3. How to patch and rebuild an RPM package

More notes