Auteur Archief

Adventure in the Linux Kernel

When I started debugging this issue, I had no idea how far down the rabbithole would go!

A few months ago, I was writing an end-to-end test for the Nextcloud integration with OnlyOffice under NixOS, to be run in a QEMU virtual machine. Curiously, my test suffered from corrupt reads from a host folder that was mounted into the VM. That was the start of a journey that led me deep into the Linux Kernel.

The circumstances under which the problem occured were very specific: a certain file, read from a host folder that was mounted into the VM, would consistently be corrupt. Specifically, this was a 12943-byte file, which inside of the VM would also be 12943 bytes, but only the first 0x3000 bytes were populated: the rest was filled with zeroes. At this point I was convinced it likely only affected a very limited set of users – but I was determined to get to the bottom of it.

Plan9

Mounting a directory from the host filesystem into a QEMU VM typically happens by passing something like -virtfs local,path=/nix/store to the qemu startup command. This tells QEMU to start a small server exposing the Plan 9 filesystem protocol (P9). The kernel ‘inside’ the VM can then use the 9P_FS kernel module to mount the directory over this protocol.

Adding some tracing options to QEMU:

virtualisation.qemu.options = [
  "-trace 'v9fs*'"
];

… I could observe that, whenever I read the file, this read would be split over two v9fs_read calls: one of 0x3000 bytes, and one of 655. The data from the second call was somehow ‘going missing’. Was the QEMU server serving an empty block, or was P9_FS inside the VM dropping them?
Patching for diagnostics

To get more insight into what was going on on the Linux Kernel side, I configured the system-under-test to log the first byte of each read:

boot.kernelPatches = [ {
  name = "enable p9 debugging";
  patch = ./kernel-rread.patch;
  extraConfig = ''
    NET_9P_DEBUG y
  '';
} ];

boot.extraModprobeConfig = ''
  # ERROR+9P+VFS+CACHE=1+4+8+8192=8205
  options 9pnet debug=8205
'';
--- a/net/9p/client.c
+++ b/net/9p/client.c
@@ -1599,7 +1599,7 @@ p9_client_read_once(struct p9_fid *fid, u64 offset, struct iov_iter *to,
    return 0;
  }
  
