The AMQP 1.0 protocol implementation em sgcWebSockets has undergone um comprehensive review against the OASIS AMQP 1.0 specification. Este artigo documenta o 30 fixes applied across 8 source files, covering critical bugs, memory leaks, specification compliance, state machine correctness, heartbeat handling, e thread safety improvements.
Sumário
- Overview
- Critical Bug Fixes
- Memory Leak Fixes
- Specification Compliance Fixes
- State Machine & Conexão Fixes
- Heartbeat & Idle Timeout Fixes
- Thread Safety Fixes
- Files Modified
1. Overview
A total de 30 fixes were applied across 6 source files e 2 interface files no AMQP 1.0 implementation. The fixes are categorized como segue:
| Category | Count | Severity |
|---|---|---|
| Critical Bug Fixes | 6 | Critical |
| Memory Leak Fixes | 4 | Critical |
| Specification Compliance | 10 | High |
| State Machine & Conexão | 5 | High |
| Heartbeat & Idle Timeout | 3 | High |
| Thread Safety | 2 | High |
2. Critical Bug Fixes
These fixes address bugs that would cause immediate runtime failures, protocol corruption, ou incorrect behavior during AMQP 1.0 communication.
2.1 Missing raise Keyword em Exception
File: sgcAMQP1_Classes.pas
An exception object was created but nunca raised when um invalid frame type was encountered. The exception was
silently discarded, allowing corrupt frames para be processed sem detection.
Before (Broken)
TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
After (Fixed)
raise TsgcAMQP1Exception.CreateFmt(S_AMQP1_INVALID_FRAME_TYPE, [vByte]);
2.2 WriteMap Map32 Missing Data e Wrong Size
File: sgcAMQP1_Classes.pas
The Map32 encoding path was missing o WriteBytes call para o
actual map data, e o size field used um incorrect offset. Map32 uses um 4-byte count field (unlike Map8 which uses
1 byte), so o size must include 4 extra bytes.
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 Inverted ContainsError Logic
File: sgcAMQP1_Frames.pas
The ContainsError method em both
TsgcAMQP1FrameRejected and
TsgcAMQP1DescribedListError
returned True when there was no error and
False when there was um erro. This caused
error information para be silently dropped e null bytes para be written when actual errors should foram serialized.
The DoWrite and
DoWriteError branches were also swapped para match o corrected logic.
Before (Broken)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := True // Wrong: True when no error
else
Result := (Error.Condition = '') and (Error.Description = '') and
(Error.Info = ''); // Wrong: True when all empty
end;
After (Fixed)
function TsgcAMQP1FrameRejected.ContainsError: Boolean;
begin
if not Assigned(FError) then
Result := False // Correct: False when no error
else
Result := (Error.Condition <> '') or (Error.Description <> '') or
(Error.Info <> ''); // Correct: True when any field set
end;
2.4 SASL PLAIN Null Byte Separators
File: sgcAMQP1_Frames.pas
The SASL PLAIN mechanism requer o formato \0username\0password com null byte
($00) separators. The implementation was not
initializing o byte array para zero, so separator positions contained garbage data. Autenticação would fail against
any standards-compliant AMQP 1.0 broker.
After (Fixed)
SetLength(FInitialResponse, 2 + Length(oUser) + Length(oPassword));
FillChar(FInitialResponse[0], Length(FInitialResponse), 0); // Zero-fill for null separators
2.5 Missing inherited Create em TsgcAMQP1Message
File: sgcAMQP1_Message.pas
The parameterized constructor de TsgcAMQP1Message did not call
inherited Create, meaning o base class was nunca initialized.
This would cause access violations ou corrupt state when using o convenience constructor.
After (Fixed)
constructor TsgcAMQP1Message.Create(const aValue: string);
begin
inherited Create;
ApplicationData.ValueType := amqp1adtAmqpValue;
ApplicationData.AMQPValue.Value := aValue;
end;
2.6 Missing Semicolon em AmqpValue.DoRead
File: sgcAMQP1_Frames.pas
A missing semicolon em TsgcAMQP1FrameAmqpValue.DoRead
would prevent compilation.
3. Memory Leak Fixes
These fixes address object lifetime management issues that would cause memory para accumulate over time, particularly during long-running AMQP 1.0 conexões com many message exchanges.
| Fix | File | Description |
|---|---|---|
| 3.1 | sgcAMQP1_Frames.pas |
FDefaultOutcome was not freed before reassignment when reading o Source descriptor. Each time um novo default outcome was received, o previous object was leaked. |
| 3.2 | sgcAMQP1_Session.pas |
Duplicate sgcFree(FCreditConsumed) call no destructor caused um potential double-free. Removed o duplicate line. |
| 3.3 | sgcAMQP1_Session.pas |
FOutgoingDeliveries was missing do session destructor. The delivery tracking list was nunca freed when um session was destroyed. |
| 3.4 | sgcAMQP1_Message.pas |
SetMessage e SetMessageAndFreeOnDestroy did not free o previous message when FFreeMessageOnDestroy was enabled. Repeated message assignments leaked memory. |
Fix 3.1 – FDefaultOutcome Freed Before Reassignment
sgcFree(FDefaultOutcome); // Free previous instance before reassignment
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
Fix 3.4 – SetMessage Frees Old 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. Specification Compliance Fixes
These fixes correct deviations do AMQP 1.0 Transport, Types, and Messaging specifications.
4.1 Begin Frame Field 7 Read Into Wrong Property
File: sgcAMQP1_Frames.pas
Field index 7 do begin performative was being read into
DesiredCapabilities instead of
Properties. Per o spec,
begin fields are: 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 Source e Target Missing Fields em DoWrite
File: sgcAMQP1_Frames.pas
The Source descriptor's
DoWrite method was not serializing the
default-outcome,
outcomes, and
capabilities fields. The
Target descriptor was missing
capabilities. This caused o broker to
use defaults instead do negotiated values, potentially leading para incorrect delivery state handling.
4.3 AmqpSequence Read Into Wrong Property
File: sgcAMQP1_Message.pas
When reading um mensagem body, amqp-sequence data was being
read into ApplicationData.AMQPValue instead of
ApplicationData.AMQPSequence. This corrupted
a mensagem body para any message using o amqp-sequence encoding.
4.4 TransactionalState Outcome Not Written
File: sgcAMQP1_Frames.pas
The transactional-state delivery state was not
serializing its outcome field, which is required
when settling deliveries within um transaction.
4.5 Disposition Last Field Cannot Distinguish Zero From Unset
Files: sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf,
sgcAMQP1_Session.pas
The disposition performative has um optional
last field (delivery-id).
Since it is um Cardinal, o valor 0 is valid e cannot be
used como um sentinel para “not set.” A new FLastAssigned
boolean flag e SetLast setter were added to
properly track whether o field was explicitly set.
procedure TsgcAMQP1FrameDisposition.SetLast(const Value: Cardinal);
begin
FLast := Value;
FLastAssigned := True;
end;
4.6 AmqpSequence Missing Value Property e Read/Write
Files: sgcAMQP1_Frames.pas,
sgcAMQP1_Frames.intf
The TsgcAMQP1FrameAmqpSequence class had no
Value property e empty
DoRead/DoWrite
métodos. The amqp-sequence body type was
completely non-functional.
4.7 Error Info Field Read como String Instead de Map
File: sgcAMQP1_Frames.pas
The AMQP 1.0 spec defines o info field do
error type como a
map. It was being read with
ReadString instead of
ReadMap, causing parsing failures
when brokers send structured error information.
4.8 Capabilities e Locales Written como String Instead de Symbol
File: sgcAMQP1_Frames.pas
The AMQP 1.0 spec defines offered-capabilities,
desired-capabilities,
outgoing-locales, and
incoming-locales como arrays of
symbol. They were being written
with WriteString instead of
WriteSymbol no
open,
begin, and
attach performatives.
A standards-compliant broker would reject these frames como having incorrect field types.
4.9 Missing Accepted Descriptor em DefaultOutcome Handler
File: sgcAMQP1_Frames.pas
The Source descriptor's default-outcome
reader somente handled released and
rejected. The most common outcome,
accepted, was not handled.
When um broker sent accepted como o padrão outcome,
it was silently ignored.
5. State Machine & Conexão Fixes
These fixes address o AMQP 1.0 conexão state machine e frame processing logic.
| Fix | File | Description |
|---|---|---|
| 5.1 | sgcAMQP1.pas |
The amqp1csOpenReceived state transition was missing o else DoRaiseInvalidState that all other states had. Invalid transitions were silently ignored instead de raising um erro. |
| 5.2 | sgcAMQP1.pas |
Frame size validation mensagem de erro displayed RemoteMaxFrameSize but should have shown LocalMaxFrameSize, since o local limit is what was being checked. |
| 5.3 | sgcAMQP1.pas |
FLastTimeRead was initialized para 0 (Delphi epoch: 1899-12-30) instead de Now. This caused immediate false idle timeout detection em startup. |
| 5.4 | sgcAMQP1.pas |
The TBytes overload de Read did not update FLastTimeRead := Now, unlike o TMemoryStream overload. This caused inconsistent heartbeat tracking. |
| 5.5 | sgcAMQP1.pas |
Header received state transition was conditional when it should sempre trigger. The state machine must transition em every valid header exchange per o AMQP 1.0 spec. |
Fix 5.1 – OpenReceived Missing Error Branch
amqp1csOpenReceived:
begin
if aState = amqp1csOpenSent then
FConnectionState := amqp1csOpened
else
DoRaiseInvalidState; // Added: was missing
end;
6. Heartbeat & Idle Timeout Fixes
The AMQP 1.0 specification requer que when um peer advertises an
idle-timeout no
open performative, o other peer must send
heartbeat frames em half o advertised interval. These fixes ensure o heartbeat mechanism
actually functions.
| Fix | File | Description |
|---|---|---|
| 6.1 | sgcAMQP1_Client.pas |
HeartBeat was nunca enabled. Both branches do idle timeout check set HeartBeat.Enabled := False. Changed para True when IdleTimeout > 0. |
| 6.2 | sgcAMQP1_Client.pas |
Disconnect did not disable heartbeat ou set FConnected := False early enough. Reordered para prevent heartbeat firing during teardown. |
| 6.3 | sgcAMQP1.pas |
FLastTimeRead not updated no TBytes Read overload (also listed em State Machine section). |
Fix 6.1 – HeartBeat Enabled
if oOpen.IdleTimeout > 0 then
begin
HeartBeat.Interval := Trunc(oOpen.IdleTimeout / 2);
HeartBeat.Enabled := True; // Was: False (heartbeat never started)
end
else
HeartBeat.Enabled := False;
Fix 6.2 – Desconectar Reordered
procedure TsgcAMQP1_Client.Disconnect;
begin
FConnected := False; // Moved first: prevents heartbeat race
DoStopIdleTimeout;
HeartBeat.Enabled := False; // Added: stop heartbeat during teardown
Clear;
DoConnectionState(amqp1csEnd);
end;
7. Thread Safety Fixes
These fixes address race conditions em concurrent access para shared data structures.
7.1 TsgcAMQP1Deliveries.Primeiro() Bounds Check e Locking
File: sgcAMQP1_Message.pas
The First() method accessed
Items[0] sem checking if um lista was empty
and sem acquiring o thread-safe lock. In um multi-threaded environment, another thread could
remover all items entre o count check e o access, causing um index-out-of-bounds exception.
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 SetMessage Safe Object Replacement
File: sgcAMQP1_Message.pas
The SetMessage method now checks
that o novo message is different do current one before freeing, preventing use-after-free
when assigning o same message object.
8. Files Modified
| File | Fixes | Categories |
|---|---|---|
Source\sgcAMQP1_Classes.pas |
2 | Critical bugs |
Source\sgcAMQP1_Frames.pas |
16 | Critical bugs, memory leaks, spec compliance |
Interfaces\sgcAMQP1_Frames.intf |
2 | Spec compliance (Disposition LastAssigned, AmqpSequence Value) |
Source\sgcAMQP1_Message.pas |
4 | Critical bugs, memory leaks, thread safety |
Source\sgcAMQP1_Session.pas |
3 | Memory leaks, spec compliance |
Source\sgcAMQP1.pas |
5 | State machine, conexão, heartbeat |
Source\sgcAMQP1_Client.pas |
3 | Heartbeat, desconectar safety |
Total: 30 fixes across 8 files, improving protocol correctness, memory safety, e reliability do AMQP 1.0 implementation against o OASIS specification.
