A l'assaut du Kernel Linux - Anatomie d'un driver
Si vous avez déjà travaillé dans une entreprise qui gère son infrastructure “en propre”, vous avez forcément entendu cette petite phrase : “de toute façon, c’est toujours la faute du réseau”. Et il est vrai que le réseau, c’est assez souvent cryptique (du moins, pour le commun des mortels dont je fais partie). Ajoutez à cela les imprimantes et ces !#[@) de drivers de cartes graphique Nvidia, et vous avez le triangle des bermudes de l’IT : ça fonctionne donc surtout, on touche pas.
Sauf que, parfois, il faut y toucher. Et cette fois-ci, notre aventure s’est terminée dans les confins du kernel Linux. Pourtant, tout commençait bien : j’avais pris mon café matinal, Spotify me diffusait du Black Room Orchestra dans les oreilles, et je codais joyeusement un frontend en Svelte. Quand soudain, un manager débarque avec une énigme : “Sauriez-vous, mesdames et messieurs, à tout hasard, pourquoi une carte réseau ne serait pas détectée sur une installation toute propre de Debian ?”. Connaissant les joies des drivers non-free de Debian, Louis, Ju (mes collègues et co-victimes dans cette affaire) et moi-même émettons quelques hypothèses pour un quickfix vite fait bien fait. Sauf que non, pas de chance, les gens qui installent les machines victimes ont déjà tout tenté. Les cartes sont apparemment des modèles assez ésothériques, coûtent cher, et surtout, l’entreprise en a acheté 16 ; il s’agirait donc de les faire fonctionner.
Forcément, ça intrigue tout le monde au bureau. Nous demandons donc s’il serait possible, à tout hasard, d’avoir sous les mains un de ces serveurs, pour pouvoir bidouiller un peu dessus par nous-même. Demande exaucée, nous mettons nos mains de devs sur un très joli HP ProLiant DL380 Gen 10, équipé d’une belle baie de disque, 700Go de RAM et ces fameuses cartes réseaux qui refusent de monter. Après les formalités d’usage (vérifier que les ports PCI sont activés dans l’UEFI, ce genre de joyeusetés), nous commençons à trafiquer la pauvre Linux GNU/Debian 12 fraîchement installée pour essayer de comprendre ce qu’il se passe. Et, en effet, en listant nos interfaces réseau (ip l
), nous constatons que les cartes réseaux sont totalement absentes !
Cependant, nous avons une première bonne nouvelle : les cartes ne sont pas factices ou physiquement mortes : dmesg
(l’utilitaire permettant de voir ce qu’il s’est passé sur le système sur un plan “bas-niveau” depuis son démarrage) nous indique que ces cartes ont bien tentées de s’inscrire, mais qu’elles ont échouées. Le tout, avec un message assez cryptique :
[ 9.526144] QLogic FastLinQ 4xxxx Core Module qed
[ 9.531195] qede init: QLogic FastLinQ 4xxxx Ethernet Driver qede
[ 9.583755] [qed_mcp_nvm_info_populate:3374()]Failed getting number of images
[ 9.583758] [qed_hw_prepare_single:4722()]Failed to populate nvm info shadow
[ 9.583761] [qed_probe:513()]hw prepare failed
Ce message nous apprend cependant plusieurs choses intéressantes :
- Premièrement, des informations sur la carte : Il s’agit de cartes HPE QLogic FastLinQ 45000, permettant d’aggréger 4 cartes réseaux de 25Gbps en une seule sortie optique de 100Gbps (note : le modèle exact de la carte a été connu en regroupant d’autres informations, notamment côté UEFI) ;
- Le driver qui s’occupe de cette carte est le driver qed ainsi que qede ;
- Et que le driver se vautre à l’initialisation de la carte.
Ce genre de cartes étant peu communes dans la nature, nous nous disons qu’il doit exister des drivers propriétaire directement fournis par HPE, et que qed / qede tentent simplement de s’activer sans succès en guise de fallback.
Nous allons alors consacrer nos recherches sur deux pistes :
- Trouver des pilotes propriétaire ;
- Tenter faire fonctionner cette carte nativement avec un autre OS.
Notre première piste va s’avérer être intéressante mais sans succès : HPE propose bien des drivers spécifiques à cette carte, mais uniquement pour des RedHat Enterprise Linux (RHEL) 8, et du SUSE 15. De plus, ces drivers semblent être en réalité de simples repackaging de qed, qede, qedr, qedf et qedi ; rien de bien spécifique donc. Par ailleurs, ces deux distributions ont la particularité de fonctionner avec de vieilles versions du kernel (5.x). N’ayant pas à disposition de licence RHEL ou SUSE, et n’ayant aucune envie de perdre du temps à créer un compte pour tenter d’obtenir une clef de développement, nous écartons cette piste.
Par ailleurs, nous constatons qu’HPE n’a pas sorti de driver pour RHEL 9, et que la dernière release date de 2022. Les cartes n’étant pas très vieilles, il semblerait étonnant que le support ait été arrêté de manière aussi précoce ; aussi nous penchons pour une intégration du driver dans le kernel Linux de manière native. Par ailleurs, la société qui fabrique réellement ces cartes, Marvell (HPE n’est en réalité qu’un revendeur OEM), n’a pas non plus publié de nouveaux drivers.
La carte n’étant pas officiellement supportée par HPE sous Debian (seuls RHEL et SUSE sont mentionnés sur le site), nous nous mettons à tenter la carte sous de nombreuses distributions :
- Ubuntu 24 LTS pour le côté “Canonical ajoute plein de non-free partout, ça peut marcher” => Échec
- CentOS Stream pour le côté “C’est juste une RHEL rebadgée et pas stable” => Échec
- Alpine Linux pour le côté “Quitte à tester la terre entière, allons y” => Échec
Nous nous mettons alors à regarder la documentation des drivers mentionnés ci-dessus. On y apprend notamment l’intérêt de chacun d’entre eux :
- qed contrôle directement le firmware, les interruptions système, etc ;
- qede contrôle le hardware et la gestion des paquets qui transitent (réseau niveau L2) ;
- qedr contrôle la convergence des 4 cartes Ethernet incluses dans notre carte PCI en un seul gros port optique (infiniband) ;
- qedi permet de gérer iSCSI (dans notre cas, aucun usage) ;
- qedf permet de gérer FCoE (idem, on s’en fiche).
Et vu le message d’erreur, c’est qed qui a du mal. A ce moment-là, on séche, au point de vouloir abandonner : aucune distribution ne fonctionne, même celles proches des recommandations officielles d’HPE. Nous retournons alors à nos sujets habituels, en attendant que l’un de nous trouve un coup d’éclat pour nous débloquer. La nuit passe, et, le lendemain matin, l’un de nous propose une idée totalement débile : “Et si on installe une Ubuntu 18, ça fait quoi ?”. On ne s’attendait à rien, très honnêtement, mais perdu pour perdu…
On installe sans trop y croire, et là… Ça fonctionne. Les cartes montent, les interfaces s’affichent, et le port détecte même que nous lui avons branché une fibre. 🤯
root@ubuntu-18-lts:~# ip a
[...]
6: ens2f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
7: ens2f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
8: ens2f2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
9: ens2f3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
10: ens5f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
11: ens5f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
12: ens5f2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
13: ens5f3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
Et là, on reste un peu sur le cul : les cartes fonctionnent sans aucun driver propriétaire. Plug’n’play, comme à la maison ! Là, on commence à avoir un doute : Et si, par erreur, la rétro-compatibilité du matériel avait été cassée dans le kernel ? En même temps, vu la tronche de la carte, difficile de pouvoir leur jeter la faute.
Comme nous sommes en train de faire basiquement n’importe quoi, nous récupérons la version du kernel bundlé sur notre Ubuntu 18 (un kernel 4.15, EOL depuis avril 2018 🥰), et on se dit que ce serait quand même vachement drôle de rallumer le serveur sous Debian 12, avec le kernel de notre Ubuntu 18.
Et là… Ça fonctionne. Notre Linux GNU/Debian 12 avec un kernel 4.15 arrive bien à détecter les cartes, et les faire monter. On retente la même opération avec un kernel 4.15 recompilé directement depuis les sources (on sait jamais, si Ubuntu avait modifié le kernel…), et même constat, ça fonctionne.
Donc, ce n’est pas une joyeuseté de l’OS ou une quelconque magie obscure ; c’est bel et bien le kernel ! A ce moment-là, l’ambiance est partagé entre la joie d’avoir fait fonctionner un truc qui n’avait jamais marché avant, et l’incompréhension totale de ce qu’il se passe. Et on ne comprend rien à juste titre, puisqu’un mantra du kernel Linux c’est de ne jamais supprimer de la compatibilité (c’est arrivé quelques fois sur du matériel très, très, TRÈS vieux, mais nous, nous sommes sur des cartes qui n’ont même pas dix ans).
Convaincus d’avoir trouvé le pot aux roses, on se lance dans un grand jeu, intitulé “Recompilons toutes les versions LTS du kernel jusqu’à trouver où ça pète !”. L’objectif, c’est de réussir à trouver le changement précis qui a cassé la carte, pour pouvoir trouver une solution et la proposer à la communauté. Et le premier saut depuis la version 4.15 du kernel, c’est la 4.19 (publié en 2018, EOL en 2029). On recompile en 4.19 et… paf, plus de cartes ! Cela veut dire que le problème est contenu dans quatre petites versions du kernel… Cela veut aussi dire qu’à quelques mois près, une Ubuntu 18 n’aurait pas démarrée, et que nous aurions sûrement abandonné ! Parfois, la chance joue en notre faveur.
Nous recompilons alors le kernel en version 4.17 et… toujours pas de cartes ! On recompile en 4.16 et ce coup-là, tout fonctionne. La modification fautive est donc contenue entre la version 4.16 et la version 4.17, dans le driver qed. Fort de cette information, nous commençons à inspecter le code source du driver et des commits effectués entre les deux versions, et l’un d’entre eux attire notre regard : le commit qui rajoute la fonction qed_mcp_nvm_info_populate
, précédemment lue dans notre message d’erreur ; à savoir le commit 43645ce. Commence alors un travail d’analyse et de compréhension du code conduisant à cette erreur.
Notre aventure commence donc dans la fonction qed_mcp_nvm_info_populate
, localisée dans le fichier drivers/net/ethernet/qlogic/qed/qed_mcp.c
, qui est la fonction qui soulève l’erreur dans notre driver; et, grâce au log [qed_mcp_nvm_info_populate:3374()]Failed getting number of images
, nous arrivons à voir le bout de code précis :
/* Acquire from MFW the amount of available images */
nvm_info.num_images = 0;
rc = qed_mcp_bist_nvm_get_num_images(p_hwfn, p_ptt, &nvm_info.num_images);
if (rc == -EOPNOTSUPP) {
DP_INFO(p_hwfn, "DRV_MSG_CODE_BIST_TEST is not supported\n");
goto out;
} else if (rc || !nvm_info.num_images) {
DP_ERR(p_hwfn, "Failed getting number of images\n");
goto err0;
}
// [...]
out:
/* Update hwfn's nvm_info */
// [...]
return 0;
err0:
qed_ptt_release(p_hwfn, p_ptt);
return rc;
Ce morceau de code permet de récupérer des informations pour peupler la variable nvm_info.num_images
en faisant appel à la fonction qed_mcp_bist_nvm_get_num_images
. Si cette fonction retourne EOPNOTSUPP
(le code d’erreur standard pour indiquer que l’opération demandée n’est pas supportée), la fonction se termine normalement en sortant via out
. En cas d’une autre erreur, la fonction loggue Failed getting number of images
et retourne une erreur via la sortie err0
(Note : ici, les erreurs sont gérées par le retour de la fonction, sous la forme d’un code d’erreur standardisé UNIX (errno) : 0 signifie que tout s’est bien déroulé, les autres codes ont une signification propre).
Nous décidons donc d’inspecter qed_mcp_bist_nvm_get_num_images
:
int qed_mcp_bist_nvm_get_num_images(struct qed_hwfn *p_hwfn, struct qed_ptt *p_ptt, u32 *num_images) {
u32 drv_mb_param = 0, rsp;
int rc = 0;
drv_mb_param = (DRV_MB_PARAM_BIST_NVM_TEST_NUM_IMAGES << DRV_MB_PARAM_BIST_TEST_INDEX_SHIFT);
rc = qed_mcp_cmd(p_hwfn, p_ptt, DRV_MSG_CODE_BIST_TEST, drv_mb_param, &rsp, num_images);
if (rc)
return rc;
if (((rsp & FW_MSG_CODE_MASK) != FW_MSG_CODE_OK))
rc = -EINVAL;
return rc;
}
Ces quelques lignes de code sont très intéressantes, car elles permettent d’aller récupérer les informations auprès du matériel grâce à l’appel à la fonction qed_mcp_cmd
, et gère le code retour grâce à une combinaison logique avec un masque. Si la combinaison du code et du masque n’est pas égal à “OK”, alors on retourne une erreur “EINVAL”, et c’est cela qui fait crasher notre driver ! On arrive donc à la partie à corriger.
On recompile le code en ajoutant plusieurs lignes de log, afin d’afficher notamment la valeur de rsp & FW_MSG_CODE_MASK
; qui nous indique 0x00000000
.
En regardant plus en détail notre masque, on découvre dans le code que FW_MSG_CODE_MASK
correspond à 0xffff0000
; et FW_MSG_CODE_OK vaut 0x00160000
. Avec un peu d’opérations logiques et de recherche, on découvre, notamment grâce au fichier qed_mfw_hsi.h
qu’une opération non-supportée correspond au code… 0x00000000
! La carte n’est donc tout simplement pas capable de retourner les informations demandées, et le driver s’arrête au lieu de poursuivre sans cela.
Et, souvenez-vous, dans qed_mcp_nvm_info_populate
, nous avions un bout de code qui vérifiait si l’opération était supportée :
if (rc == -EOPNOTSUPP) {
DP_INFO(p_hwfn, "DRV_MSG_CODE_BIST_TEST is not supported\n");
goto out;
}
Sauf que EOPNOTSUPP
n’était jamais retourné par les fonctions sous-jacentes ! On rajoute donc un simple check dans qed_mcp_bist_nvm_get_num_images
:
int qed_mcp_bist_nvm_get_num_images(struct qed_hwfn *p_hwfn, struct qed_ptt *p_ptt, u32 *num_images) {
u32 drv_mb_param = 0, rsp;
int rc = 0;
drv_mb_param = (DRV_MB_PARAM_BIST_NVM_TEST_NUM_IMAGES << DRV_MB_PARAM_BIST_TEST_INDEX_SHIFT);
rc = qed_mcp_cmd(p_hwfn, p_ptt, DRV_MSG_CODE_BIST_TEST, drv_mb_param, &rsp, num_images);
if (rc)
return rc;
- if (((rsp & FW_MSG_CODE_MASK) != FW_MSG_CODE_OK))
+ if (((rsp & FW_MSG_CODE_MASK) == FW_MSG_CODE_UNSUPPORTED))
+ rc = -EOPNOTSUPP;
+ else if (((rsp & FW_MSG_CODE_MASK) != FW_MSG_CODE_OK))
rc = -EINVAL;
return rc;
}
On recompile en version 6.1, on relance notre Debian avec le kernel et… Tada, ça fonctionne ! On package alors le driver sous forme de module pour le mettre à disposition, et surtout, on propose un patch dans le kernel.
Rajout du 03/12/2024 : Nous sommes dans le Kernel ! 🎉
Par acquis de conscience, on branche notre superbe carte à un switch capable de comprendre ce type de carte, et tout va bien !
6: ens2f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
7: ens2f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
8: ens2f2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
9: ens2f3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
10: ens5f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state UP [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet6 fe80::1602:ecff:fec9:7020/64 scope link
valid_lft forever preferred_lft forever
11: ens5f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state UP [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet6 fe80::1602:ecff:fec9:7021/64 scope link
valid_lft forever preferred_lft forever
12: ens5f2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state UP [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet6 fe80::1602:ecff:fec9:7022/64 scope link
valid_lft forever preferred_lft forever
13: ens5f3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state UP [...]
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet6 fe80::1602:ecff:fec9:7023/64 scope link
valid_lft forever preferred_lft forever
Et nous voici à la fin de ce récit, de cette plongée dans les entrailles du Kernel Linux. Un énorme merci à Louis & Ju pour m’avoir accompagné sur cette aventure bien rigolote, et à bientôt !