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.
167 lines
4.4 KiB
ObjectPascal
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.
|