Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions agent/src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ public function run(): void
// The message length includes the 4 bytes of the header itself
$messageLength = $unpackedHeader[1];

if ($messageLength < 5) {
\call_user_func($this->onConnectionError, new \RuntimeException(
\sprintf('Invalid envelope frame length of %d bytes. The length must include the 4-byte header and at least 1 byte of payload.', $messageLength)
));

$connection->close();

return;
}

if ($messageLength - 4 > self::MAX_ENVELOPE_SIZE) {
\call_user_func($this->onConnectionError, new \RuntimeException(
\sprintf('Envelope size of %d bytes exceeds maximum allowed size of %d bytes.', $messageLength - 4, self::MAX_ENVELOPE_SIZE)
Expand Down
25 changes: 25 additions & 0 deletions agent/tests/AgentForwardingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ public function testAgentForwardsMultipleEnvelopesToUpstream(): void
$this->assertEquals(2, $serverOutput['request_count']);
}

public function testAgentRejectsZeroLengthFrameWithoutBlockingEventLoop(): void
{
$this->startTestAgent();

try {
$this->sendRawDataToAgent(pack('N', 0));

// Give the agent a chance to process the malformed frame before checking liveness.
usleep(100000);

$status = $this->getControlServerStatus(1.0);

if ($status === false) {
$this->forceStopTestAgent();
}

$this->assertTrue($status !== false, 'The control server should remain responsive after a zero-length frame.');
$this->assertSame(['queue_size' => 0], json_decode((string) $status, true));
} finally {
if ($this->agentProcess !== null) {
$this->stopTestAgent();
}
}
}

public function testAgentCompressesEnvelopeToUpstream(): void
{
if (!\extension_loaded('zlib')) {
Expand Down
46 changes: 40 additions & 6 deletions agent/tests/TestAgent.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ public function startTestAgent(): string
* The envelope must be a string in Sentry envelope format.
*/
public function sendEnvelopeToAgent(string $envelope): void
{
// The agent uses a 4-byte big-endian length prefix protocol
// The length includes the 4 bytes of the header itself
$length = \strlen($envelope) + 4;
$header = pack('N', $length);

$this->sendRawDataToAgent($header . $envelope);
}

/**
* Send raw data to the test agent.
*/
public function sendRawDataToAgent(string $data): void
{
if ($this->agentProcess === null) {
throw new \RuntimeException('There is no test agent instance running.');
Expand All @@ -138,19 +151,31 @@ public function sendEnvelopeToAgent(string $envelope): void
throw new \RuntimeException("Failed to connect to test agent: {$errstr} ({$errno})");
}

// The agent uses a 4-byte big-endian length prefix protocol
// The length includes the 4 bytes of the header itself
$length = \strlen($envelope) + 4;
$header = pack('N', $length);

fwrite($socket, $header . $envelope);
fwrite($socket, $data);

// Gracefully shutdown the write side, ensuring all data is sent before closing
stream_socket_shutdown($socket, \STREAM_SHUT_WR);

fclose($socket);
}

/**
* Get the raw control server status response.
*
* @return string|false
*/
public function getControlServerStatus(float $timeout = 1.0)
{
if ($this->agentProcess === null) {
throw new \RuntimeException('There is no test agent instance running.');
}

$controlServerAddress = "127.0.0.1:{$this->controlServerPort}";
$streamContext = stream_context_create(['http' => ['timeout' => $timeout]]);

return @file_get_contents("http://{$controlServerAddress}/status", false, $streamContext);
}

/**
* Wait for the agent queue to drain (all envelopes processed).
*
Expand Down Expand Up @@ -223,6 +248,15 @@ public function stopTestAgent(): string
// Wait for the queue to drain before killing the process
$this->waitForQueueDrain();

return $this->forceStopTestAgent();
}

protected function forceStopTestAgent(): string
{
if (!$this->agentProcess) {
throw new \RuntimeException('There is no test agent instance running.');
}

for ($i = 0; $i < 20; ++$i) {
$status = proc_get_status($this->agentProcess);

Expand Down
Binary file modified bin/sentry-agent
Binary file not shown.
2 changes: 1 addition & 1 deletion bin/sentry-agent.sig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
C779A799D4389C7E52048F1271B64B01DFFBD75C59E111C5DB180004E980D2CD6F56D59FDD14825EE3F6F8F002F5405D0CB97FB46199D730C7FDB77D4BB5A9A4
AFBE92B982EC0B29C172B8DCC97652704441D3559372FDE7A513B66CB759E655836548F1B7A67547BAE87141E7AC3FB12E51B53A835A23B6BEE2A25E7F44F908
Loading