Skip to content

Fix infinite loop on EOF/short-read in MAR-record reader (ArgusReadCo… - #38

Merged
openargus merged 2 commits into
mainfrom
fix/eof-infinite-loop-mar-reader
Sep 8, 2026
Merged

openargus merged 2 commits into
mainfrom
fix/eof-infinite-loop-mar-reader

Conversation

@openargus

Copy link
Copy Markdown
Owner

…nnection)

ArgusReadConnection() has two near-identical loops that read the remaining bytes of a management (MAR) record after its initial 16-byte cookie header has already been validated -- one for local files (ARGUS_FILE, using fread()), one for socket connections (ARGUS_SOCKET, using read()). Both loop 'while (cnt != size)', accumulating bytes until the full MAR record has been read. Both had defective handling of the case where the underlying read call makes no further progress:

  • The ARGUS_FILE case's exit logic (checking feof()/ferror() and breaking out via goto out) was entirely wrapped in a /* ... */ comment, i.e. commented out completely. Once fread() started returning 0 (EOF) or a persistent error, the loop had no way to detect this and looped calling fread() again unconditionally, forever, since cnt can never reach size without forward progress.

  • The ARGUS_SOCKET case's exit logic only checked 'br < 0' (a hard read() error) before breaking out. read() returning exactly 0 -- the normal, correct way a TCP peer signals a clean disconnect -- fell through the same 'else' branch without matching that check, so the loop kept calling read() again, which returns 0 forever on an already-closed connection, spinning identically to the ARGUS_FILE case above.

In both cases, this means a truncated native or compressed local input file, or a remote peer/radium connection that disconnects (or never sends the remainder of its MAR record) partway through the initial handshake, hangs the client indefinitely with no way to make progress, time out, or exit -- a self-inflicted denial of service requiring no attacker at all, just a truncated file or a dropped connection.

Fixed by restoring/completing the missing exit condition in each loop: the ARGUS_FILE case's exit block is uncommented (fread() <= 0 now correctly checked via feof()/ferror() and treated as terminal); the ARGUS_SOCKET case's condition is changed from 'br < 0' to unconditional (any non-positive read(), matching the ARGUS_FILE case's <= 0 handling), since read() returning 0 is exactly as terminal for this loop's purposes as returning a negative error code.

Verified via clean rebuild (0 errors, 0 warnings on the changed file); the standard RIMPAC-pcap smoke test (1825 records, unaffected); the gzip/bzip2 decompression paths (interacting with the same ARGUS_FILE loop) still correctly read a real compressed argus file end-to-end; and, most directly, reproducing the actual defect on both paths before fixing it and confirming the fix resolves it:

  • A deliberately truncated local argus file (24 bytes -- a valid 16-byte cookie header followed by an incomplete MAR record) hung the pre-fix binary indefinitely (had to be force-killed after several seconds); the post-fix binary exits cleanly and promptly. Also reproduced and fixed for a truncated gzip-compressed file.

  • A local TCP test server that sends a real 16-byte ARGUS_V5_COOKIE MAR header (copied from a legitimate argus data file) and then closes the connection before sending the rest of the MAR record hung the pre-fix binary indefinitely (again force-killed); the post-fix binary exits cleanly and promptly.

This is legitimate malformed/truncated-input testing (a real, if incomplete, argus data stream), not adversarial payload construction, consistent with this review's testing policy.

…nnection)

ArgusReadConnection() has two near-identical loops that read the
remaining bytes of a management (MAR) record after its initial
16-byte cookie header has already been validated -- one for local
files (ARGUS_FILE, using fread()), one for socket connections
(ARGUS_SOCKET, using read()). Both loop 'while (cnt != size)',
accumulating bytes until the full MAR record has been read. Both had
defective handling of the case where the underlying read call makes no
further progress:

- The ARGUS_FILE case's exit logic (checking feof()/ferror() and
  breaking out via goto out) was entirely wrapped in a /* ... */
  comment, i.e. commented out completely. Once fread() started
  returning 0 (EOF) or a persistent error, the loop had no way to
  detect this and looped calling fread() again unconditionally,
  forever, since cnt can never reach size without forward progress.

- The ARGUS_SOCKET case's exit logic only checked 'br < 0' (a hard
  read() error) before breaking out. read() returning exactly 0 --
  the normal, correct way a TCP peer signals a clean disconnect --
  fell through the same 'else' branch without matching that check, so
  the loop kept calling read() again, which returns 0 forever on an
  already-closed connection, spinning identically to the ARGUS_FILE
  case above.

In both cases, this means a truncated native or compressed local
input file, or a remote peer/radium connection that disconnects (or
never sends the remainder of its MAR record) partway through the
initial handshake, hangs the client indefinitely with no way to make
progress, time out, or exit -- a self-inflicted denial of service
requiring no attacker at all, just a truncated file or a dropped
connection.

Fixed by restoring/completing the missing exit condition in each loop:
the ARGUS_FILE case's exit block is uncommented (fread() <= 0 now
correctly checked via feof()/ferror() and treated as terminal); the
ARGUS_SOCKET case's condition is changed from 'br < 0' to unconditional
(any non-positive read(), matching the ARGUS_FILE case's <= 0
handling), since read() returning 0 is exactly as terminal for this
loop's purposes as returning a negative error code.

Verified via clean rebuild (0 errors, 0 warnings on the changed file);
the standard RIMPAC-pcap smoke test (1825 records, unaffected); the
gzip/bzip2 decompression paths (interacting with the same ARGUS_FILE
loop) still correctly read a real compressed argus file end-to-end;
and, most directly, reproducing the actual defect on both paths before
fixing it and confirming the fix resolves it:

  - A deliberately truncated local argus file (24 bytes -- a valid
    16-byte cookie header followed by an incomplete MAR record) hung
    the pre-fix binary indefinitely (had to be force-killed after
    several seconds); the post-fix binary exits cleanly and promptly.
    Also reproduced and fixed for a truncated gzip-compressed file.

  - A local TCP test server that sends a real 16-byte ARGUS_V5_COOKIE
    MAR header (copied from a legitimate argus data file) and then
    closes the connection before sending the rest of the MAR record
    hung the pre-fix binary indefinitely (again force-killed); the
    post-fix binary exits cleanly and promptly.

This is legitimate malformed/truncated-input testing (a real, if
incomplete, argus data stream), not adversarial payload construction,
consistent with this review's testing policy.
@openargus
openargus merged commit 1410c95 into main Sep 8, 2026
5 checks passed
@openargus
openargus deleted the fix/eof-infinite-loop-mar-reader branch September 8, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant