Kernel

What the Desktop Taught Me This Year

I spent much of this year fixing things that I first tried to solve at the wrong layer. A missing button event sent me into desktop settings. A wandering disk name produced a worse shell script. A driver race invited more logging. My instincts have been very energetic, if not consistently useful.

The better method is to follow the mechanism. Kernel devices appear through the device model and sysfs; udev applies naming policy. Input travels from the kernel through X.Org to applications. Qt objects have explicit parent ownership. Deferred kernel work has context and lifetime rules. Git history is a graph of objects, not a remote folder with ceremony.

KDE 3.5 ties the user-facing side together with unusual steadiness. Its maturity is not one clever feature but the accumulated reliability of applications, libraries, and conventions.

My practical conclusion for the year is to identify the boundary before editing the configuration or code. Ask what layer owns the decision, what evidence crosses that boundary, and what lifetime the data has. Then make one change and test it.

This sounds obvious when written in December. In February it apparently required an oops. Education remains committed to memorable examples.

Replacing the Directory Poll With inotify

Now that Linux 2.6.13 includes inotify, I replaced a timer in a small file-watching utility. The timer scanned a directory every second, which was simple, portable, and wasteful. It also introduced the charming choice between delayed updates and more frequent useless work.

My first inotify version assumed each read() returned one complete event. It worked during gentle testing and became nonsense when several files changed quickly. The interface returns a byte stream containing one or more variable-length struct inotify_event records. Correct code walks the buffer by each fixed header plus its len field.

union {
        struct inotify_event event;
        char bytes[4096];
} buffer;
ssize_t count;
size_t offset = 0;

do {
        count = read(fd, buffer.bytes, sizeof(buffer.bytes));
} while (count < 0 && errno == EINTR);

if (count < 0) {
        perror("read");
        exit(EXIT_FAILURE);
}
if (count == 0) {
        fprintf(stderr, "inotify descriptor reached EOF\n");
        exit(EXIT_FAILURE);
}

while (offset < (size_t)count) {
        struct inotify_event *event;
        size_t event_size;

        if ((size_t)count - offset < sizeof(*event)) {
                fprintf(stderr, "short inotify event header\n");
                exit(EXIT_FAILURE);
        }
        event = (struct inotify_event *)(buffer.bytes + offset);
        event_size = sizeof(*event) + event->len;
        if (event_size > (size_t)count - offset) {
                fprintf(stderr, "short inotify event name\n");
                exit(EXIT_FAILURE);
        }
        handle_event(event);
        offset += event_size;
}

The union gives the byte storage suitable alignment for struct inotify_event; a plain character array doesn’t promise that. The code also deals with interruption, read failure, EOF, and truncated records before treating bytes as an event. A watch identifies a pathname to the kernel, and events report changes associated with a watch descriptor.

The largest conceptual correction was learning that notifications are hints about state changes, not a private journal guaranteed to preserve my worldview. Events can arrive in bursts. A queue can overflow. A watched object can move or disappear. On overflow, the safe response is to rescan and rebuild known state rather than guess what was missed.

Renames deserve similar care. Related move events carry a cookie that can help pair them, but the application still needs sensible behavior when only one side is visible. Watching a directory also does not automatically mean recursively watching every directory below it.

I kept one function that performs a complete scan and made event handling update the same internal model. That gives the program a recovery path and keeps startup behavior consistent with overflow recovery. The event loop is an optimization over known state, not the only source of truth.

Compared with polling, inotify is immediate and avoids repeated directory walks when nothing changes. It also exposes more edge cases because the kernel reports actual operations rather than letting a periodic snapshot blur them together.

I prefer the new mechanism, with one reservation: event-driven doesn’t mean infallible. Read complete buffers, parse every record, expect overflow, and keep a rescan path. The kernel says something happened; deciding what the application should now believe is still our job.

Making a Use-After-Free Leave Tracks

A driver crashed several calls after an object had been freed. The final bad pointer looked plausible, and ordinary logs only showed that teardown had happened sometime earlier. I needed the stale access to leave a clearer fingerprint.

I rebuilt the test kernel with CONFIG_DEBUG_SLAB. The slab allocator’s debugging checks and poisoning replace freed storage with a recognizable pattern instead of leaving yesterday’s fields looking valid. The next failure stopped looking like random corruption: the pointer led into an object full of poison bytes.

That didn’t identify the owner by itself. I decoded the complete oops against the matching unstripped vmlinux, then followed the call chain back to a timer callback. The timer retained the private pointer after the remove path had called kfree().

The repair was pleasantly unoriginal: prevent rearming, delete the timer with the synchronizing form, and only then free the object. I ran repeated load, activity, and unload cycles with slab debugging still enabled. I also forced probe failures after each acquisition to make sure partially built objects followed the same lifetime rules.

Poisoning is a diagnostic, not memory safety. It works when the freed bytes haven’t already been reused, and it makes a debug kernel slower. Here it turned a believable stale structure into an unmistakably dead one, which was exactly the clue I needed.

Watching Linux History Move to Git

The interesting part of Git this week is no longer that it exists. Kernel developers are beginning to use it for actual Linux work, which changes the test from “can these objects be written?” to “can people exchange and trust history under pressure?”

I tried to imitate the old centralized habit by treating one repository as the place where truth lived and every other copy as a temporary client. That misses Git’s shape. Each repository can hold commits and their parent relationships. Fetching obtains objects and updates selected references; merging combines lines of development rather than asking a central server to manufacture history.

The kernel workflow makes the reason concrete. Maintainers can collect changes for their subsystem, preserve those commits, and pass a resulting line of history upward. The top-level maintainer can inspect and merge it. Signed e-mail and established review practices still matter; a fast object database does not decide whether a patch is wise.

