docs(upstream): SIGSEGV symbolisé — l'hypothèse tombe, c'est un pointeur NUL
L'hypothèse « durée de vie du Logger vis-à-vis des événements postés » est FAUSSE. Ce n'est pas un usage après libération : c'est un déréférencement de nullptr, et la garde censée l'empêcher interroge un ensemble différent de celui qu'elle protège. Correctif d'une ligne. ÉTABLI, avec les symboles 1.15.2 (identifiants de build vérifiés) : · #0 libnymea.so.1 + 0x14ea24 → Logger::log(...), logger.cpp:55 · #1 libnymea-core.so.1+0x1edae6 → la lambda de ThingManagerImplementation:: executeAction connectée à finished · rdi = 0x0 au signal. Dans l'ABI SysV AMD64, rdi porte le premier argument, donc `this`. Logger::log a été appelée sur un pointeur NUL — un usage après libération donnerait une adresse quelconque, pas zéro. LE MÉCANISME. La lambda fait `m_actionLoggers.value(clé)->log(...)`, et QHash::value() rend nullptr sur une clé absente. Sa garde est `thing->loggedActionTypeIds().contains(...)` — la liste DU THING, pas la TABLE des journaliseurs. Au retrait d'un Thing, unregisterActionLogger vide la table pendant que la liste du Thing reste intacte. La connexion prend `this` (le ThingManager) pour contexte, donc un `finished` déjà posté survit au retrait : garde vraie, table vide, nullptr déréférencé. C'est notre défaut récurrent, dans du code amont : UN GARDE-FOU QUI INTERROGE LE MAUVAIS OBJET. Il a l'air de protéger l'accès qui suit et n'a aucun rapport avec lui — d'où son invisibilité à la relecture. Et ce n'est pas limité aux tests : toute suppression de Thing pendant qu'une action est en vol ouvre la même fenêtre en production, qu'une action longue élargit. SYMBOLISÉ SANS RIEN INSTALLER, et sans privilèges : `apt-get download` puis `dpkg-deb -x`, et `addr2line` sur les fichiers extraits. Noté dans le rapport, parce que c'est aussi la voie qui ne touche pas la machine. gdb, lui, ne peut pas produire de pile ici : le binaire de test et le plugin ont été reconstruits depuis le plantage, donc leurs identifiants ne correspondent plus au vidage. La résolution par offset dans les bibliothèques reste valide, et suffit. RÉSERVE ÉCRITE : les symboles sont ceux de 1.15.2, mais le code cité est lu dans l'arbre 1.16.0, seule source locale. Les deux symboles s'y retrouvent à l'identique ; la formulation exacte du correctif reste à vérifier sur la branche. LEÇON GRAVÉE (AGENTS.md, à côté de la règle des identités) : entre deux instants du MÊME fuseau, la soustraction rend la durée d'horloge murale, jamais la durée réelle — donc un contrôle qui cherche les journées de 23 ou 25 h avec cette arithmétique rend 24 h partout et conclut qu'elles n'existent pas. Trois de mes contrôles s'y sont trompés dans ce lot, sur la question même que le champ existe pour régler. La parade générale est la vraie leçon : l'attendu d'un test ne se calcule pas avec les primitives du code testé. L'oracle du test de frontière vient de hors Qt ; le re-dériver avec QDateTime aurait refermé l'identité sur elle-même — la faute que la règle voisine traque, retrouvée un cran plus bas, non plus dans l'assertion mais dans la fabrication de sa valeur attendue.
This commit is contained in:
parent
fb18a282e1
commit
9eef60fc35
28
AGENTS.md
28
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é
|
||||
|
||||
@ -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<QString>&, const QMap<QString, QVariant>&)`, 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<QString> const&, QMap<QString,QVariant> 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' <binaire> <core>
|
||||
```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' <binaire> <core>` 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=<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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user