How to Do the Impossible?
The answer to that question is simple: don’t stop, no matter how hard the task is. Even if everyone around you says it’s impossible.
My task was to replace a device driver in Linux that was baked permanently into the kernel. If I’d had the kernel source, the whole thing would have been fairly straightforward, but in our case (the Nook e-reader’s kernel) the available source was broken and a year out of date. On x86 I could have disassembled the driver’s init code in the kernel and NOPed it out, but this was already ARM architecture, with the kernel packed as a zImage.
The first experiments were mundane — clever insmod/rmmod invocations, fiddling with the driver name, and so on. Then I asked some Linux-savvy friends for advice, and they confirmed that standard tools couldn’t pull this off. Exceptions are rare — if a driver accepts parameters with device addresses, enable/disable commands, and the like, there’s still something you can do, but in our case there were no parameters at all.
Then I started experimenting at the source level. This code caught my attention:
static int __devinit synaptics_ts_init(void) { synaptics_wq = create_singlethread_workqueue("synaptics_wq"); if (!synaptics_wq) return -ENOMEM; return i2c_add_driver(&synaptics_ts_driver); } static void __exit synaptics_ts_exit(void) { i2c_del_driver(&synaptics_ts_driver); if (synaptics_wq) destroy_workqueue(synaptics_wq); }
If there’s a function i2c_del_driver for unloading a driver, why can’t I call it from my own module to unload someone else’s driver? Unfortunately, the Linux kernel has situations where you simply can’t reach an object without a pointer to it. For instance, destroy_workqueue can only be called with a pointer to the queue, and only whoever created that queue has it — there’s no getting it otherwise, since no find_workqueue exists in nature.
In our case there was a chance, though — while i2c doesn’t let you look up a driver by name, the i2c-core.c source made it clear that it uses the low-level device_register/device_unregister calls from linux/device.h. There I also found the driver_find function, which looks up a driver by name and bus identifier. The name of the built-in driver is known, and the bus in question is i2c, with its identifier in the i2c_bus_type variable:
struct device_driver * other;
other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);
if (other)
{
printk("Previous driver found: %s\n", other->name);
return -ENOMEM;
}
This code correctly finds the previous driver and prints its name. Of course, that alone wasn’t enough, so I added driver_unregister(other) — and got a kernel panic 🙂
It’s reasonable to assume that since we register a driver through i2c_add_driver, we should also remove it through i2c_del_driver rather than directly. Looking at the i2c_driver structure, you can see it contains a device_driver, which is exactly what driver_find returns. There are several ways to convert a pointer to a nested structure into a pointer to its parent, but the simplest is the standard to_i2c_driver conversion. That gave me this code:
struct i2c_driver * otherDriver;
struct device_driver * other;
other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);
if (other)
{
otherDriver = to_i2c_driver(other);
printk(KERN_ERR "Previous driver found: %s, addr 0x%x, owner %x\n", other->name, (int)otherDriver, (int)other->owner);
i2c_del_driver(otherDriver);
}
Characteristically, this also causes a kernel panic. At this point I’d nearly concluded that deregistration was impossible, but I happened to notice in drivers/base/core.c that a call to put_driver usually follows driver_find. It turned out that drivers, as kernel objects, use intrusive reference counting, and after driver_find the counter goes up by one, which prevents i2c_del_driver from unloading the driver. Adding that call fixed everything, and the built-in driver started unloading correctly.
Of course, not everything worked right away, since there was still a workqueue with the same name, and the built-in driver turned out to be a bit “sloppy” too — it didn’t remove its sysfs files on unload — but all of that turned out to be solvable.
The result of all this “dancing with a tambourine” was my own touchscreen driver for the Nook with Multitouch support. You can read more about the driver itself at these links:
http://nookdevs.com/Multitouch
http://www.the-ebook.org/forum/viewtopic.php?t=16728
Originally published 2010-11-27.
