Writing a laptop driver for fun and profit, or How to commit to kernel even if you're not that smart
Where it all began
Let’s start with our problem statement. We have 1 (one) laptop. A new, gamer laptop. With some RGB-backlight on its keyboard. It looks like this:
Picture taken from lenovo.com
There’s also a program installed on this laptop. That’s the thing that controls our backlight.
One problem – the program runs under Windows, and we want everything to work on our favourite Linux. Want LEDs to flash and those pretty colours to blink on and off and such. A natural question arises, can we do all that without reverse-engineering and writing our own drivers?
A natural answer arises, no. Let’s open IDA and get cracking.
Step 1 – Digging in the code
There are three triggers that make the backlight glow. In order of ascending difficulty:
1. Big buff gamer program called Lenovo Nerve Center, which has a big buff setup menu just for this backlight.
2. Hotkey combination Fn+Space – possibly managed by the aforementioned program, too.
3. BIOS. While the laptop is loading, the backlight blinks red for a brief moment. Maybe we can use that?
I had to try all 3. But there was some success only along the first one, so let’s talk about the first one here.
We open the folder with our program:
Notice here! There’s a DLL with a very interesting name – LedSettingsPlugin.dll. Could it be...? Let’s open it in IDA Pro and see for ourselves.
Look at the right half of the screen here – so, so much debug information! Let’s use it. Some strings look like function names here. I wonder why…
Oh, these are function names. Convenient! Let’s call some things by their own names and then look at the function list again. To name things in IDA you can use the hotkey N, or just right-click the thing you want to name.
Looking at functions, we see something called Y720LedSetHelper::SetLEDStatusEx. Looks like what we need! It forms some kind of a string, and then gives it to something called CHidDevHelper::HidRequestsByPath. The interesting part we should be looking at is var_38, lovingly highlighted by IDA.
Var_34 is interesting, too. It goes right after var_38 – in assembler traditions, variables are stored in reverse order under the RSP. Var_34 in IDA is just the name of the constant – -34h.
Starting from var_38, our program puts a zero, then style, colour, the number three and the block – part of the keyboard that will gain this colour. (Later on, the experiments will show that the number three is actually the brightness. Adding a control on that will make our driver even better than the official one!)
Now, let us take a deep dive into HidRequestsByPath and try to figure out what do we need to send and where.
Look, two functions. HidD_GetFeature and HidD_SetFeature.
They aren’t tracked in the file… but are very well tracked in the official Microsoft documentation — here and here.
Well, we can pat ourselves on the back now. This is rock bottom, there’s nowhere to dig deeper. Linux has those two functions – we just go back, and call them with the same arguments. ...Right?
Step 2 – launching the prototype...?
Not exactly. Let’s go back to Linux and open up lsusb. We’ll find our keyboard there, and we’ll send something to it.
Integrated Technology Express, Inc. is the most interesting one here. Let’s use the popular tool /dev/hidraw. We can find the proper one just by looking at the corresponding/sys/class/hidraw/hidraw*/device/uevent.
This one. All the digits match — that means /dev/hidraw0 is our device. But let’s try to send something… and nothing happens! Why. At this point, burnout kicks in. Maybe it’s not for mere mortals, this reverse engineering thing?
But let’s keep trying. Were the author more good at this, he would have reversed Nerve Center some more and come up with h a solution… But we have no brain. Let’s reboot to Windows, there’s an idea.
There’s this thing in Windows – Device Manager, they call it. Has a lot of functions – but we’re interested in one. It allows you to disable devices. Simple and authorative.
So let’s just disable all the devices until our LED stops blinking!
This did it. If we peek at its Hardware ID – we’ll see what it is, that mysterious LED device.
Look – it’s him.
The device that has been pestering my dmesg for several years.
[Why wasn’t it shown in lsusb? It’s not USB at all, of course! The backlight uses a protocol called I2C HID – it allows the manufacturer to
Sidestep 2.5 – let’s make a commit
This search for the correct device has been heavily abridged. In the original telling, I had to open my setup almost all the way to the kernel before I realised why nothing worked. Also, I did not enjoy this dmesg log showing itself to me every time I unlock my computer. Since we’re here – why not go and write a short commit?
We need to find two things – the place where I2C HID devices are handled, and the place where their quirks are stored. Here. Let’s not think too hard and look at the error: incomplete report. Let’s make it complete.
Add a new quirk, let’s call it I2C_HID_QUIRK_BAD_INPUT_SIZE or something like that. To keep the theming.
Also let’s add our device to the list of quirked. What we’ve done so far:
1. Looked up “i2c hid linux kernel” in a search engine. Clicked the 4th result in DuckDuckGo.
2. Wrote three (!) words in English – BAD_INPUT_SIZE
3. Added one to BIT(4). Result – BIT(5).
4. Added a number to hid-ids.h – the ID of our device (not illustrated, but it’s similar in difficulty)
We’re programmers, let’s do some programming now.
See the line:
ret_size = ihid->inbuf | ihid->inbuf << 8;
And then it complains that ret_size is not what it ones.
Whenever our quirk is active, let’s do this but backwards.
Send the patch to the mailing list… (test it beforehand!). To be honest, getting into that mailing list was trickier than the actual patch. It’s not simple by any means.
Step 3 – driver!
At this point I remembered – I was trying to write a driver. Decided I won’t commit that to kernel yet – questions began arising, at this point you really have to think some. If anyone reading this is good at kernel and can consult me, I’d be glad to talk.
Let’s open python. (Used to be bash, but you change your mind very quickly with that sort of language). Let’s use the popular tool /dev/hidraw.
/dev/hidraw0, /dev/hidraw1 — how do those files work, anyways? For simple I/O you can use them as any other, and it seems that is would just work. And GetFeature and SetFeature – well, that’s a different story entirely.
StackOverflow (or someone else, I can’t remember) told me I would have to look into something called ioctl. It’s a special way of working with uncommon files like HID devices, terminals and similar disgusting things.
It seems simple on the surface – you give it an open file handle (not Python-level, OS-level! Read more on OS handles here if you'd like to know.), some number and a buffer – and then it does something with that buffer and gives it back to you. I’m not trying to be witty here, it’s just that it’s almost fully implementation-dependent. Here’s an example: 0xC0114806. What’s that? That’s SetFeature. That’s also
(6 << 29) | (ord('H') << 8) | (0x06 << 0) | (0x11 << 16).
The first 6 means the file will be open both for reading and for writing. We only write, but the docs said 6 that means we put 6. Ord(‘H’) stands for HID. Sometimes it stands for other things, depending on the file. But now it’s HID.
0x06 is the command itself. Sixth command of HID is SetFeature, which we need. And the last part? The buffer’s size.
All that remains is putting it together into an ioctl call, and you get a driver. It works..
Afterword from the author
Hope the article was an interesting read. Even a useful read, possibly. Some parts were omitted or shortened for brevity – a more observant reader might notice that the driver can read the keyboard’s current state, and the set_status function has a second ioctl call that wasn’t even mentioned in the text. Hardware development and the Linux kernel are big things, and one tale won’t cover it all. And in the end of the day, maybe the article isn’t about this. Maybe it’s just about how doing something small and good, for open-source or for yourself, is not hard at all. You just need to find a week’s worth of free evenings, some tea and a wish to dig around in code.