Repository navigation
Fix infinite loop on EOF/short-read in MAR-record reader (ArgusReadCo… - #38
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…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.