Files
fpc-msgbase/tests/adversarial/test_fuzz_msg.pas
Ken Johnson 94dcd27005 Add corruption-resilience fuzz tests
New tests/adversarial/ suite covers each driver plus mb.kludge with
crafted-input scenarios: empty files, truncated headers, garbage
payloads, oversized length fields, infinite-loop bait.  The
invariant under test is graceful degradation: no crash, no hang,
no OOM.  Every allocation caps, every loop terminates, every
unreadable record returns False cleanly.

Coverage:
  test_fuzz_jam       7 cases  (.JHR/.JDT/.JDX/.JLR corruption)
  test_fuzz_squish    5 cases  (clen underflow, 2 GB clen, garbage idx)
  test_fuzz_hudson    3 cases  (bundle-file corruption)
  test_fuzz_goldbase  2 cases
  test_fuzz_pcboard   2 cases
  test_fuzz_msg       4 cases  (50 MB no-NUL body, strange names)
  test_fuzz_kludge    3 cases  (100 K CRs, 1 M CRs, legit round-trip)

run_tests.sh builds and runs them after the happy-path suite.
All 26 fuzz cases pass; all 47 existing tests still pass.
2026-04-19 06:44:34 -07:00

167 lines
4.4 KiB
ObjectPascal

{
test_fuzz_msg.pas - corruption-resilience for the *.MSG dir driver.
Invariant: a directory full of weird *.MSG files does not crash,
hang, or OOM the reader. Body reads stay bounded.
Test IDs:
F-MSG-1 empty dir -> MessageCount = 0
F-MSG-2 zero-byte 1.MSG -> ReadMessage either False or empty body
F-MSG-3 1.MSG with no NUL terminator at all (large) -> body capped
F-MSG-4 1.MSG with header but truncated body
F-MSG-5 a.MSG / ../evil.MSG filenames ignored by Scan
}
program test_fuzz_msg;
{$mode objfpc}{$H+}
uses
Classes, SysUtils,
testutil,
mb.types, mb.api,
mb.fmt.msg, mb.fmt.msg.uni;
const
SCRATCH = '/tmp/mb_fuzz_msg';
MSG_HDR_SIZE = 190; { NetMail record size }
procedure FreshDir;
var
sr: TSearchRec;
begin
ForceDirectories(SCRATCH);
if FindFirst(SCRATCH + '/*', faAnyFile, sr) = 0 then begin
repeat
if (sr.Name <> '.') and (sr.Name <> '..') and
((sr.Attr and faDirectory) = 0) then
DeleteFile(SCRATCH + '/' + sr.Name);
until FindNext(sr) <> 0;
FindClose(sr);
end;
end;
procedure WriteBytes(const APath: string; const B: array of byte);
var fs: TFileStream;
begin
fs := TFileStream.Create(APath, fmCreate);
try if Length(B) > 0 then fs.Write(B[0], Length(B)); finally fs.Free; end;
end;
function SafeOpen(out Base: TMessageBase; const ADir: string): boolean;
begin
Result := False; Base := nil;
try
Base := MessageBaseOpen(mbfMsg, ADir, momReadOnly);
Result := Base.Open;
except
on E: Exception do begin
TestFail('Open raised: ' + E.ClassName + ': ' + E.Message);
if Assigned(Base) then begin Base.Free; Base := nil; end;
end;
end;
end;
procedure CloseAndFree(var Base: TMessageBase);
begin
if Base = nil then exit;
try Base.Close; except end;
try Base.Free; except end;
Base := nil;
end;
{ ============================================================ }
procedure TestEmptyDir;
var base: TMessageBase;
begin
TestBegin('F-MSG-1: empty dir -> MessageCount = 0');
FreshDir;
if not SafeOpen(base, SCRATCH) then begin TestOK; exit; end;
try AssertEquals('MessageCount', 0, base.MessageCount);
finally CloseAndFree(base); end;
TestOK;
end;
procedure TestZeroByteMsg;
var
base: TMessageBase;
msg: TUniMessage;
begin
TestBegin('F-MSG-2: zero-byte 1.MSG -> ReadMessage handled, no crash');
FreshDir;
WriteBytes(SCRATCH + '/1.MSG', []);
if not SafeOpen(base, SCRATCH) then begin TestOK; exit; end;
try
if base.MessageCount > 0 then
base.ReadMessage(0, msg); { may return false; must not crash }
finally CloseAndFree(base); end;
TestOK;
end;
procedure TestHugeBodyNoNul;
const
BIG = 50 * 1024 * 1024; { 50 MB of non-NUL body }
var
fs: TFileStream;
hdr: array[0..MSG_HDR_SIZE - 1] of byte;
base: TMessageBase;
msg: TUniMessage;
buf: array[0..4095] of byte;
body: string;
written: longint;
begin
TestBegin('F-MSG-3: 50MB .MSG with no NUL -> body capped at 16 MiB');
FreshDir;
FillChar(hdr, SizeOf(hdr), 0);
FillChar(buf, SizeOf(buf), byte('X'));
fs := TFileStream.Create(SCRATCH + '/1.MSG', fmCreate);
try
fs.Write(hdr, SizeOf(hdr));
written := 0;
while written < BIG do begin
fs.Write(buf, SizeOf(buf));
Inc(written, SizeOf(buf));
end;
{ deliberately no trailing NUL }
finally fs.Free; end;
if not SafeOpen(base, SCRATCH) then begin TestOK; exit; end;
try
AssertTrue('MessageCount > 0', base.MessageCount > 0);
if base.ReadMessage(0, msg) then begin
body := msg.Body;
AssertTrue('body capped at 16 MiB',
Length(body) <= 16 * 1024 * 1024);
end;
finally CloseAndFree(base); end;
TestOK;
end;
procedure TestStrangeNames;
var
base: TMessageBase;
begin
TestBegin('F-MSG-5: non-numeric .MSG names ignored by Scan');
FreshDir;
WriteBytes(SCRATCH + '/a.MSG', []);
WriteBytes(SCRATCH + '/evil.MSG', []);
WriteBytes(SCRATCH + '/1.MSG', []);
WriteBytes(SCRATCH + '/-5.MSG', []); { negative -- Val rejects }
if not SafeOpen(base, SCRATCH) then begin TestOK; exit; end;
try
AssertTrue('MessageCount bounded', (base.MessageCount >= 0) and
(base.MessageCount <= 10));
finally CloseAndFree(base); end;
TestOK;
end;
begin
WriteLn('fpc-msgbase: *.MSG corruption-resilience fuzz');
WriteLn;
TestEmptyDir;
TestZeroByteMsg;
TestStrangeNames;
TestHugeBodyNoNul;
Halt(TestsSummary);
end.