I also learned not to confuse content identity with human trust. A hash can reveal that an object changed. It cannot tell me whether the author understood locking or whether I fetched from the person I intended. Technical integrity and project trust reinforce one another, but they are not synonyms.

Performance is already part of the appeal. Operations over the kernel tree need to be quick enough that developers use them freely. Cheap local history and branches encourage smaller experiments because recording work does not require a conversation with a remote machine.

That changes behavior as much as speed. A maintainer can inspect and organize work before publishing it instead of using the shared repository as a scratch pad.

The interface remains under construction, and I would not recommend that every project switch during lunch. Linux has an urgent need, unusually capable maintainers, and a willingness to work close to the machinery. That is a special environment, not a universal migration plan.

But adoption by the kernel gives Git a severe and useful proving ground. If it can preserve a large, branching C codebase while maintainers exchange changes independently, the design will have earned attention. I am keeping my test repository now. Calling it comfortable would be generous; calling it merely a toy would already be unfair.

A 2.6 Driver and the Trail of Evidence

I ported a small driver experiment to a newer 2.6 kernel this week. The old code compiled after a few mechanical edits, so I congratulated myself much too early. Loading the module produced an oops during device removal, which is the kernel’s way of reviewing code without softening the language.

My first attempt was to stare at the final faulting line and add a null check. That stopped one crash but left the design wrong. The pointer was not unexpectedly null; it referred to an object whose lifetime had ended. A defensive condition would merely make the race less visible.

The 2.6 device model asks drivers to express relationships among buses, devices, and drivers. Registration is not just bookkeeping. It establishes ownership and determines which callbacks can run as devices appear and disappear. I had treated remove as the reverse spelling of probe, but not as the reverse sequence of acquired resources.

I wrote down each successful step in probe: enable the device, reserve its region, allocate private state, initialize synchronization, and expose the interface. Then I made every failure path unwind only the steps already completed. remove performs the same cleanup in reverse order after preventing new work from entering.

static int sample_probe(struct device *device)
{
        struct sample *sample;
        int error;

        sample = kmalloc(sizeof(*sample), GFP_KERNEL);
        if (!sample)
                return -ENOMEM;

        memset(sample, 0, sizeof(*sample));
        error = reserve_hardware(sample);
        if (error)
                goto free_sample;

        /* Published callbacks must be able to find fully initialized state. */
        dev_set_drvdata(device, sample);

        error = register_interface(sample);
        if (error)
                goto clear_drvdata;

        return 0;

clear_drvdata:
        dev_set_drvdata(device, NULL);
release_hardware:
        release_hardware(sample);
free_sample:
        kfree(sample);
        return error;
}

This is shortened code, but the order matters: private data is attached before register_interface() makes callbacks possible. If publication fails, the failure path clears that pointer before releasing and freeing the state. In C, a linear chain of labels is often clearer than nested cleanup conditionals.

Removal uses the other half of the same rule:

static int sample_remove(struct device *device)
{
        struct sample *sample = dev_get_drvdata(device);

        unregister_interface(sample);
        stop_hardware(sample);
        cancel_deferred_work(sample);
        dev_set_drvdata(device, NULL);
        release_hardware(sample);
        kfree(sample);
        return 0;
}

unregister_interface() first prevents new user callbacks. The hardware is then stopped and outstanding work is drained while drvdata still points to live memory. Only after no callback can use it do I detach and free it.

For evidence, I enabled the kernel debugging options relevant to memory and locking and built a kernel specifically for this work. They cost performance, but a development kernel is a measuring instrument, not a racing bicycle. I also kept the unstripped vmlinux and the exact .config beside the test build.

When the next oops appeared, the symbolic trace identified both the callback and its caller. printk() markers around acquisition and release confirmed that queued work could still run after teardown began. The repair was to stop new requests, cancel or drain deferred activity, and only then free the state it could reference.

The order now looks like this:

block new work
stop device activity
wait for deferred work
remove user-visible interface
release hardware resources
free private state

I tested failure paths too. Returning an artificial error after each acquisition showed whether cleanup restored the previous state. Module load and unload in a loop then exercised the unremarkable path repeatedly. Repetition is valuable because races dislike appointments.

I included reference counts in the review even though the first crash did not mention them. A reference is a claim that an object must remain alive, not merely an integer to increment near a pointer. Every path acquiring one needs a visible release, and teardown must prevent new claims before waiting for old ones. Counting without a shutdown rule only measures how confused the lifetime has become.

Lock choice followed the same reasoning. I did not replace every lock with a larger one to make the warning disappear. I listed which fields formed one invariant and which contexts accessed them. A spinlock suited the short state transition shared with interrupt context; work that could sleep stayed outside it. Narrow critical sections made both the rules and the traces easier to read.

One subtle lesson was to distrust logs as timing-neutral. A printk() can alter scheduling enough to hide a race. I used it to establish broad ordering, then relied on assertions, debug checks, and repeated tests rather than declaring victory when extra logging made the crash vanish.

An oops after testing still required a disciplined capture. I saved the complete report, not only the final address, and matched it to the exact kernel image. A decoded call chain is context: it shows how execution arrived, which often matters more than the instruction that finally touched invalid memory.

The kernel APIs will continue to evolve, and copying a driver from an older tree will continue to be tempting. The durable knowledge is not a particular field name. It is the discipline of checking the current in-tree callers, understanding callback context, defining object lifetime, and making cleanup the exact inverse of setup.

I still prefer user-space debugging, where a bad pointer usually ruins one process rather than the afternoon. The kernel does leave a trail if the build preserves symbols and the code preserves invariants. That’s better than adding null checks until the machine goes quiet.