Debugging

When Rendering Bugs Move

A Qt application of mine began drawing stale text after I scrolled a custom widget. The bug appeared only with desktop compositing enabled, so I immediately blamed the driver. Then I ran it on a different card and saw the same stale text in a slightly different place.

My naive fix was to call update() repeatedly after scrolling. That reduced the frequency without curing it. Repainting more often can hide an invalid region briefly, but it cannot make incorrect geometry correct.

I reduced the widget until it painted only two rectangles and a label. The problem remained when one rectangle moved outside the area returned by the widget’s internal item bounds. My clipping assumptions were wrong: I invalidated the old logical rectangle but painted a pen and text extending beyond it. Compositing changed timing and exposure enough to reveal the mistake more reliably.

After expanding the dirty region to include every painted pixel, both drivers behaved. I then tested without compositing, with XRender compositing and with OpenGL compositing. That matrix is useful because a defect that follows one backend suggests a different layer from a defect that follows the application everywhere.

Adding a plain background behind the moving item made stale areas obvious and gave the reduced test a result anyone could recognize immediately.

Driver debugging still requires exact information. Card family, kernel module, X driver, Mesa version, colour depth and compositor backend can all matter. “Latest driver” is not a version, especially a week later. A minimal program is worth far more than a description of a large application that sometimes looks odd.

The most useful distinction is between wrong commands and wrong execution. If the application issues the wrong clipping, geometry or lifetime operations, changing drivers may only move the symptom. If a tiny correct sequence fails on one driver and succeeds elsewhere, the driver report becomes persuasive.

My practical conclusion is to simplify before assigning blame. Disable layers one at a time, record the exact matrix, and inspect the application’s painted bounds even when hardware acceleration appears implicated. Graphics stacks are complicated enough without accusing the innocent portion first. Unfortunately, every portion has previous convictions.

Chasing Black Windows

After a driver update, newly opened windows sometimes became black under KWin’s OpenGL compositor. The same windows were fine with XRender. Turning off every effect changed nothing, which at least got the animations out of the suspect list.

The useful split was the texture-from-pixmap path. Under X11 compositing, the server redirects a window into a pixmap. KWin’s OpenGL path then uses the driver’s texture-from-pixmap support to bind that pixmap as a texture for the scene. XRender composites the pixmap through a different route. Applications had painted the right pixels; the OpenGL path was failing to present them.

I could make it happen by opening two terminals, covering one, and uncovering it. Damage arrived, but the exposed window stayed black or returned in strips. Switching to the previous driver fixed that exact sequence. The newer driver also behaved if I disabled its acceleration option, slowly.

That made a much better report than “KWin draws black windows.” I included the X server, Mesa, kernel and driver versions, the OpenGL backend, hardware identifiers, and the short cover/uncover sequence. I also noted that XRender worked. That last fact tells a driver developer far more than a screenshot of a black rectangle.

Screenshots were awkward: some captured the correct window even while the display showed black. The redirected pixmap could be fine while the texture or final scanout was stale. A camera photo was ugly but honest.

I am using XRender for normal work and keeping the failing setup available for testing. Reinstalling KWin would only replace the code that asks the driver to bind the same pixmap. The break is farther down the path, and now there is a reproducible way to reach it.

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.

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.

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.