L'implémentation du protocole AMQP 1.0 dans sgcWebSockets a fait l'objet d'une revue complète par rapport à la spécification OASIS AMQP 1.0. Cet article documente les 30 corrections appliquées dans 8 fichiers source, couvrant des bugs critiques, des fuites mémoire, la conformité à la spécification, la justesse de la machine à états, la gestion du heartbeat et des améliorations de la sécurité des threads.
Table des matières
- Aperçu
- Corrections de bugs critiques
- Corrections de fuites mémoire
- Corrections de conformité à la spécification
- Corrections de la machine à états et de la connexion
- Corrections du heartbeat et du timeout d'inactivité
- Corrections de la sécurité des threads
- Fichiers modifiés
1. Aperçu
Au total, 30 corrections ont été appliquées dans 6 fichiers source et 2 fichiers d'interface de l'implémentation AMQP 1.0. Les corrections sont catégorisées comme suit :
| Catégorie | Nombre | Sévérité |
|---|---|---|
| Corrections de bugs critiques | 6 | Critique |
| Corrections de fuites mémoire | 4 | Critical |
| Conformité à la spécification | 10 | Haut |
| Machine à états et connexion | 5 | High |
| Heartbeat et timeout d'inactivité | 3 | High |
| Sécurité des threads | 2 | High |
2. Corrections de bugs critiques
Ces corrections traitent des bugs causant des échecs immédiats à l'exécution, une corruption de protocole ou un comportement incorrect pendant la communication AMQP 1.0.
2.1 Mot-clé raise manquant dans une exception
Fichier : sgcAMQP1_Classes.pas
Un objet d'exception était créé mais jamais levé lorsqu'un type de trame invalide était rencontré. L'exception était
silencieusement ignorée, permettant le traitement de trames corrompues sans détection.
Avant (cassé)
TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
Après (corrigé)
raise TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
2.2 WriteMap Map32 avec données manquantes et taille incorrecte
Fichier : sgcAMQP1_Classes.pas
Le chemin d'encodage Map32 omettait l'appel à WriteBytes pour les
données réelles de la map, et le champ size utilisait un offset incorrect. Map32 utilise un champ de comptage de 4 octets (contrairement à Map8 qui utilise
1 octet), donc la taille doit inclure 4 octets supplémentaires.
Before (Broken)
else
begin
WriteUByte(Ord(amqp1DataMap32));
_WriteUInt32(vSize + 1);
_WriteUInt32(oJSON.Count * 2);
// Missing: WriteBytes(oArray.Bytes);
end;
After (Fixed)
else
begin
WriteUByte(Ord(amqp1DataMap32));
_WriteUInt32(vSize + 4);
_WriteUInt32(oJSON.Count * 2);
WriteBytes(oArray.Bytes);
end;
2.3 Logique ContainsError inversée
Fichier : sgcAMQP1_Frames.pas
La méthode ContainsError dans
TsgcAMQP1FrameRejected et
TsgcAMQP1DescribedListError
renvoyait True quand il n'y avait pas d'erreur et
False quand il y avait une erreur. Cela provoquait
la perte silencieuse des informations d'erreur et l'écriture d'octets nuls alors que les erreurs réelles auraient dû être sérialisées.
Les branches DoWrite et
DoWriteError ont également été inversées pour correspondre à la logique corrigée.
Before (Broken)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := True // Faux : True quand pas d'erreur
else
Result := (Error.Condition = '') and (Error.Description = '') and
(Error.Info = ''); // Faux : True quand tous vides
end;
After (Fixed)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := False // Correct : False quand pas d'erreur
else
Result := (Error.Condition <> '') or (Error.Description <> '') or
(Error.Info <> ''); // Correct : True quand un champ est défini
end;
2.4 Séparateurs à octets nuls de SASL PLAIN
Fichier : sgcAMQP1_Frames.pas
Le mécanisme SASL PLAIN requiert le format \0username\0password avec des séparateurs
($00) à octets nuls. L'implémentation n'initialisait pas
le tableau d'octets à zéro, donc les positions des séparateurs contenaient des données aléatoires. L'authentification échouait contre
tout broker AMQP 1.0 conforme aux standards.
After (Fixed)
SetLength(FInitialResponse, 2 + Length(oUser) + Length(oPassword));
FillChar(FInitialResponse[0], Length(FInitialResponse), 0); // Remplir de zéros pour les séparateurs nuls
2.5 inherited Create manquant dans TsgcAMQP1Message
Fichier : sgcAMQP1_Message.pas
Le constructeur paramétré de TsgcAMQP1Message n'appelait pas
inherited Create, ce qui signifie que la classe de base n'était jamais initialisée.
Cela provoquait des violations d'accès ou un état corrompu lors de l'utilisation du constructeur de commodité.
After (Fixed)
constructor TsgcAMQP1Message.Create(const aValue: string);
begin
inherited Create;
ApplicationData.ValueType := amqp1adtAmqpValue;
ApplicationData.AMQPValue.Value := aValue;
end;
2.6 Point-virgule manquant dans AmqpValue.DoRead
Fichier : sgcAMQP1_Frames.pas
Un point-virgule manquant dans TsgcAMQP1FrameAmqpValue.DoRead
empêchait la compilation.
3. Corrections de fuites mémoire
Ces corrections traitent des problèmes de gestion de durée de vie d'objets qui faisaient s'accumuler la mémoire au fil du temps, en particulier pendant des connexions AMQP 1.0 longues avec de nombreux échanges de messages.
| Correction | Fichier | Description |
|---|---|---|
| 3.1 | sgcAMQP1_Frames.pas |
FDefaultOutcome n'était pas libéré avant réaffectation lors de la lecture du descripteur Source. À chaque réception d'un nouveau default outcome, l'objet précédent était fuité. |
| 3.2 | sgcAMQP1_Session.pas |
Appel dupliqué à sgcFree(FCreditConsumed) dans le destructeur provoquant un potentiel double-free. Ligne dupliquée supprimée. |
| 3.3 | sgcAMQP1_Session.pas |
FOutgoingDeliveries manquait dans le destructeur de session. La liste de suivi des livraisons n'était jamais libérée à la destruction d'une session. |
| 3.4 | sgcAMQP1_Message.pas |
SetMessage et SetMessageAndFreeOnDestroy ne libéraient pas le message précédent quand FFreeMessageOnDestroy était activé. Les affectations répétées de message faisaient fuir la mémoire. |
Correction 3.1 – FDefaultOutcome libéré avant réaffectation
sgcFree(FDefaultOutcome); // Libérer l'instance précédente avant réaffectation
if oDescriptor.Code = amqp1dcptReleased then
FDefaultOutcome := TsgcAMQP1FrameReleased.Create
else if oDescriptor.Code = amqp1dcptAccepted then
FDefaultOutcome := TsgcAMQP1FrameAccepted.Create
else if oDescriptor.Code = amqp1dcptRejected then
FDefaultOutcome := TsgcAMQP1FrameRejected.Create
Correction 3.4 – SetMessage libère l'ancien message
procedure TsgcAMQP1Delivery.SetMessage(const aMessage: TsgcAMQP1Message);
begin
if FFreeMessageOnDestroy and Assigned(F_Message) and (F_Message <> aMessage) then
sgcFree(F_Message);
FFreeMessageOnDestroy := False;
F_Message := aMessage;
end;
4. Corrections de conformité à la spécification
Ces corrections corrigent des écarts par rapport aux spécifications AMQP 1.0 Transport, Types et Messaging.
4.1 Champ 7 de la trame Begin lu dans la mauvaise propriété
Fichier : sgcAMQP1_Frames.pas
Le champ d'index 7 de la performative begin était lu dans
DesiredCapabilities au lieu de
Properties. Selon la spécification,
les champs de begin sont : remote-channel(0), next-outgoing-id(1), incoming-window(2), outgoing-window(3), handle-max(4),
offered-capabilities(5), desired-capabilities(6), properties(7).
4.2 Champs manquants pour Source et Target dans DoWrite
Fichier : sgcAMQP1_Frames.pas
La méthode DoWrite du descripteur Source
ne sérialisait pas les champs
default-outcome,
outcomes et
capabilities. Le descripteur
Target omettait
capabilities. Cela faisait que le broker
utilisait les valeurs par défaut au lieu des valeurs négociées, pouvant entraîner une gestion incorrecte de l'état des livraisons.
4.3 AmqpSequence lu dans la mauvaise propriété
Fichier : sgcAMQP1_Message.pas
Lors de la lecture du corps du message, les données amqp-sequence étaient lues
dans ApplicationData.AMQPValue au lieu de
ApplicationData.AMQPSequence. Cela corrompait
le corps du message pour tout message utilisant l'encodage amqp-sequence.
4.4 Outcome de TransactionalState non écrit
Fichier : sgcAMQP1_Frames.pas
L'état de livraison transactional-state ne
sérialisait pas son champ outcome, requis
lors du règlement des livraisons dans une transaction.
4.5 Le champ Last de Disposition ne distingue pas zéro de non défini
Fichiers : sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf,
sgcAMQP1_Session.pas
La performative disposition a un champ optionnel
last (delivery-id).
Comme c'est un Cardinal, la valeur 0 est valide et ne peut pas être
utilisée comme sentinelle pour « non défini ». Un nouveau drapeau booléen FLastAssigned
et un setter SetLast ont été ajoutés pour
suivre correctement si le champ a été explicitement défini.
procedure TsgcAMQP1FrameDisposition.SetLast(const Value: Cardinal);
begin
FLast := Value;
FLastAssigned := True;
end;
4.6 AmqpSequence sans propriété Value ni Read/Write
Fichiers : sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf
La classe TsgcAMQP1FrameAmqpSequence n'avait pas de
propriété Value et avait des méthodes
DoRead/DoWrite
vides. Le type de corps amqp-sequence était
totalement non fonctionnel.
4.7 Le champ Info de l'erreur est lu comme String au lieu de Map
Fichier : sgcAMQP1_Frames.pas
La spécification AMQP 1.0 définit le champ info du
type error comme une
map. Il était lu avec
ReadString au lieu de
ReadMap, causant des échecs de parsing
quand les brokers envoyaient des informations d'erreur structurées.
4.8 Capabilities et Locales écrits comme String au lieu de Symbol
Fichier : sgcAMQP1_Frames.pas
La spécification AMQP 1.0 définit offered-capabilities,
desired-capabilities,
outgoing-locales et
incoming-locales comme des tableaux de
symbol. Ils étaient écrits
avec WriteString au lieu de
WriteSymbol dans les performatives
open,
begin et
attach.
Un broker conforme aux standards rejetterait ces trames car les types des champs sont incorrects.
4.9 Descripteur Accepted manquant dans le handler DefaultOutcome
Fichier : sgcAMQP1_Frames.pas
Le lecteur de default-outcome du descripteur Source
ne gérait que released et
rejected. L'outcome le plus courant,
accepted, n'était pas géré.
Quand un broker envoyait accepted comme default outcome,
il était silencieusement ignoré.
5. Corrections de la machine à états et de la connexion
Ces corrections traitent de la machine à états de la connexion AMQP 1.0 et de la logique de traitement des trames.
| Fix | File | Description |
|---|---|---|
| 5.1 | sgcAMQP1.pas |
La transition d'état amqp1csOpenReceived n'avait pas le else DoRaiseInvalidState que tous les autres états possédaient. Les transitions invalides étaient silencieusement ignorées au lieu de lever une erreur. |
| 5.2 | sgcAMQP1.pas |
Le message d'erreur de validation de taille de trame affichait RemoteMaxFrameSize mais aurait dû afficher LocalMaxFrameSize, puisque c'est la limite locale qui était vérifiée. |
| 5.3 | sgcAMQP1.pas |
FLastTimeRead était initialisé à 0 (époque Delphi : 1899-12-30) au lieu de Now. Cela provoquait une fausse détection immédiate du timeout d'inactivité au démarrage. |
| 5.4 | sgcAMQP1.pas |
La surcharge TBytes de Read ne mettait pas à jour FLastTimeRead := Now, contrairement à la surcharge TMemoryStream. Cela provoquait un suivi incohérent du heartbeat. |
| 5.5 | sgcAMQP1.pas |
La transition d'état Header reçu était conditionnelle alors qu'elle devrait toujours se déclencher. La machine à états doit transiter à chaque échange d'en-tête valide selon la spécification AMQP 1.0. |
Correction 5.1 – Branche d'erreur manquante pour OpenReceived
amqp1csOpenReceived:
begin
if aState = amqp1csOpenSent then
FConnectionState := amqp1csOpened
else
DoRaiseInvalidState; // Ajouté : était manquant
end;
6. Corrections du heartbeat et du timeout d'inactivité
La spécification AMQP 1.0 exige que lorsqu'un peer annonce un
idle-timeout dans la performative
open, l'autre peer doit envoyer
des trames heartbeat à la moitié de l'intervalle annoncé. Ces corrections garantissent que le mécanisme de heartbeat
fonctionne réellement.
| Fix | File | Description |
|---|---|---|
| 6.1 | sgcAMQP1_Client.pas |
HeartBeat n'était jamais activé. Les deux branches de la vérification du timeout d'inactivité positionnaient HeartBeat.Enabled := False. Changé en True quand IdleTimeout > 0. |
| 6.2 | sgcAMQP1_Client.pas |
Disconnect ne désactivait pas le heartbeat et ne positionnait pas FConnected := False assez tôt. Réordonné pour empêcher le heartbeat de se déclencher pendant la fermeture. |
| 6.3 | sgcAMQP1.pas |
FLastTimeRead non mis à jour dans la surcharge Read sur TBytes (également mentionné dans la section Machine à états). |
Correction 6.1 – HeartBeat activé
if oOpen.IdleTimeout > 0 then
begin
HeartBeat.Interval := Trunc(oOpen.IdleTimeout / 2);
HeartBeat.Enabled := True; // Avant : False (heartbeat jamais démarré)
end
else
HeartBeat.Enabled := False;
Correction 6.2 – Disconnect réordonné
procedure TsgcAMQP1_Client.Disconnect;
begin
FConnected := False; // Déplacé en premier : évite la condition de course du heartbeat
DoStopIdleTimeout;
HeartBeat.Enabled := False; // Ajouté : arrête le heartbeat pendant la fermeture
Clear;
DoConnectionState(amqp1csEnd);
end;
7. Corrections de la sécurité des threads
Ces corrections traitent des conditions de course dans l'accès concurrent à des structures de données partagées.
7.1 TsgcAMQP1Deliveries.First() : vérification des bornes et verrouillage
Fichier : sgcAMQP1_Message.pas
La méthode First() accédait à
Items[0] sans vérifier si la liste était vide
et sans acquérir le verrou thread-safe. Dans un environnement multi-thread, un autre thread pouvait
supprimer tous les éléments entre la vérification du count et l'accès, causant une exception d'index hors bornes.
After (Fixed)
function TsgcAMQP1Deliveries.First: TsgcAMQP1Delivery;
var
oList: TList;
begin
result := nil;
oList := LockList;
Try
if oList.Count > 0 then
result := TsgcAMQP1Delivery(oList[0]);
Finally
UnLockList;
End;
end;
7.2 Remplacement sûr d'objet par SetMessage
Fichier : sgcAMQP1_Message.pas
La méthode SetMessage vérifie maintenant
que le nouveau message est différent de l'actuel avant de le libérer, prévenant un use-after-free
lors de l'affectation du même objet message.
8. Fichiers modifiés
| File | Corrections | Catégories |
|---|---|---|
Source\sgcAMQP1_Classes.pas |
2 | Bugs critiques |
Source\sgcAMQP1_Frames.pas |
16 | Bugs critiques, fuites mémoire, conformité à la spécification |
Interfaces\sgcAMQP1_Frames.intf |
2 | Conformité à la spécification (Disposition LastAssigned, AmqpSequence Value) |
Source\sgcAMQP1_Message.pas |
4 | Bugs critiques, fuites mémoire, sécurité des threads |
Source\sgcAMQP1_Session.pas |
3 | Fuites mémoire, conformité à la spécification |
Source\sgcAMQP1.pas |
5 | Machine à états, connexion, heartbeat |
Source\sgcAMQP1_Client.pas |
3 | Heartbeat, sûreté du disconnect |
Total : 30 corrections dans 8 fichiers, améliorant la justesse du protocole, la sécurité mémoire et la fiabilité de l'implémentation AMQP 1.0 par rapport à la spécification OASIS.