+ p9_debug(P9_DEBUG_9P, "<<< RREAD count %u, %02x\n", received, *dataptr);
  
  if (non_zc) {
    int n = copy_to_iter(dataptr, received, to);

Sure enough, I saw the 2 RREAD calls on the linux kernel logs as well, and the expected character showed up at the start of the second read.

Reading the code

Poring over the kernel source code, one thing that stood out was that the 9p client would use ‘zerocopy’ APIs for regular reads, but not for small requests:

/* Don't bother zerocopy for small IO (< 1024) */
if (clnt->trans_mod->zc_request && rsize > 1024) {
  /* response header len is 11
   * PDU Header(7) + IO Size (4)
   */
  req = p9_client_zc_rpc(clnt, P9_TREAD, to, NULL, rsize,
0, 11, "dqd", fid->fid,
offset, rsize);
} else {
  non_zc = true;
  req = p9_client_rpc(clnt, P9_TREAD, "dqd", fid->fid, offset,
rsize);
}

Sure enough, the initial 0x3000 read would be a ‘zerocopy’ read, while the second 655-byte read would be small enough to use a ‘regular’ read. Perhaps these APIs were incompatible? It didn’t seem that way – but some experiments with patches did confirm that always using zerocopy made the problem go away. It was clear that I wasn’t going to get closer to the root cause without better visibility into what was actually going on.

Would it be able to attach a debugger to the running kernel?

It seemed plausible: QEMU has facilities for attaching gdb, and sure enough I could:

virtualisation.qemu.options = [
  "-trace 'v9fs*'"
  # open gdbserver on TCP port 1234
  "-s"
];

While gdb successfully attached, it was… pretty much useless, since the running kernel was the compressed bzImage that does not include any debugging symbols. After some digging I found out Nix also ships a vmlinux that does have debugging symbols in the kernel.dev output – promising, but even with that I saw nothing but gibberish.

WHY2025

With this challenge top-of-mind I joined the Dutch hacker camp WHY2025, a great place to meet interesting people. I camped out at the Nix village:

There I bumped into Raito who pointed me to a thread on the kernel ‘regressions’ mailinglist where several people were running into a suspiciously-similar problem.
Debugging Symbols

Encouraged by the fact that the impact might be wider than I originally thought, I continued figuring out how to attach a debugger to the guest kernel. One obvious-in-hindsight piece of the puzzle was that I had to disable kernel Address Space Layout Randomization:

boot.kernelParams = [
  "nokaslr"
];

With that, I could set breakpoints and see reasonable backtraces!

Sadly, I could still not set breakpoints in 9p methods. Something extra was needed to support debugging code that’s in a kernel module – it turns out, a considerable side quest.

Module Debugging Symbols

While the command-line interface to gdb may look simple, looks can be deceiving: it’s a powerful tool. It even supports a plug-in architecture, and the Linux kernel comes with a gdb plugin for loading symbols for kernel modules written in Python.

This did require avoiding stripping the modules:

boot.kernelPackages = pkgs.linuxPackagesFor (pkgs.linux.overrideAttrs(a: {
  dontStrip = true;
}));

Also, part of the gdb plugin code is generated during the kernel build process, but the Nix packaging of the Linux kernel didn’t expose this yet. I patched the packaging to make this part of the dev output:

--- a/pkgs/os-specific/linux/kernel/manual-config.nix
+++ b/pkgs/os-specific/linux/kernel/manual-config.nix
@@ -325,6 +325,7 @@ lib.makeOverridable (
  "KBUILD_BUILD_VERSION=1-NixOS"
  kernelConf.target
  "vmlinux" # for "perf" and things like that
+ "scripts_gdb"
  ]
  ++ optional isModular "modules"
  ++ optionals buildDTBs [
@@ -432,6 +433,10 @@ lib.makeOverridable (
  postInstall = optionalString isModular ''
  mkdir -p $dev
  cp vmlinux $dev/
+
+ mkdir -p $dev/lib/modules/${modDirVersion}/build/scripts
+ cp -rL ../scripts/gdb/ $dev/lib/modules/${modDirVersion}/build/scripts
+
  if [ -z "''${dontStrip-}" ]; then
    installFlagsArray+=("INSTALL_MOD_STRIP=1")
  fi

Combined with just the right version of the Linux kernel sources (courtesy of pkgs.srcOnly), I could now fully debug the running kernel, including setting breakpoints and inspecting the local variables inside kernel modules.

Folio memory pages

Armed with this looking glass into the running kernel, I could now further inspect the (zerocopy and regular) calls dealing with the data read from the 9P protocol into the user buffers. With the help of some great articles on LWN I deciphered how the data was stored as a folio (basically, a structure of multiple memory regions that together form one ’logical’ region) with iov_iter structures pointing into those pages and coordinating operations on them.

Especially interesting was the observation that, after the first read, the folio was exactly 0x3000 (actually: three pages of 0x1000) in size. When the second p9 read happened, the folio would be ‘grown’ to make room for the additional 655 bytes and then the new data would be copied into it.

However, iov_iter is basically a ‘cursor’ into the folio. When the folio is exactly three 0x1000 segments, and the cursor is ‘at 0x3000’, where do you put it? There is no ‘fourth segment’ that you can put it on the beginning of, so apparently it is placed in the third segment, one byte past its last one. This is what confused the subsequent non-zerocopy read – and led to the problem of the bytes going ‘missing’.

Bringing in the big guns

With the problem pinned down like this, I felt comfortable sharing my findings on the regressions list. What’s more, I could share not only my description of the situation, but an actually-working Nix configuration demonstrating it. With that, 9P_FS maintainer Dominique Martinet could reproduce the issue within hours

this is a huge leap forward (hopeful it’s the same problem and we don’t have two similar issues lurking here…), we can’t thank you enough.

… and when I woke up to a beautiful sunny WHY2025 morning I found his preliminary patch in my inbox:

So that wasn’t a 9p bug, I’m not sure if I should be happy or not?

I’ve sent “proper-ish” patches at [1]

It’s taken some more reviews and iterations, but the fix is now making its way into various kernel releases.

Conclusion

What looked like a bug in an obscure corner of QEMU turned out to be a bug in the Linux Kernel Memory subsystem that, had we not found it, may have caused countless hard-to-diagnose issues in the future.

Big thanks to the NGI Fediversity programme, that saw the value of putting time into this lower-level issue and allowed me to dedicate part of the Source-based Nextcloud and OnlyOffice budget towards figuring this out – otherwise I might not have been able to justify the effort.

A lot of the work went into figuring out how exactly to put the various tools together to achieve the right diagnostic environment. With the scripts_gdb patch now merged and the required configuration documented as a working NixOS configuration, this should easy to set up from now on!

This article also appeared on my personal blog at https://arnout.engelen.eu/blog/linux-kernel-adventure/

#24

09 2025

Hack42 @ WHY2025

Al sinds 1989 wordt in Nederland iedere 4 jaar een grote hackersbijeenkomst georganiseerd, sinds de tweede editie in de vorm van een meerdaags kampeerevenement. Iedere editie vindt het evenement zichzelf opnieuw uit, inclusief nieuwe naam. HAR2009 gaf de start aan de eerste Nederlandse hackerspaces, waaronder Hack42. De nieuwste editie, WHY2025, was een paar weken geleden een groot succes – en Hack42 was weer goed vertegenwoordigd.

Zo kwamen de huisstijl en vlaggen onder andere van de hand van Janneke, was Moem actief voor team Deco, en heeft Malou onder meer de banners voor de party- en karaokebar, doorzichtige bekers, en de ‘mate market’-bordjes gemaakt en met Denz bars bemensd. Cat was actief in de keuken van Heaven, Jos zat in de programmacommissie en hp197 in PL. Ad en raboof hebben de social media, online en offline informatievoorziening onder handen genomen. Mack en Bix zorgden in Productiehuis voor de audio- en videotechniek, zowel ‘live’ als voor de opnames die weer snel op https://media.ccc.de/c/WHY2025 beschikbaar kwamen.

In de aanloop naar het evenement hebben we een van de orga-meet gehost, en ook waren we goed vertegenwoordigd in het programma: Stitch, Bix, Maja, Nino, BugBlue, Cyb0rg42 en raboof gaven een talk – soms zelfs meerdere!

Het was weer super hoe deelnemers uit de Nederlandse en internationale community allemaal hun unieke persoonlijke bijdrage leverden om er een veelkleurig evenement van te maken.

Tags: ,

#04

09 2025

Nix-meetup Software Freedom Day

Op 21 September, Software Freedom Day, verwelkomden we de Nederlandse Nix Meetup bij Hack42: rond de 20 geïnteresseerden, zowel bekende gezichten uit de community als nieuwe

Het formele programma bestond uit een presentatie van raboof over hoe Nix mooi samengaat met software freedom, en van Adriel Matei over zijn framework om een neovim-configuratie te genereren vanuit Nix.

Een groep Nix-geinteresseerden in de lounge

Na de presentaties bleef het nog lang gezellig, en zijn er al allerlei plannen gesmeed voor de volgende meetup. We kijken ernaar uit!

#24

09 2024

WiFi-repeater met Liminix

In een vorig blog hebben we een seriële aansluiting gesoldeerd aan een WiFi-router om het boot-proces te kunnen volgen en beinvloeden. Het uiteindelijke doel is Liminix te installeren op deze router. Gelukkig is deze router al goed ondersteund onder OpenWRT, dus dat is een mooi beginpunt.

OpenWRT-upgrade

Liminix is qua kernel gebaseerd op OpenWRT. Helaas zat Liminix nog op een oude OpenWRT-snapshot met Linux 5.15.71, waaronder deze router nog niet goed ondersteund wordt. Daarom hebben we eerst de OpenWRT in Liminix geupdate naar v23.05.2 met kernel 5.15.137.

Image-configuratie in Liminix

Een device in Liminix wordt gedefineerd in “devices/<devicenaam>/default.nix”. We introduceren dus een “devices/tp-archer-ax23/default.nix“, grotendeels gebaseerd op een bestaand device. We gebruiken hierbij de OpenWRT-documentatie om de architectuur, geheugen-offsets e.d. goed te kiezen, en de OpenWRT-code om een kernel-configuratie te genereren.

Booten met TFTP

De image-configuratie is een boel trial-and-error, waarbij de seriële terminal die we vorige keer hebben aangesloten onmisbaar was: via die weg kun je de U-Boot terminal benaderen.

Met Liminix kun je de tftpboot-output bouwen, die bestaat uit een image, een Device Tree (dtb), en instructies hoe je image en dtb naar het geheugen van het device kunt kopiëren en starten. De dtb wordt daarbij zo aangepast dat tijdens het bootproces het stuk van het geheugen waar het image zelf staat netjes wordt vermeden.

Het was nogal verwarrend dat de Liminix-instructies een bootm-commando met 3 parameters opleverde: het locatie van het image, de locatie van de ramdisk (if any), en de locatie van de FTD/dtb.

Helaas heeft the AX23 nog een oudere U-Boot versie, waarbij het bootm-commando nog maar 2 parameters krijgt. Dit betekent dat de dtb niet op de bootm-commandline kan worden meegegeven, maar aan het image moet worden geplakt. Daarvoor kan de tftpboot-module van Liminix iets worden aangepast.

TP-Link safeloader image

Nu we een image kunnen maken dat via TFTP netjes boot, werd het tijd een image te maken dat permanenter op de machine geflashed kan worden. Blijkbaar heeft TP-Link de U-Boot zo aangepast dat deze werkt met hun eigen ‘safeloader’-imageformaat. Als ik het goed begrijp hebben ze met dit formaat wat safeguards toegevoegd zodat je niet meer zomaar een image kunt uploaden dat voor een ander device bedoeld is. Gelukkig zat er in de OpenWRT firmware-utils al een tplink-safeloader tool waarvoor we een Liminix-module hebben toegevoegd.

Inrichting als repeater

Het mooie van Liminix is dat je de configuratie van je device in een soort configuratiebestand kunt beschrijven, en dan een image genereren.

Het configuratiebestand dat ik gebruik vind je hier. Omdat ik de router als WiFi-extender wilde inzetten, is het relatief eenvoudig: een ‘bridge’ om zowel de LAN-poorten als de WLAN-poorten, een DHCP-client om de router van een IP-adres te voorzien, en hostapd-configuratie voor beide WLAN-interfaces. Dit werkte vrij vlot, maar haalde teleurstellende snelheden. Nadat ik de hostapd-configuratie van OpenWRT heb overgenomen ging het wel snel. Ik heb geen bijzonder snelle internetverbinding, maar het is wel leuk dat WiFi nu ook op kantoor niet meer de bottleneck is in de verbinding met het internet:

speedtest-output

Volgende stappen

Voorlopig ben ik tevreden met het resultaat.

De meeste verbeteringen hebben we als PR aangeboden aan Liminix en zijn ondertussen geaccepteerd. Alleen de support voor U-Boot mboot prompts met maar 2 parameters zou eigenlijk nog een keer opgeschoond moeten worden zodat die ook kan worden aangeboden.

Wel moet ik nog steeds met ethernet-kabels aan de slag om nieuwe images te flashen. Het zou mooi zijn over het netwerk te kunnen updaten. Liminix heeft daar wel modules voor, daar moet ik me nog verder in verdiepen.

#03

04 2024

Verslag: I Love Free Software Day 2024

“I Love Free Software Day” is een leuke positieve campagne van de Free Software Foundation Europe (FSFE) om op valentijnsdag de mensen te bedanken die vrije software mogelijk maken. Met vele bijeenkomsten is het ook een goede gelegenheid om gezellig samen te komen. In Nederland was er een workshop in Den Haag en een bijeenkomst bij ons.

De bijeenkomst begon met een rondleiding voor iedereen die nog niet in de hackerspace was geweest. Toen konden we echt beginnen. Eerst een voorstelronde onder het genot van stukken pizza. Iedereen vertelde over persoonlijk ervaringen met vrije software en over de software en mensen die een bedankje verdienden.

Group picture of attendees (except for photographer)

Op deze manier maakten we kennis met veel verschillende software: webbrowsers Waterfox en Firefox, browser extensie Vimium, desktopomgeving KDE, muziekprogramma Audacity, tekstverwerker Vim (in memoriam Bram Moolenaar), foto importsoftware Rapid Photo Downloader, smartphone besturingssystemen CalyxOS en UBports, smartphone installer OpenAndroidInstaller, catalogussoftware Omeka, compiler Free Pascal, onafhankelijke cloudomgeving Nextcloud, documentverwerker LibreOffice en huisautomatiseringsoftware Home Assistant. Dat was een enorm inspirerende en leerzame rondgang. Opmerkelijk was dat voor vrijwel iedereen Firefox een van de eerste vrije software applicaties was.

Het schrijven van bedankjes begon vooral met wat emails en chatberichten omdat van veel projecten en ontwikkelaars geen postadres bekend is. Maar na wat zoeken kwamen er steeds meer handgeschreven I Love Free Software Day-briefkaarten op tafel te liggen, klaar om te versturen. Het was mooi om te zien hoe de samenwerking werd gezocht door elkaars kaarten te steunen met een ondertekening. Terwijl we bezig waren zagen we online op social media ook al de nodige bedankjes. Zo viel ons oog op de geanimeerde harten door Fedora.

Written I Love Free Software postcards

Het was een gezellige avond die echt de community bij elkaar heeft gebracht. Het was mooi om het enthousiasme te zien en de lieve woorden te horen. Hopelijk hebben de gestuurde bedankjes een grote impact.

Ik kijk nu al uit naar de liefdevolle bijeenkomst volgend jaar.

#28

03 2024

WiFi-router met aanhangsel

Om meer inzicht te krijgen in het boot-proces, en later aan de slag te gaan met custom firmware zoals OpenWrt en Liminix, wilden we graag toegang tot de seriele poort van deze WiFi-router:

TP-Link Archer AX23 with a serial-USB connector sticking out of it

Hiervoor hebben we eerst het doosje opengemaakt en zijn op zoek gegaan naar de juiste contacten. Dit was nog niet gedocumenteerd, maar de vier niet-aangesloten contacten op een rijtje waren een goede gok. Met de multimeter was goed te zien dat het tweede contact de GND was, de andere drie contacten allemaal zo rond de 3.3v zaten. Met de ‘frequency’-setting van de multimeter was goed te zien dat het laatste contact het meest ‘noisy’ was, waaraan we afleidden dat dat waarschijnlijk de TX was. Het derde contact had een net iets zwakkere stroom dan de eerste, dus RX.

We hadden een aantal USB-naar-serial-converters liggen. Aangezien we niet wilden riskeren e.e.a. te beschadigen maten we eerst de stroomsterkte van de converters. Eentje had een signaal van 3,7V, dus die hebben we voor de zekerheid opzij gelegd. Met “screen /dev/ttyUSB0 115200” konden we snel bevestigen dat we de juiste pins te pakken hadden: zowel output als input werkten direct. Voor later gebruik hebben we er wat Dupont-draadjes aangesoldeerd:

De printplaat van de AX23, met draadjes aan de seriele verbinding gesoldeerd en een USB-serial converter

Voor het nageslacht hebben we de foto met instructies ook aan de OpenWrt-documentatie toegevoegd op https://openwrt.org/toh/tp-link/archer_ax23_v1#serial

#30

01 2024