Skip to content
Merged
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
180 changes: 99 additions & 81 deletions src/findings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,87 @@ namespace coretrace::runtime_analyzer
{
namespace
{
// A bounds report line, "ct: <kind> WRITE|READ of size N", and its CWE by access.
// A rule ParseFindings reports, with what a SARIF consumer shows of it.
struct CatalogueRule
{
std::string_view id;
RuleDescription description;
};

constexpr CatalogueRule kRules[] = {
{"heap-buffer-overflow",
{"Heap buffer overflow",
"A read or a write past the bounds of a heap allocation, "
"observed while the program ran.",
"high"}},
{"stack-buffer-overflow",
{"Stack buffer overflow",
"A read or a write past the bounds of a stack object, "
"observed while the program ran.",
"high"}},
{"heap-use-after-free",
{"Use of freed heap memory",
"A read or a write to heap memory after it was freed, "
"observed while the program ran.",
"high"}},
{"double-free",
{"Double free", "Heap memory freed a second time, observed while the program ran.",
"high"}},
{"memory-leak",
{"Memory leak", "Heap memory the program allocated and had not freed when it ended.",
"high"}},
{"vtable-null-this",
{"Virtual call through a null object",
"A virtual call made through a null pointer to an object.", "high"}},
{"vtable-use-after-free",
{"Virtual call through a freed object",
"A virtual call made through an object whose memory was already freed.", "high"}},
{"vtable-corrupted",
{"Virtual call through a corrupted vtable",
"A virtual call made through an object whose vtable pointer is missing or "
"invalid.",
"high"}},
{"vcall-invalid-target",
{"Virtual call to non-executable memory",
"A virtual call whose target address is not in executable memory.", "high"}},
{"vtable-type-mismatch",
{"Object of an unexpected dynamic type",
"At a virtual call, the object's dynamic type differs from its static type, or "
"comes from another module: a possible type confusion, or a legitimate cast.",
"medium"}},
{"vtable-diagnostic",
{"Other virtual call issue",
"A vtable or virtual call problem the runtime reported without a more specific "
"rule, such as a vtable without type information, as built with -fno-rtti.",
"low"}},
};

// The catalogue entry of `id`. The parser names its rules only through it, so a rule
// missing from kRules does not compile.
[[nodiscard]] consteval const CatalogueRule& RuleNamed(std::string_view id)
{
for (const CatalogueRule& rule : kRules)
{
if (rule.id == id)
{
return rule;
}
}
throw "rule missing from kRules";
}

// A bounds report line, "ct: <rule id> WRITE|READ of size N", and its CWE by access.
struct BoundsRule
{
std::string_view kind;
const CatalogueRule& rule;
std::string_view write_cwe;
std::string_view read_cwe;
};

constexpr BoundsRule kBoundsRules[] = {
{"heap-buffer-overflow", "CWE-122", "CWE-125"},
{"stack-buffer-overflow", "CWE-121", "CWE-125"},
{"heap-use-after-free", "CWE-416", "CWE-416"},
{RuleNamed("heap-buffer-overflow"), "CWE-122", "CWE-125"},
{RuleNamed("stack-buffer-overflow"), "CWE-121", "CWE-125"},
{RuleNamed("heap-use-after-free"), "CWE-416", "CWE-416"},
};

// The value of a "key=value" field, up to the next space. The key must start the line
Expand Down Expand Up @@ -92,21 +161,24 @@ namespace coretrace::runtime_analyzer
struct VtableRule
{
std::string_view warning; // matched as a prefix of the box's "warn" values
std::string_view rule;
const CatalogueRule& rule;
std::string_view cwe;
Severity severity;
};

constexpr VtableRule kVtableRules[] = {
{"null this pointer", "vtable-null-this", "CWE-476", Severity::Error},
{"vptr on freed object", "vtable-use-after-free", "CWE-416", Severity::Error},
{"vtable resolve failed", "vtable-corrupted", "CWE-843", Severity::Error},
{"no vptr", "vtable-corrupted", "CWE-843", Severity::Error},
{"target in non-exec memory", "vcall-invalid-target", "CWE-843", Severity::Error},
{"static!=dynamic type", "vtable-type-mismatch", "CWE-843", Severity::Warning},
{"module mismatch: ", "vtable-type-mismatch", "CWE-843", Severity::Warning},
{"null this pointer", RuleNamed("vtable-null-this"), "CWE-476", Severity::Error},
{"vptr on freed object", RuleNamed("vtable-use-after-free"), "CWE-416",
Severity::Error},
{"vtable resolve failed", RuleNamed("vtable-corrupted"), "CWE-843", Severity::Error},
{"no vptr", RuleNamed("vtable-corrupted"), "CWE-843", Severity::Error},
{"target in non-exec memory", RuleNamed("vcall-invalid-target"), "CWE-843",
Severity::Error},
{"static!=dynamic type", RuleNamed("vtable-type-mismatch"), "CWE-843",
Severity::Warning},
{"module mismatch: ", RuleNamed("vtable-type-mismatch"), "CWE-843", Severity::Warning},
// Valid under -fno-rtti, so it never outranks another warning of the box.
{"missing typeinfo", "vtable-diagnostic", "CWE-843", Severity::Warning},
{"missing typeinfo", RuleNamed("vtable-diagnostic"), "CWE-843", Severity::Warning},
};

// A box the runtime logged at WARN level: "[VTABLE]" or "[VCALL]", then "│ label : value │"
Expand Down Expand Up @@ -192,7 +264,7 @@ namespace coretrace::runtime_analyzer
{
return std::nullopt;
}
finding.rule = rule != nullptr ? rule->rule : "vtable-diagnostic";
finding.rule = rule != nullptr ? rule->rule.id : RuleNamed("vtable-diagnostic").id;
finding.cwe = rule != nullptr ? rule->cwe : "CWE-843";
finding.severity = rule != nullptr ? rule->severity : Severity::Warning;
return finding;
Expand All @@ -212,31 +284,31 @@ namespace coretrace::runtime_analyzer

[[nodiscard]] std::optional<Finding> BoundsFinding(std::string_view line)
{
for (const BoundsRule& rule : kBoundsRules)
for (const BoundsRule& bounds : kBoundsRules)
{
const std::size_t at = line.find("ct: " + std::string(rule.kind) + " ");
const std::size_t at = line.find("ct: " + std::string(bounds.rule.id) + " ");
if (at == std::string_view::npos)
{
continue;
}
Finding finding;
finding.rule = rule.kind;
finding.rule = bounds.rule.id;
finding.message = line.substr(at + 4);
const bool write = finding.message.find(" WRITE ") != std::string::npos;
finding.cwe = write ? rule.write_cwe : rule.read_cwe;
finding.cwe = write ? bounds.write_cwe : bounds.read_cwe;
return finding;
}
return std::nullopt;
}

// The site fields of a one-line report: "site=" is the faulting access and
// "alloc_site=" the allocation.
[[nodiscard]] Finding OneLineFinding(std::string_view line, std::string_view rule,
[[nodiscard]] Finding OneLineFinding(std::string_view line, const CatalogueRule& rule,
std::string_view cwe, Severity severity,
std::string message)
{
Finding finding;
finding.rule = rule;
finding.rule = rule.id;
finding.cwe = cwe;
finding.severity = severity;
finding.message = std::move(message);
Expand All @@ -254,63 +326,9 @@ namespace coretrace::runtime_analyzer

std::optional<RuleDescription> DescribeRule(std::string_view rule)
{
struct DescribedRule
{
std::string_view rule;
RuleDescription description;
};
// Every rule ParseFindings reports: the bounds rules, double-free and memory-leak, and the
// vtable rules with their fallback.
static constexpr DescribedRule kRules[] = {
{"heap-buffer-overflow",
{"Heap buffer overflow",
"A read or a write past the bounds of a heap allocation, "
"observed while the program ran.",
"high"}},
{"stack-buffer-overflow",
{"Stack buffer overflow",
"A read or a write past the bounds of a stack object, "
"observed while the program ran.",
"high"}},
{"heap-use-after-free",
{"Use of freed heap memory",
"A read or a write to heap memory after it was freed, "
"observed while the program ran.",
"high"}},
{"double-free",
{"Double free", "Heap memory freed a second time, observed while the program ran.",
"high"}},
{"memory-leak",
{"Memory leak", "Heap memory the program allocated and had not freed when it ended.",
"high"}},
{"vtable-null-this",
{"Virtual call through a null object",
"A virtual call made through a null pointer to an object.", "high"}},
{"vtable-use-after-free",
{"Virtual call through a freed object",
"A virtual call made through an object whose memory was already freed.", "high"}},
{"vtable-corrupted",
{"Virtual call through a corrupted vtable",
"A virtual call made through an object whose vtable pointer is missing or "
"invalid.",
"high"}},
{"vcall-invalid-target",
{"Virtual call to non-executable memory",
"A virtual call whose target address is not in executable memory.", "high"}},
{"vtable-type-mismatch",
{"Object of an unexpected dynamic type",
"At a virtual call, the object's dynamic type differs from its static type, or "
"comes from another module: a possible type confusion, or a legitimate cast.",
"medium"}},
{"vtable-diagnostic",
{"Other virtual call issue",
"A vtable or virtual call problem the runtime reported without a more specific "
"rule, such as a vtable without type information, as built with -fno-rtti.",
"low"}},
};
for (const DescribedRule& described : kRules)
for (const CatalogueRule& described : kRules)
{
if (described.rule == rule)
if (described.id == rule)
{
return described.description;
}
Expand Down Expand Up @@ -379,8 +397,8 @@ namespace coretrace::runtime_analyzer
}
else if (line.find("(double free)") != std::string_view::npos)
{
findings.push_back(
OneLineFinding(line, "double-free", "CWE-415", Severity::Error, "double free"));
findings.push_back(OneLineFinding(line, RuleNamed("double-free"), "CWE-415",
Severity::Error, "double free"));
}
else if (line.find("ct: leak ptr=") != std::string_view::npos)
{
Expand All @@ -389,8 +407,8 @@ namespace coretrace::runtime_analyzer
{
message += " of " + std::string(*size) + " bytes";
}
findings.push_back(OneLineFinding(line, "memory-leak", "CWE-401", Severity::Warning,
std::move(message)));
findings.push_back(OneLineFinding(line, RuleNamed("memory-leak"), "CWE-401",
Severity::Warning, std::move(message)));
}
}
return findings;
Expand Down
Loading