diff --git a/AGENTS.md b/AGENTS.md index db7ab54..db0e56c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -968,6 +968,34 @@ aucune, la décision est une intention, et elle se périmera à la vitesse du co > **ce que ce contrôle aurait attrapé s'il avait échoué** — s'il n'y a pas de réponse, il ne > contrôle rien. +> ### Cas d'espèce, et il mord fort : l'arithmétique de dates (2026-09-05) +> +> **Entre deux instants du MÊME fuseau, la soustraction rend la durée d'HORLOGE MURALE, jamais la +> durée réelle.** Conséquence directe : un contrôle qui cherche les journées de 23 ou 25 heures +> avec cette arithmétique rendra **24 h partout**, et conclura qu'elles n'existent pas. +> +> **Trois de mes propres contrôles s'y sont trompés dans le même lot** — un balayage annuel qui +> n'a rien trouvé, puis deux calculs rendant 24 h sur les deux dates de transition — et cela sur +> la question exacte que le champ `periodStartedAt` existe pour régler. Le contrôle qui prouve +> passe par les **époques**, jamais par une soustraction de dates. +> +> **La parade générale, et c'est elle qui compte** : *l'attendu d'un test ne se calcule pas avec +> les primitives du code testé.* Les époques de référence du test de frontière viennent d'un +> **oracle calculé hors de Qt**. Les re-dériver avec `QDateTime` aurait refermé l'identité sur +> elle-même — exactement la faute que la règle ci-dessus traque, retrouvée **un cran plus bas** : +> non plus dans l'assertion, mais dans la fabrication de sa valeur attendue. +> +> Deux instruments m'ont menti dans le même lot pour la même raison de forme — répondre « normal » +> parce qu'ils ne mesurent pas ce qu'on croit : +> +> - `readelf -n | grep "Build-ID:"` rend vide, parce que `readelf` écrit **`Build ID:`** avec une +> espace. Conclusion tirée sur le moment : « les symboles ne correspondent pas. » Ils +> correspondaient. +> - la soustraction de dates ci-dessus. +> +> Dans les deux cas la sortie était **plausible** — un vide, un 24. Un instrument qui rend une +> valeur crédible ne signale jamais qu'il a mesuré autre chose. + > ### Une conclusion JUSTE posée sur un raisonnement FAUX survit jusqu'à ce qu'on bâtisse dessus > > Deux occurrences, de deux côtés de la frontière : `allocatedW` côté app, la marge par phase côté diff --git a/docs/upstream/RAPPORT_nymea_logger_sigsegv.md b/docs/upstream/RAPPORT_nymea_logger_sigsegv.md index 6566eee..b66d3ed 100644 --- a/docs/upstream/RAPPORT_nymea_logger_sigsegv.md +++ b/docs/upstream/RAPPORT_nymea_logger_sigsegv.md @@ -1,87 +1,143 @@ -# Rapport amont — SIGSEGV dans `Logger::log()` (libnymea 1.15.2) +# SIGSEGV dans `Logger::log` — déréférencement d'un journaliseur ABSENT -**À remonter à nymea.** Rédigé le 2026-08-31 ; le vidage est conservé (voir « Pièces »). +**À remonter à nymea.** Rédigé le 2026-08-31, **symbolisé et conclu le 2026-09-05**. +Le vidage est conservé (voir « Pièces »). -> **Pourquoi ce rapport plutôt qu'une ligne de changelog.** Le plantage n'a aucune trame ETM dans -> sa pile — il est donc « hors de notre lot ». Mais *hors de notre lot* ne veut pas dire *sans -> conséquence* : un plantage dans le moteur de journalisation, c'est **`nymead` qui tombe chez un -> client**, et un HEMS qui cesse de piloter. Une box qui ne pilote plus ne se signale pas d'elle-même. +> **L'hypothèse initiale — durée de vie du `Logger` vis-à-vis des événements postés — est +> TOMBÉE.** Ce n'est pas un usage après libération. C'est un **pointeur nul déréférencé**, et la +> garde qui devait l'empêcher interroge un ensemble différent de celui qu'elle protège. Le +> correctif est d'une ligne. -## Symptôme +## Conclusion, en une phrase -`SIGSEGV` dans `Logger::log(const QList&, const QMap&)`, atteint par -un **événement posté** (`sendPostedEvents` → `QObject::event`) depuis `libnymea-core`. +`ThingManagerImplementation::executeAction()` connecte une lambda à `ThingActionInfo::finished` +qui écrit un journal d'action via `m_actionLoggers.value(clé)->log(...)`. **`QHash::value()` rend +`nullptr` sur une clé absente**, et le résultat est déréférencé sans contrôle. La garde utilisée — +`thing->loggedActionTypeIds().contains(actionType.id())` — porte sur **la liste du Thing**, pas sur +**la table des journaliseurs** ; les deux divergent au retrait d'un Thing, et c'est là que ça casse. -``` -#0 Logger::log(QList const&, QMap const&) libnymea.so.1 + 0x14ea24 -#1 ?? libnymea-core.so.1 + 0x1edae6 -#2 ?? libQt6Core.so.6 + 0x1e4403 -#3 QObject::event(QEvent*) libQt6Core.so.6 -#4 QCoreApplication::notifyInternal2(QObject*, QEvent*) libQt6Core.so.6 -#5 QCoreApplicationPrivate::sendPostedEvents(...) libQt6Core.so.6 -#6 ?? libQt6Core.so.6 + 0x3a8c57 -#7 g_main_context_iteration libglib-2.0.so.0 +## Ce qui est ÉTABLI + +Symbolisé avec `libnymea1-dbgsym` et `libnymea-core-dbgsym` en `1.15.2+202606191336~trixie1` — la +version exacte du plantage, identifiants de build vérifiés (`libnymea.so.1` : +`3696e684…`, `libnymea-core.so.1` : `b50a1ae5…`). + +| trame | résolution | +|---|---| +| `#0` `libnymea.so.1 + 0x14ea24` | `Logger::log(QStringList const&, QVariantMap const&)` — `libnymea/logging/logger.cpp:55` | +| `#1` `libnymea-core.so.1 + 0x1edae6` | `ThingManagerImplementation::executeAction(Action const&)::{lambda()#1}::operator()() const` | + +**Et le registre tranche la nature du défaut** : `rdi = 0x0` au moment du signal. Dans l'ABI SysV +AMD64, `rdi` porte le premier argument, donc **`this`** pour une méthode non statique. +**`Logger::log` a été appelée sur un pointeur nul.** Un usage après libération donnerait une +adresse quelconque, pas zéro. + +Le corps de `Logger::log` tient en une instruction — `m_engine->logEvent(this, tags, values)` — +donc la lecture de `m_engine` à l'offset d'un `this` nul produit le SIGSEGV. + +## Le mécanisme + +```cpp +// thingmanagerimplementation.cpp — executeAction() +connect(info, &ThingActionInfo::finished, this, [=](){ + if (thing->loggedActionTypeIds().contains(actionType.id())) { // ← garde + ... + m_actionLoggers.value(thing->id().toString() + "-" + actionType.name())->log({}, {...}); + } // ↑ rend nullptr si la clé est absente +}); ``` -Un seul fil actif ; les cinq autres sont en attente. Le processus tenait ouverte la base -`/tmp/nymea-test/energylogs.sqlite` (moteur de journalisation SQLite). - -## Reproductibilité — FAIBLE, et c'est le point - -Observé **une fois** sur une exécution de suite de tests en tâche de fond. **Non reproduit** en -trois séquences identiques ensuite, ni en exécution isolée (48/48, code de retour 0). - -C'est précisément ce qui rend le rapport utile : à cette fréquence, le réflexe est de relancer -jusqu'à ce que ça passe et de conclure à une « fragilité de test ». Le vidage a été récupéré au -lieu d'être ignoré, et il montre que le plantage n'est **pas** dans le code de test. - -## Hypothèse — à confirmer par quelqu'un qui a les symboles - -L'enchaînement — événement **posté**, délivré depuis la boucle glib, sur un objet du moteur de -journalisation, en fin de vie de processus — a la forme d'un **usage après libération** : une -invocation en file d'attente délivrée à un `Logger` déjà détruit pendant l'arrêt. - -**C'est une hypothèse, pas une conclusion.** La trame `libnymea-core.so.1 + 0x1edae6` n'a pas -encore été symbolisée. Elle peut l'être, et **sans aucun risque** — vérifié le 2026-09-01 : - -```bash -V=1.15.2+202606191336~trixie1 # la version EXACTE qui a planté -sudo apt-get install libnymea1-dbgsym=$V libnymea-core-dbgsym=$V -gdb -batch -ex 'bt full' +```cpp +void ThingManagerImplementation::unregisterActionLogger(Thing *thing, const ActionTypeId &id) { + ... + m_actionLoggers.remove(name); // la TABLE est vidée +} ``` -Trois points qui rendent l'opération sûre, et qui méritent d'être vérifiés avant de la lancer : +Au retrait d'un Thing, la boucle `foreach (actionTypeId : thing->loggedActionTypeIds()) +unregisterActionLogger(...)` vide la table. **La liste `loggedActionTypeIds()` du Thing, elle, +n'est pas touchée** : c'est une propriété du Thing, pas de la table. -1. **La version exacte est encore au dépôt.** `libnymea1-dbgsym` et `libnymea-core-dbgsym` - existent en `1.15.2+202606191336~trixie1`. C'est indispensable : des symboles d'une autre - version ne résolvent pas un offset — ils le résolvent **faux**, ce qui est pire. -2. **L'épinglage évite la cascade.** Sans lui, `apt` prendrait la 1.16.0 et **mettrait à jour - toute la pile de développement** (`nymea-sdk`, `nymea-dev-tools`, `libnymea-core-dev`…), - changeant l'environnement de compilation. Avec les versions épinglées, l'installation - n'ajoute que les deux paquets de symboles : rien d'autre n'est touché. -3. **Le banc ne peut pas être affecté.** Ces paquets sont `Section: debug`, n'installent que - `/usr/lib/debug`, et sont **amd64** — la machine de développement. Le banc `.75` est arm64. +La connexion est établie avec **`this` (le ThingManager) pour contexte**, pas avec le Thing ni +avec l'`info`. Un `finished` déjà posté survit donc au retrait du Thing. Quand la lambda +s'exécute : -Si l'hypothèse tient, elle a une conséquence de production : l'arrêt d'un `nymead` chargé peut -laisser des écritures de journal en file, et le risque n'est pas limité aux tests. +1. la garde consulte `loggedActionTypeIds()` → **vraie**, la liste est intacte ; +2. `m_actionLoggers.value(clé)` → **`nullptr`**, l'entrée a été retirée ; +3. `nullptr->log(...)` → SIGSEGV. + +**La garde et la table ne parlent pas du même objet.** C'est ce qui rend le défaut invisible à la +relecture : le code *a* une garde, elle *a* l'air de protéger l'accès qui suit, et elle n'a aucun +rapport avec lui. + +## Pourquoi la reproductibilité est FAIBLE, et pourquoi ce n'est pas rassurant + +Observé **une fois** sur une exécution de suite de tests en tâche de fond ; non reproduit en trois +séquences identiques ni en exécution isolée (48/48, code 0). La fenêtre exige qu'une action soit +**encore en vol** au moment où son Thing est retiré — ce qu'un démontage de suite de tests produit +naturellement, et qu'une exécution tranquille ne produit presque jamais. + +**Ce n'est pas limité aux tests.** Toute suppression de Thing pendant qu'une action est en cours +ouvre la même fenêtre, sur une installation de production. Une action longue — une borne de +recharge qui négocie, un appareil Modbus lent — l'élargit. + +## Correctif proposé + +Le minimum, une ligne, sans changement de conception : + +```cpp +Logger *logger = m_actionLoggers.value(thing->id().toString() + "-" + actionType.name()); +if (logger) + logger->log({}, {...}); +``` + +Mieux, et c'est la vraie leçon : **faire porter la garde sur la table qu'on va indexer** +(`m_actionLoggers.contains(clé)`) plutôt que sur une liste qui lui ressemble. Les mêmes deux +lignes existent pour `m_stateLoggers` et `m_eventLoggers` — à vérifier de la même façon. + +> Le `Logger` lui-même n'est pas en cause : sa durée de vie est correcte. C'est son ABSENCE de la +> table qui n'est pas testée. ## Environnement | | | |---|---| -| `libnymea1` | `1.15.2+202606191336~trixie1` | +| `libnymea1` / `libnymea-core` | `1.15.2+202606191336~trixie1` | | Qt | `6.8.2+dfsg-9+deb13u2` | | Système | Debian 13 (trixie), x86_64 | | Binaire | suite de tests `charging` du plugin énergie ETM | +## Réserve de lecture + +La symbolisation vient des **symboles 1.15.2**, donc les noms et les lignes sont ceux de la +version qui a planté. En revanche, **le code cité ci-dessus est lu dans l'arbre 1.16.0** (seule +source disponible localement). Les deux symboles résolus s'y retrouvent à l'identique, et +`Logger::log` y fait toujours trois lignes — mais la formulation exacte du correctif est à +vérifier contre 1.15.2 par quelqu'un qui a la branche. + ## Pièces -- Vidage conservé : `coredumpctl` du 2026-08-31 07:06:01 CEST, PID 115071, 2,7 Mio compressés. -- Commande de symbolisation utile côté nymea : - `gdb -batch -ex 'bt full' ` avec `libnymea-dbgsym` installé. +- Vidage : `coredumpctl` du 2026-08-31 07:06:01 CEST, PID 115071, 2,7 Mio compressés (77 Mio + décompressés), conservé. +- Symbolisation reproductible **sans installer quoi que ce soit**, et sans privilèges : + +```bash +V=1.15.2+202606191336~trixie1 +apt-get download libnymea1-dbgsym=$V libnymea-core-dbgsym=$V +for d in *.deb; do dpkg-deb -x "$d" root/; done +addr2line -f -C -e root/usr/lib/debug/.build-id/36/96e684….debug 0x14ea24 # → Logger::log +addr2line -f -C -e root/usr/lib/debug/.build-id/b5/0a1ae5….debug 0x1edae6 # → la lambda +gdb -batch -q --core= -ex "info all-registers rdi" # → rdi = 0x0 +``` + +> `gdb` ne peut PAS produire de pile complète ici : le binaire de test et le plugin ont été +> reconstruits depuis le plantage, donc leurs identifiants de build ne correspondent plus au +> vidage. La résolution **par offset** dans les bibliothèques, elle, reste valide — leurs +> identifiants correspondent, et c'est tout ce dont la conclusion a besoin. ## Ce qui est demandé -1. Symboliser la trame `libnymea-core.so.1 + 0x1edae6` — elle nomme l'appelant, et l'hypothèse - tombe ou se confirme immédiatement. -2. Vérifier la durée de vie du `Logger` vis-à-vis des événements postés à l'arrêt. +1. Confirmer le correctif contre la branche 1.15.x, et vérifier les deux tables jumelles + (`m_stateLoggers`, `m_eventLoggers`). +2. Décider si la connexion doit prendre `thing` ou `info` pour contexte plutôt que `this` — ce + qui fermerait la fenêtre au lieu de la rendre inoffensive.