# Sterownik ethernetu Krótki wstęp na [bugzilla.kernel.org](https://bugzilla.kernel.org/show_bug.cgi?id=102171). TLDR: [ta funkcja](https://code.woboq.org/linux/linux/drivers/net/ethernet/atheros/alx/main.c.html#alx_intr_handle_misc) czasem zaspamowuje logi (`fatal interrupt 0x4001607, resetting`), i jak się spróbuje wyłączyć ten sterownik, to system zamiera proces po procesie. Winny jest pewnie sprzęt, ale świrująca karta sieciowa powinna wywalać co najwyżej swój sterownik, a nie stos sieciowy, czy całe jądro. Co robić jak żyć? Na szczęście jest [Kdump](https://www.kernel.org/doc/html/latest/admin-guide/kdump/kdump.html) i [crash](https://people.redhat.com/~anderson/) (dostępny pakiet na redhatach i gentoo), żeby obejrzeć, co się dzieje. ## Co mówi nam `crash` Pierwszy zrzut pamięci zrobiłem wykonując `modprobe -r alx` (usunięcie modułu) i doprowadzając tym do jakiegoś `OOPS`, a że ustawiłem ```sh CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_ON_OOPS_VALUE=1 ``` (można to zrobić równie dobrze w linii poleceń jądra), to wysypało się. uruchamiamy: ```sh $ crash vmlinux alx-rmmod.core KERNEL: vmlinux DUMPFILE: alx-rmmod.core CPUS: 4 DATE: Sat Apr 11 12:38:00 2020 UPTIME: 19:08:47 LOAD AVERAGE: 2.51, 2.80, 2.51 TASKS: 240 NODENAME: arusekk RELEASE: 5.6.2-gentoo VERSION: #4 SMP Wed Apr 8 15:40:31 CEST 2020 MACHINE: x86_64 (2494 Mhz) MEMORY: 7.7 GB PANIC: "Oops: 0000 [#1] SMP" (check log for details) PID: 2418 COMMAND: "kworker/3:4" TASK: ffff888255d62d80 [THREAD_INFO: ffff888255d62d80] CPU: 3 STATE: TASK_RUNNING (PANIC) ``` Możemy sprawdzić, na co czekało `modprobe -r alx` (pid 6884 poznałem wpisując ps) ``` crash> bt 6884 PID: 6884 TASK: ffff888264a91080 CPU: 0 COMMAND: "modprobe" #0 [ffffc90001c6fc40] __schedule at ffffffff8191a8c4 #1 [ffffc90001c6fcc0] schedule at ffffffff8191ad25 #2 [ffffc90001c6fcd8] schedule_timeout at ffffffff8191df34 #3 [ffffc90001c6fd50] wait_for_completion at ffffffff8191bdbe #4 [ffffc90001c6fda8] rcu_barrier at ffffffff811096b0 #5 [ffffc90001c6fdc0] netdev_run_todo at ffffffff81830358 #6 [ffffc90001c6fe20] alx_remove at ffffffffa01a4b59 [alx] #7 [ffffc90001c6fe38] pci_device_remove at ffffffff814ca041 #8 [ffffc90001c6fe58] device_release_driver_internal at ffffffff816f6d5c #9 [ffffc90001c6fe78] driver_detach at ffffffff816f6e8a ##10 [ffffc90001c6fe90] bus_remove_driver at ffffffff816f5c6a ##11 [ffffc90001c6fea0] pci_unregister_driver at ffffffff814c873f ##12 [ffffc90001c6fed0] __x64_sys_delete_module at ffffffff81129dea ##13 [ffffc90001c6ff38] do_syscall_64 at ffffffff81001b21 ##14 [ffffc90001c6ff50] entry_SYSCALL_64_after_hwframe at ffffffff81a00068 RIP: 00007f3376509f57 RSP: 00007ffd5b3e7e48 RFLAGS: 00000206 RAX: ffffffffffffffda RBX: 0000555d882d5b70 RCX: 00007f3376509f57 RDX: 0000000000000000 RSI: 0000000000000800 RDI: 0000555d882d5bd8 RBP: 0000555d882d5b70 R8: 0000000000000000 R9: 0000000000000000 R10: fffffffffffffcf9 R11: 0000000000000206 R12: 0000555d882d5bd8 R13: 0000000000000000 R14: 0000000000000000 R15: 0000555d882d5b70 ORIG_RAX: 00000000000000b0 CS: 0033 SS: 002b ``` No fajnie, to teraz sprawdźmy, skąd się wziął popłoch: ``` crash> bt -p PID: 2418 TASK: ffff888255d62d80 CPU: 3 COMMAND: "kworker/3:4" #0 [ffffc90002697bf8] machine_kexec at ffffffff81040103 #1 [ffffc90002697c40] __crash_kexec at ffffffff8112f008 #2 [ffffc90002697d08] crash_kexec at ffffffff8112fc80 #3 [ffffc90002697d18] oops_end at ffffffff8101514e #4 [ffffc90002697d38] no_context at ffffffff81047f43 #5 [ffffc90002697da0] page_fault at ffffffff81a00ecf [exception RIP: alx_reinit_rings+39] RIP: ffffffffa01a6177 RSP: ffffc90002697e58 RFLAGS: 00010246 RAX: 0000000000000000 RBX: ffff888262589880 RCX: 0000000000000000 RDX: 0000000000000000 RSI: 0000000000000002 RDI: ffff888262589880 RBP: ffff888262589880 R8: ffff888266000240 R9: ffff888266000300 R10: 0000000000000000 R11: ffffffff822241c8 R12: ffff8882674e1600 R13: 0000000000000000 R14: ffff8882674e5400 R15: 0000000000000000 ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018 #6 [ffffc90002697e60] alx_activate at ffffffffa01a62a9 [alx] #7 [ffffc90002697e70] alx_reset at ffffffffa01a66d5 [alx] #8 [ffffc90002697e80] process_one_work at ffffffff810be9b7 #9 [ffffc90002697eb8] worker_thread at ffffffff810bf3d5 ##10 [ffffc90002697f10] kthread at ffffffff810c5c5a ##11 [ffffc90002697f50] ret_from_fork at ffffffff81a0015f ``` Jak widać, jest to wątek jądra, jakiś `worker_thread` obsługujący reset urządzenia. Przekonajmy się, co to za kod się wyłożył. ```c crash> add-symbol-file /usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko 0xffffffffa01a4000 add symbol table from file "/usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko" at .text_addr = 0xffffffffa01a4000 Reading symbols from /usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko...done. crash> disassemble /m alx_reinit_rings Dump of assembler code for function alx_reinit_rings: 451 static void alx_init_ring_ptrs(struct alx_priv *alx) 452 { 453 struct alx_hw *hw = &alx->hw; 454 u32 addr_hi = ((u64)alx->descmem.dma) >> 32; 455 struct alx_napi *np; 456 int i; 457 458 for (i = 0; i < alx->num_napi; i++) { 0xffffffffa01a6165 <+21>: test %eax,%eax 0xffffffffa01a6167 <+23>: jle 0xffffffffa01a6221 <alx_reinit_rings+209> 0xffffffffa01a616d <+29>: xor %edx,%edx 0xffffffffa01a6211 <+193>: add $0x1,%rdx 0xffffffffa01a6215 <+197>: cmp %edx,0x278(%rbp) 0xffffffffa01a621b <+203>: jg 0xffffffffa01a616f <alx_reinit_rings+31> 459 np = alx->qnapi[i]; 0xffffffffa01a616f <+31>: mov 0x230(%rbp,%rdx,8),%rax 460 if (np->txq) { 0xffffffffa01a6177 <+39>: mov 0x190(%rax),%rcx ``` Rejestr `rax` (wskazujący na np) zawiera zero, stąd też nie możemy odczytać, co w nim jest. ## Prawdziwa przyczyna Wnioskuję łatwo, że rejestr `rbp` wskazuje na `struct alx_priv` ```c crash> ptype (struct alx_priv*)0xffff888262589880 type = struct alx_priv { struct net_device *dev; struct alx_hw hw; int num_vec; struct { dma_addr_t dma; void *virt; unsigned int size; } descmem; struct alx_napi *qnapi[8]; int num_txq; int num_rxq; int num_napi; spinlock_t irq_lock; u32 int_mask; unsigned int tx_ringsz; unsigned int rx_ringsz; unsigned int rxbuf_size; struct work_struct link_check_wk; struct work_struct reset_wk; u16 msg_enable; spinlock_t stats_lock; } * crash> p ((struct alx_priv*)0xffff888262589880)->qnapi $1 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} ``` No to widzimy, że ktoś wyczyścił tablicę `qnapi`, kiedy my jeszcze wykonujemy `alx_activate`. I w tym problem, że w ogóle je wykonujemy. ## Kod sterownika ```sh $ cd /usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx ``` Interesuje nas tu tylko `main.c`. Zajmijmy się póki co wątkiem przerwania (tym, który dokonał dereferencji zera). Ze stosu wywołań widać, że wykonuje się w kontekście zadania `alx_reset`. Jedyne odwołanie do `alx_reset` to przypisanie go do obsługi zadania `reset_wk`, ustawianego w `alx_probe`. ```c=1830 INIT_WORK(&alx->reset_wk, alx_reset); ``` Ta jest kolejkowana przez ```c=212 static void alx_schedule_reset(struct alx_priv *alx) { schedule_work(&alx->reset_wk); } ``` Wywołaną przez ```c=331 static bool alx_intr_handle_misc(struct alx_priv *alx, u32 intr) { struct alx_hw *hw = &alx->hw; if (intr & ALX_ISR_FATAL) { netif_warn(alx, hw, alx->dev, "fatal interrupt 0x%x, resetting\n", intr); alx_schedule_reset(alx); return true; } ``` w kontekście przerwania sprzętowego. Teraz przejdźmy do wątku `modprobe -r alx`. Wyzerował on `qnapi`, które jest zerowane tylko w `alx_free_napis`, które wykonywane jest tylko przez `alx_stop` i podczas błędów inicjalizacji. ```c=1856 static void alx_remove(struct pci_dev *pdev) { struct alx_priv *alx = pci_get_drvdata(pdev); struct alx_hw *hw = &alx->hw; cancel_work_sync(&alx->link_check_wk); cancel_work_sync(&alx->reset_wk); /* restore permanent mac address */ alx_set_macaddr(hw, hw->perm_addr); unregister_netdev(alx->dev); /* dalej nie dochodzi */ ``` Tu pojawiają się kolejne pliki z kodu źródłowego jądra: `net/core/dev.c` ```c=9985 void unregister_netdev(struct net_device *dev) { rtnl_lock(); unregister_netdevice(dev); rtnl_unlock(); } ``` Tu wisi proces czekając na nigdy: ```c=9571 /* The sequence is: * * rtnl_lock(); * ... * register_netdevice(x1); * register_netdevice(x2); * ... * unregister_netdevice(y1); * unregister_netdevice(y2); * ... * rtnl_unlock(); * free_netdev(y1); * free_netdev(y2); * * We are invoked by rtnl_unlock(). * This allows us to deal with problems: * 1) We can delete sysfs objects which invoke hotplug * without deadlocking with linkwatch via keventd. * 2) Since we run with the RTNL semaphore not held, we can sleep * safely in order to wait for the netdev refcnt to drop to zero. * * We must not return until all unregister events added during * the interval the lock was held have been completed. */ void netdev_run_todo(void) { struct list_head list; /* Snapshot list, allow later requests */ list_replace_init(&net_todo_list, &list); __rtnl_unlock(); /* Wait for rcu callbacks to finish before next phase */ if (!list_empty(&list)) rcu_barrier(); /* <==== */ ``` Więc interesuje nas poprzednia funkcja (z `include/linux/netdevice.h`) ```c=2713 static inline void unregister_netdevice(struct net_device *dev) { unregister_netdevice_queue(dev, NULL); } ``` znów `net/core/dev.c` ```c=9940 void unregister_netdevice_queue(struct net_device *dev, struct list_head *head) { ASSERT_RTNL(); if (head) { list_move_tail(&dev->unreg_list, head); } else { rollback_registered(dev); /* Finish processing unregister after unlock */ net_set_todo(dev); } } ``` dalej: ```c=8850 static void rollback_registered(struct net_device *dev) { LIST_HEAD(single); list_add(&dev->unreg_list, &single); rollback_registered_many(&single); list_del(&single); } ``` czyli ```c=8759 static void rollback_registered_many(struct list_head *head) { struct net_device *dev, *tmp; LIST_HEAD(close_head); BUG_ON(dev_boot_phase); ASSERT_RTNL(); list_for_each_entry_safe(dev, tmp, head, unreg_list) { /* Some devices call without registering * for initialization unwind. Remove those * devices and proceed with the remaining. */ if (dev->reg_state == NETREG_UNINITIALIZED) { pr_debug("unregister_netdevice: device %s/%p never was registered\n", dev->name, dev); WARN_ON(1); list_del(&dev->unreg_list); continue; } dev->dismantle = true; BUG_ON(dev->reg_state != NETREG_REGISTERED); } /* If device is running, close it first. */ list_for_each_entry(dev, head, unreg_list) list_add_tail(&dev->close_list, &close_head); dev_close_many(&close_head, true); /* dalej nieistotne */ ``` co z kolei okazuje się wykonywać `ndo_stop()` ```c=1483 static void __dev_close_many(struct list_head *head) { struct net_device *dev; ASSERT_RTNL(); might_sleep(); list_for_each_entry(dev, head, close_list) { /* Temporarily disable netpoll until the interface is down */ netpoll_poll_disable(dev); call_netdevice_notifiers(NETDEV_GOING_DOWN, dev); clear_bit(__LINK_STATE_START, &dev->state); /* Synchronize to scheduled poll. We cannot touch poll list, it * can be even on different cpu. So just clear netif_running(). * * dev->stop() will invoke napi_disable() on all of it's * napi_struct instances on this device. */ smp_mb__after_atomic(); /* Commit netif_running(). */ } dev_deactivate_many(head); list_for_each_entry(dev, head, close_list) { const struct net_device_ops *ops = dev->netdev_ops; /* * Call the device specific close. This cannot fail. * Only if device is UP * * We allow it to be called even after a DETACH hot-plug * event. */ if (ops->ndo_stop) ops->ndo_stop(dev); /* <===== TUTAJ */ dev->flags &= ~IFF_UP; netpoll_poll_enable(dev); } } static void __dev_close(struct net_device *dev) { LIST_HEAD(single); list_add(&dev->close_list, &single); __dev_close_many(&single); list_del(&single); } void dev_close_many(struct list_head *head, bool unlink) { struct net_device *dev, *tmp; /* Remove the devices that don't need to be closed */ list_for_each_entry_safe(dev, tmp, head, close_list) if (!(dev->flags & IFF_UP)) list_del_init(&dev->close_list); __dev_close_many(head); list_for_each_entry_safe(dev, tmp, head, close_list) { rtmsg_ifinfo(RTM_NEWLINK, dev, IFF_UP|IFF_RUNNING, GFP_KERNEL); call_netdevice_notifiers(NETDEV_DOWN, dev); if (unlink) list_del_init(&dev->close_list); } } ``` Za to nasze `ndo_stop` to `alx_stop` (powrót do drivers/net/ethernet/atheros/alx/main.c): ```c=1673 static const struct net_device_ops alx_netdev_ops = { .ndo_open = alx_open, .ndo_stop = alx_stop, ``` ```c=1340 static int alx_stop(struct net_device *netdev) { __alx_stop(netdev_priv(netdev)); return 0; } ``` ```c=1250 static void __alx_stop(struct alx_priv *alx) { alx_halt(alx); alx_free_irq(alx); alx_free_rings(alx); alx_free_napis(alx); /* <=== to on dealokuje rzeczy */ } ``` Uff. Gdzie zatem jest błąd? W pewnym sensie w `alx_remove`, w pewnym w `alx_(schedule_)reset`. Przypomnijmy kod `alx_remove`: ```c=1856 static void alx_remove(struct pci_dev *pdev) { struct alx_priv *alx = pci_get_drvdata(pdev); struct alx_hw *hw = &alx->hw; cancel_work_sync(&alx->link_check_wk); cancel_work_sync(&alx->reset_wk); /* restore permanent mac address */ alx_set_macaddr(hw, hw->perm_addr); unregister_netdev(alx->dev); /* dalej nie dochodzi */ ``` Otóż programista prawdobodobnie nie był świadomy, że `cancel_work_sync` nie chroni nas przed przyszłym `schedule_work`. ```c=212 static void alx_schedule_reset(struct alx_priv *alx) { schedule_work(&alx->reset_wk); } ``` Należałoby wprowadzić jakieś pole, w którym zapisywalibyśmy, że sterownik jest w trakcie wyłączania. [FreeBSD](http://bxr.su/FreeBSD/sys/dev/alc/if_alc.c#alc_int_task) rozwiązuje ten problem nieco inaczej. ## Rzeczywista przyczyna spamu `fatal int fatal interrupt 0x4001607, resetting` To jest trudniejsze, nie pokażę, dlaczego, bo nie wiem. ```sh $ crash vmlinux alx-sysrq.core KERNEL: vmlinux DUMPFILE: alx-sysrq.core CPUS: 4 DATE: Mon Apr 13 21:07:33 2020 UPTIME: 10:09:46 LOAD AVERAGE: 1.89, 3.07, 3.54 TASKS: 111 NODENAME: arusekk RELEASE: 5.6.2-gentoo VERSION: #4 SMP Wed Apr 8 15:40:31 CEST 2020 MACHINE: x86_64 (2494 Mhz) MEMORY: 7.7 GB PANIC: "Kernel panic - not syncing: sysrq triggered crash" PID: 0 COMMAND: "swapper/0" TASK: ffffffff8220f4c0 (1 of 4) [THREAD_INFO: ffffffff8220f4c0] CPU: 0 STATE: TASK_RUNNING (PANIC) ``` ```c crash> add-symbol-file /usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko 0xffffffffa001a000 add symbol table from file "/usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko" at .text_addr = 0xffffffffa001a000 Reading symbols from /usr/src/linux-5.6.2-gentoo/drivers/net/ethernet/atheros/alx/alx.ko...done. ``` Wygląda na to, że urządzenie zgłasza co chwilę NMI, że jest w złym stanie, a wszystkie rejestry ma wypełnione jedynkami. Jednak z jakiegoś powodu automatyczny reset zaprogramowany w module nie pomaga. ## Błędy (?) znalezione przy okazji Być może jest to jakiś błąd jądra, bo `cp /proc/vmcore /mnt/dane/krasze/alx-rmmod` nie działa, read zawodzi z ENOMEM (dziwne, bo czyta tylko po 4KiB na raz do pliku). Napisałem na szybko program, który robi `mem = mmap(NULL, size, PROT_READ, MAP_SHARED, vmcore_fd, 0)` i `write(STDOUT_FILENO, mem, size)`, zadziałał. Inny błąd, który znalazłem (akurat robiąc CTFa i próbując uruchomić Netscape 0.9 z 1994), to że moduł `ia32_aout` mapuje kod binarek ZMAGIC (format a.out) bez prawa do wykonania. Jak znajdę, o co chodzi w którejś z tych spraw, i naprawię, to może zgłoszę pierwszego patcha do jądra linuksa :)