Blog

Debugging the Desktop From evdev to X.Org

An extra mouse button worked in one application but not another, which sent me directly to the KDE control center. I changed mappings until ordinary buttons became surprising too. This was useful only as a demonstration that random adjustments scale badly.

I started over at the bottom of the stack. The kernel input layer exposes event devices and reports key, button, and relative motion events. X.Org opens an input device through its configured driver, translates those events into the X input model, and sends them to clients. KDE and its applications then apply shortcuts or application behavior.

That sequence gives each test a specific question. Kernel event diagnostics answer whether hardware input reaches the kernel. The X.Org log answers which device and driver the server opened. xev answers which X events a client receives. An application test answers what that client does with them.

The button appeared at the kernel layer but was mapped unexpectedly by X.Org. I had copied a configuration stanza from a different mouse, including a button mapping whose numbers reflected that device. Removing the borrowed assumption and mapping the observed buttons deliberately fixed the problem.

The exercise also exposed a trap in logs. A warning near the end of Xorg.0.log looked relevant because it mentioned input, but the earlier device initialization lines showed that it belonged to another configured device. Timestamps, identifiers, and full initialization sections matter more than a conveniently alarming word.

I saved a small checklist with the configuration:

1. Does the kernel report the event?
2. Which device does X.Org open?
3. What does xev receive?
4. How does the application interpret it?

This is not sophisticated debugging, and that is why it works. Each layer has a narrow contract. The first failed contract determines where to investigate; layers above it cannot repair missing information.

Desktop Linux can feel complicated because these boundaries are visible. I prefer visible complexity to one giant mystery, but only when I respect the boundaries. Starting at the application and changing every layer on the way down is not diagnosis. It is redecorating the staircase while looking for a leak.

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.

Porting a Qt 3 Dialog to Qt 4

Qt 4.0 is out, so I chose a small Qt 3.3 dialog as a porting exercise. “Small” was important. Porting an entire application before understanding one dialog is how weekends acquire stack traces.

My first attempt was purely mechanical: change includes until the compiler became quiet. It became quieter, but not helpful. Qt 4 reorganizes classes into modules, changes parts of the collection and painting APIs, and separates concerns that older code often mixed together. A successful compile would not prove that I had preserved ownership, event behavior, or rendering.

I started again by writing down the dialog’s behavior. It displays a list of files, accepts a selection, paints a small preview, and emits a result. That description gave me four pieces to port and test independently instead of one pile of compiler diagnostics.

The build was the first useful boundary. The project now names the Qt modules it uses rather than assuming one enormous library. Includes should name the class headers actually required. This exposed accidental dependencies that had been hidden by broad headers in the old source.

Collections needed care, but not cleverness. I resisted making a grand compatibility wrapper around every changed type. This application has one current port, not a treaty organization. At each call site I checked iterator behavior, copying, and whether Qt or standard C++ ownership applied.

The list view was the largest conceptual change. Old code tended to let a widget store items and presentation behavior together. Qt 4’s item-view approach makes the model explicit. My naive response was to force the old item manipulation into the new widget. The cleaner solution was to expose the file data through a model and let the view handle selection and display.

That split initially looked like extra ceremony. It paid for itself when I wanted to sort the view without rearranging the underlying file objects. Presentation order and data ownership are not the same thing, and the API now encourages me to admit it.

Painting also demanded attention. The preview had drawn directly in a way that relied on Qt 3 behavior. In Qt 4 I kept custom drawing inside the paint event, used the supplied paint device, and treated coordinates and clipping as part of the callback contract.

void Preview::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setClipRegion(event->region());
    painter.drawPixmap(0, 0, previewPixmap);
}

This is deliberately plain. Caching the pixmap avoids repeating file work during repainting, while the widget remains responsible for drawing itself when requested. Calling painting code from arbitrary places had seemed direct in the old version; it was actually borrowing assumptions from the event loop.

Object ownership survived mostly intact because parent-child relationships still provide a clear lifetime for QObject instances. I nevertheless checked every allocation. A port is a good time to discover that an object was parented only by luck or that two cleanup paths both believed they were in charge.

Signals and slots remain familiar, which made them easy to overlook. I checked each important connect() and exercised the dialog through the keyboard as well as the mouse. Compilation catches C++ type errors; it does not guarantee that a runtime signal signature agrees with its slot.

I kept the Qt 3 version running beside the port and compared behavior with the same small directory. Selection changes, empty lists, unreadable files, cancellation, and repeated opening all went on a short test sheet. The empty case found a stale selection pointer that the happy path had concealed.

The temptation during a port is to redesign everything disliked in the old code. I limited changes to those required by the new APIs or needed to make the behavior testable. Otherwise a regression becomes impossible to classify: was it introduced by Qt 4, the port, or my spontaneous improvement campaign?

My early opinion is that Qt 4 asks C++ applications to make more structure explicit, particularly around data and views. That can feel verbose when porting a tiny widget, but it scales better than teaching every widget to own, sort, and display its own private universe.

The useful conclusion is to port by responsibility, not by compiler error. Establish a build boundary, preserve observable behavior, understand the replacement mechanism, and test ownership and callbacks. The compiler is an excellent assistant, but it has never used the dialog and is therefore a poor product manager.

KDE 3.4 at Working Speed

KDE 3.4 brought Akregator into the standard release, so I moved one concrete morning routine into it: read a few dozen project feeds, stop halfway, and continue later through Kontact.

The first useful behavior is dull but essential: read state survives. I marked a few entries read in Akregator, closed Kontact, fetched another batch, and reopened it. Old articles stayed read and new ones remained easy to spot. A feed reader that forgets this turns every restart into archaeology.

Kontact integration matters because it doesn’t create a second, slightly different copy of the feeds. Opening the Akregator component in Kontact and the standalone application showed the same subscriptions and article state. Links still opened through KDE’s normal browser handling, so reading a project post didn’t require a new set of application preferences.

I also disconnected the network before opening cached entries. Text already fetched remained available, while a refresh failed visibly instead of silently marking feeds current. That’s the sort of release behavior I care about: the application is honest about which state is local and which depends on the network.

Duplicate handling needed a closer look. Several feeds republish an item with corrected text, and some are careless with identifiers. Akregator mostly kept the list stable rather than presenting every refresh as breaking news. Where a feed changed identity as well as content, the duplicate was at least explainable from the source.

There are rough edges. Feed folders and filters invite more organization than I want before coffee, and malformed feeds remain somebody’s debugging exercise. I resisted building a taxonomy and kept three folders.

This is a small addition to a large desktop release, but it shows the advantage of KDE’s shared parts. Akregator can live inside Kontact, hand links to the usual browser, and use familiar shortcuts without pretending feed reading is a separate universe.

After a week, the best result is that I can stop reading and trust the list when I return. That’s enough; the feeds don’t need to become a lifestyle.