From 9a33155233c4588cff9c4b82fcd0a2b732065441 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Sat, 18 Oct 2025 13:25:17 +0200 Subject: [PATCH 1/9] init DecOverflowWhenComparing query --- .../DecOverflowWhenComparing.c | 62 +++++++++++++++++ .../DecOverflowWhenComparing.qhelp | 34 ++++++++++ .../DecOverflowWhenComparing.ql | 57 ++++++++++++++++ .../DecOverflowWhenComparing.c | 66 +++++++++++++++++++ .../DecOverflowWhenComparing.expected | 0 .../DecOverflowWhenComparing.qlref | 1 + 6 files changed, 220 insertions(+) create mode 100644 cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c create mode 100644 cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qhelp create mode 100644 cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql create mode 100644 cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c create mode 100644 cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected create mode 100644 cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qlref diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c new file mode 100644 index 0000000..465ad8a --- /dev/null +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +// from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 +char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) +{ + int i = 0, j = 0; + uint32_t len; + uint32_t domainlen = 0; + char *domain = NULL; + + if ((data == NULL) || (datalen == 0)) return NULL; + + /* + * i: index into input data + * j: index into output string + */ + while (datalen-- > 0) + { + printf("%d\n", len); + len = data[i++]; + domainlen += (len + 1); + domain = reallocf(domain, domainlen); + + if (domain == NULL) return NULL; + + if (len == 0) break; // DNS root (NUL) + + if (j > 0) + { + domain[j++] = datalen ? '.' : '\0'; + } + + while ((len-- > 0) && (0 != datalen--)) + { + if (data[i] == '.') + { + /* special case: escape the '.' with a '\' */ + domain = reallocf(domain, ++domainlen); + if (domain == NULL) return NULL; + + domain[j++] = '\\'; + } + + domain[j++] = data[i++]; + } + } + + domain[j] = '\0'; + + return domain; +} + +int main() { + const uint16_t datalen = 128; + uint8_t data[datalen] = {}; + memcpy(data, "\x04quildu\x03xyz\x00", 11); + _mdns_parse_domain_name(data, datalen); +} + diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qhelp b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qhelp new file mode 100644 index 0000000..c51c425 --- /dev/null +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qhelp @@ -0,0 +1,34 @@ + + + +

+Finds unsigned integer overflow issues with the following heuristic: +* variable is compared to 0 and decremented +* variable is used after the comparison and decrementation + +In such cases it is likely that the decrementation was not expected. +

+ +

+You can read about a real-world vulnerability here: https://github.com/trailofbits/exploits/tree/main/obts-2025-macos-lpe +

+
+ + +

+Move the decrementation outside of comparison and/or add explicit checks for overflows. +

+
+ + + + +

+The datalen variable may overflow to UINT_MAX given a specific input. +

+
+ +
+ diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql new file mode 100644 index 0000000..de29b18 --- /dev/null +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -0,0 +1,57 @@ +/** + * @name Decrementation overflow when comparing + * @id tob/cpp/dec-overflow-when-comparing + * @description This query finds unsigned integer overflows resulting from unchecked decrementation during comparison. + * @kind problem + * @tags security + * @problem.severity error + * @precision high + * @security-severity 5.0 + * @group security + */ + +import cpp +import semmle.code.cpp.ir.IR + +from Variable var, VariableAccess varAcc, DecrementOperation dec, + VariableAccess varAccAfterOverflow, ComparisonOperation cmp +where + // get unsigned variable that is decremented + varAcc = var.getAnAccess() and + varAccAfterOverflow = var.getAnAccess() and + varAcc != varAccAfterOverflow and + dec.getOperand() = varAcc.getExplicitlyConverted() and + varAcc.getUnderlyingType().(IntegralType).isUnsigned() and + + // only decrementations inside comparisons + cmp.getAnOperand().getAChild*() = varAcc and + cmp.getAnOperand() instanceof Zero and + + // only if the variable is used after the comparison + cmp.getASuccessor+() = varAccAfterOverflow and + cmp.getAnOperand().getAChild*() != varAccAfterOverflow and + + // skip if the variable is overwritten + // TODO: handle loops correctly + // not exists(VariableAccess varAccLV | varAccLV.isUsedAsLValue() | + // varAccLV = var.getAnAccess() and + // varAccLV != varAcc and + // varAccLV != varAccAfterOverflow and + // cmp.getASuccessor+() = varAccLV and + // varAccAfterOverflow.getAPredecessor+() = varAccLV + // ) and + + // var-- > 0 (0 < var--) then accesses only in false branch + // var-- >= 0 then accesses in all branches + // var-- == 0 then accesses in all branches + // var-- != 0 then accesses in all branches + if ( + (cmp instanceof GTExpr and cmp.getRightOperand() instanceof Zero) + or + (cmp instanceof LTExpr and cmp.getLeftOperand() instanceof Zero) + ) + then + cmp.getAFalseSuccessor().getASuccessor*() = varAccAfterOverflow + else + any() +select cmp, varAccAfterOverflow \ No newline at end of file diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c new file mode 100644 index 0000000..08bcb71 --- /dev/null +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -0,0 +1,66 @@ +#ifndef USE_HEADERS +#include "../../../include/libc/string_stubs.h" + +#else +#include +#include +#include +#include +#endif + +// from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 +char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) +{ + int i = 0, j = 0; + uint32_t len; + uint32_t domainlen = 0; + char *domain = NULL; + + if ((data == NULL) || (datalen == 0)) return NULL; + + /* + * i: index into input data + * j: index into output string + */ + while (datalen-- > 0) + { + len = data[i++]; + domainlen += (len + 1); + domain = reallocf(domain, domainlen); + + if (domain == NULL) return NULL; + + if (len == 0) break; // DNS root (NUL) + + if (j > 0) + { + domain[j++] = datalen ? '.' : '\0'; + } + + while ((len-- > 0) && (0 != datalen--)) + { + if (data[i] == '.') + { + /* special case: escape the '.' with a '\' */ + domain = reallocf(domain, ++domainlen); + if (domain == NULL) return NULL; + + domain[j++] = '\\'; + } + + domain[j++] = data[i++]; + } + } + + domain[j] = '\0'; + + return domain; +} + +int main() { + const uint16_t datalen = 128; + uint8_t data[datalen] = {}; + memcpy(data, "\x04quildu\x03xyz\x00", 11); + _mdns_parse_domain_name(data, datalen); +} + diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected new file mode 100644 index 0000000..e69de29 diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qlref b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qlref new file mode 100644 index 0000000..154ed3e --- /dev/null +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.qlref @@ -0,0 +1 @@ +security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql \ No newline at end of file From 711f1979a22efc6b87baaaa514048b6e8e0f9423 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Sat, 18 Oct 2025 17:51:58 +0200 Subject: [PATCH 2/9] more work DecOverflowWhenComparing --- .../DecOverflowWhenComparing.c | 83 ++++++++++++++++++- .../DecOverflowWhenComparing.ql | 50 ++++++++--- cpp/test/include/libc/stdint.h | 15 ++++ .../DecOverflowWhenComparing.c | 15 ++-- 4 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 cpp/test/include/libc/stdint.h diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index 465ad8a..3bc5fde 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -3,11 +3,88 @@ #include #include +size_t min(size_t a, size_t b) { + return a < b ? a : b; +} + +void MlasReorderInputNhwc( + const float* S, + float* D, + size_t InputChannels, + size_t RowCount, + size_t FullRowCount + ) +{ + const size_t BlockSize = FullRowCount % 123; + + // + // Iterate over batches of the input size to improve locality. + // + + for (size_t OuterRowCountRemaining = RowCount; OuterRowCountRemaining > 0; ) { + + size_t OuterRowCountBatch = 32; + + const size_t OuterRowCountThisIteration = min(OuterRowCountRemaining, OuterRowCountBatch); + OuterRowCountRemaining -= OuterRowCountThisIteration; + + // + // Iterate over BlockSize batches of the input channels. + // + + const float* s = S; + float* d = D; + + for (size_t i = InputChannels; i > 0;) { + + const size_t InputChannelsThisIteration = min(i, BlockSize); + i -= InputChannelsThisIteration; + + const float* ss = s; + float* dd = d; + size_t InnerRowCountRemaining = OuterRowCountThisIteration; + + if (InputChannelsThisIteration == BlockSize) { + + if (BlockSize == 8) { + + while (InnerRowCountRemaining-- > 0) { + ss += InputChannels; + dd += 8; + } + + } else { + + while (InnerRowCountRemaining-- > 0) { + ss += InputChannels; + dd += 16; + } + } + + } else { + + size_t BlockPadding = BlockSize - InputChannelsThisIteration; + + while (InnerRowCountRemaining-- > 0) { + ss += InputChannels; + dd += BlockSize; + } + } + + s += InputChannelsThisIteration; + d += BlockSize * FullRowCount; + } + + S += InputChannels * OuterRowCountThisIteration; + D += BlockSize * OuterRowCountThisIteration; + } +} + // from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) { int i = 0, j = 0; - uint32_t len; + // uint32_t len; uint32_t domainlen = 0; char *domain = NULL; @@ -19,8 +96,8 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) */ while (datalen-- > 0) { - printf("%d\n", len); - len = data[i++]; + // printf("%d\n", len); + uint32_t len = data[i++]; domainlen += (len + 1); domain = reallocf(domain, domainlen); diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index de29b18..6d2744c 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -12,8 +12,33 @@ import cpp import semmle.code.cpp.ir.IR +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis -from Variable var, VariableAccess varAcc, DecrementOperation dec, +/** + * Find CFG paths from start to end that do not cross over node that is var's lvalue access + * TODO: there must be an API for that... + */ +predicate successorGuarded(ControlFlowNode start, ControlFlowNode end, Variable var) { + start = end + or + exists(ControlFlowNode interm | + start.getASuccessor() = interm and + + // break the path if variable is overwritten + not ( + interm = var.getAnAccess() and + interm.(VariableAccess).isLValue() + ) and + + ( + interm.getASuccessor() = end + or + successorGuarded(interm, end, var) + ) + ) +} + +from Variable var, VariableAccess varAcc, PostfixDecrExpr dec, VariableAccess varAccAfterOverflow, ComparisonOperation cmp where // get unsigned variable that is decremented @@ -28,19 +53,9 @@ where cmp.getAnOperand() instanceof Zero and // only if the variable is used after the comparison - cmp.getASuccessor+() = varAccAfterOverflow and + successorGuarded(cmp, varAccAfterOverflow, var) and cmp.getAnOperand().getAChild*() != varAccAfterOverflow and - // skip if the variable is overwritten - // TODO: handle loops correctly - // not exists(VariableAccess varAccLV | varAccLV.isUsedAsLValue() | - // varAccLV = var.getAnAccess() and - // varAccLV != varAcc and - // varAccLV != varAccAfterOverflow and - // cmp.getASuccessor+() = varAccLV and - // varAccAfterOverflow.getAPredecessor+() = varAccLV - // ) and - // var-- > 0 (0 < var--) then accesses only in false branch // var-- >= 0 then accesses in all branches // var-- == 0 then accesses in all branches @@ -54,4 +69,13 @@ where cmp.getAFalseSuccessor().getASuccessor*() = varAccAfterOverflow else any() -select cmp, varAccAfterOverflow \ No newline at end of file + + and + + // only if var may possibly be zero during comparison + lowerBound(varAcc) = 0 + + // skip tests etc + and not dec.getFile().getAbsolutePath().toLowerCase().matches(["%test%", "%vendor%", "%third_party%"]) + +select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), varAccAfterOverflow, varAccAfterOverflow.toString() \ No newline at end of file diff --git a/cpp/test/include/libc/stdint.h b/cpp/test/include/libc/stdint.h new file mode 100644 index 0000000..d02afcd --- /dev/null +++ b/cpp/test/include/libc/stdint.h @@ -0,0 +1,15 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_STDINT_STUB_H +#define HEADER_STDINT_STUB_H + +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned int uint32_t; + +#endif +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS \ No newline at end of file diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index 08bcb71..fd32da6 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -1,12 +1,13 @@ -#ifndef USE_HEADERS #include "../../../include/libc/string_stubs.h" +#include "../../../include/libc/stdlib.h" +#include "../../../include/libc/unistd.h" +#include "../../../include/libc/stdint.h" + +// #include +// #include +// #include +// #include -#else -#include -#include -#include -#include -#endif // from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) From 5d13a974845301ace3fe5da5c35b30218797cf33 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 10:52:23 +0100 Subject: [PATCH 3/9] nodes,edges --- .../DecOverflowWhenComparing.ql | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index 6d2744c..011a269 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -2,7 +2,7 @@ * @name Decrementation overflow when comparing * @id tob/cpp/dec-overflow-when-comparing * @description This query finds unsigned integer overflows resulting from unchecked decrementation during comparison. - * @kind problem + * @kind graph * @tags security * @problem.severity error * @precision high @@ -14,6 +14,33 @@ import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis +query predicate nodes(ControlFlowNode node, string key, string value) { + exists(Variable var, PostfixDecrExpr dec | + dec.getOperand() = var.getAnAccess().getExplicitlyConverted() and + var.getUnderlyingType().(IntegralType).isUnsigned() and + successorGuarded(node, _, var) and + key = node.toString() and + value = node.toString() + "-val" + ) +} + +query predicate edges(ControlFlowNode source, ControlFlowNode target, string key, string value) { + exists(Variable var, PostfixDecrExpr dec, VariableAccess acc | + var.getAnAccess() = acc and + dec.getOperand() = acc.getExplicitlyConverted() and + var.getUnderlyingType().(IntegralType).isUnsigned() and + + source.getASuccessor() = target and + + key = source.toString() + "-key" and + value = target.toString() + "-val" + ) +} + +query predicate graphProperties(string key, string value) { + key = "semmle.graphKind" and value = "graph" +} + /** * Find CFG paths from start to end that do not cross over node that is var's lvalue access * TODO: there must be an API for that... @@ -38,6 +65,7 @@ predicate successorGuarded(ControlFlowNode start, ControlFlowNode end, Variable ) } +/* from Variable var, VariableAccess varAcc, PostfixDecrExpr dec, VariableAccess varAccAfterOverflow, ComparisonOperation cmp where @@ -78,4 +106,6 @@ where // skip tests etc and not dec.getFile().getAbsolutePath().toLowerCase().matches(["%test%", "%vendor%", "%third_party%"]) -select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), varAccAfterOverflow, varAccAfterOverflow.toString() \ No newline at end of file +select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), varAccAfterOverflow, varAccAfterOverflow.toString() + +*/ \ No newline at end of file From 6f4f24c22f080269dd4a5458dc6da554bf386810 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 14:07:14 +0100 Subject: [PATCH 4/9] restart work on the query, fix tests --- cpp/test/include/libc/stdlib.h | 4 +++- cpp/test/include/libc/string_stubs.h | 3 ++- .../DecOverflowWhenComparing.c | 12 ++---------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/cpp/test/include/libc/stdlib.h b/cpp/test/include/libc/stdlib.h index cc18de3..7f28000 100644 --- a/cpp/test/include/libc/stdlib.h +++ b/cpp/test/include/libc/stdlib.h @@ -7,6 +7,8 @@ extern "C" { #endif +extern void *reallocf(void *, unsigned long); + int rand(void) { return 42; } @@ -32,4 +34,4 @@ void *malloc(unsigned long); #include -#endif // --- end USE_HEADERS \ No newline at end of file +#endif // --- end USE_HEADERS diff --git a/cpp/test/include/libc/string_stubs.h b/cpp/test/include/libc/string_stubs.h index a5cd662..400551c 100644 --- a/cpp/test/include/libc/string_stubs.h +++ b/cpp/test/include/libc/string_stubs.h @@ -12,6 +12,7 @@ extern "C" { #endif typedef int wchar_t; +extern void *memcpy(void *dst, const void *src, unsigned long n); extern char* strcpy_s(char* dst, int max_amount, char* src); extern int _mbsncat(char* dst, char* src, int count); extern int _mbsncmp(char* dst, char* src, int count); @@ -50,4 +51,4 @@ extern void closelog(void); #define _mbsnbcmp memcmp #define mempcpy memcpy -#endif \ No newline at end of file +#endif diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index fd32da6..68fc367 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -3,12 +3,6 @@ #include "../../../include/libc/unistd.h" #include "../../../include/libc/stdint.h" -// #include -// #include -// #include -// #include - - // from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) { @@ -59,9 +53,7 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) } int main() { - const uint16_t datalen = 128; - uint8_t data[datalen] = {}; + uint8_t data[128] = {0}; memcpy(data, "\x04quildu\x03xyz\x00", 11); - _mdns_parse_domain_name(data, datalen); + _mdns_parse_domain_name(data, 128); } - From f254a738f14704623eba4b07cf54760bdf620bbb Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 15:17:00 +0100 Subject: [PATCH 5/9] fix query --- .../DecOverflowWhenComparing.c | 84 ------------------- .../DecOverflowWhenComparing.ql | 53 +++--------- .../DecOverflowWhenComparing.c | 12 +-- .../DecOverflowWhenComparing.expected | 1 + 4 files changed, 18 insertions(+), 132 deletions(-) diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index 3bc5fde..bed379a 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -3,100 +3,17 @@ #include #include -size_t min(size_t a, size_t b) { - return a < b ? a : b; -} - -void MlasReorderInputNhwc( - const float* S, - float* D, - size_t InputChannels, - size_t RowCount, - size_t FullRowCount - ) -{ - const size_t BlockSize = FullRowCount % 123; - - // - // Iterate over batches of the input size to improve locality. - // - - for (size_t OuterRowCountRemaining = RowCount; OuterRowCountRemaining > 0; ) { - - size_t OuterRowCountBatch = 32; - - const size_t OuterRowCountThisIteration = min(OuterRowCountRemaining, OuterRowCountBatch); - OuterRowCountRemaining -= OuterRowCountThisIteration; - - // - // Iterate over BlockSize batches of the input channels. - // - - const float* s = S; - float* d = D; - - for (size_t i = InputChannels; i > 0;) { - - const size_t InputChannelsThisIteration = min(i, BlockSize); - i -= InputChannelsThisIteration; - - const float* ss = s; - float* dd = d; - size_t InnerRowCountRemaining = OuterRowCountThisIteration; - - if (InputChannelsThisIteration == BlockSize) { - - if (BlockSize == 8) { - - while (InnerRowCountRemaining-- > 0) { - ss += InputChannels; - dd += 8; - } - - } else { - - while (InnerRowCountRemaining-- > 0) { - ss += InputChannels; - dd += 16; - } - } - - } else { - - size_t BlockPadding = BlockSize - InputChannelsThisIteration; - - while (InnerRowCountRemaining-- > 0) { - ss += InputChannels; - dd += BlockSize; - } - } - - s += InputChannelsThisIteration; - d += BlockSize * FullRowCount; - } - - S += InputChannels * OuterRowCountThisIteration; - D += BlockSize * OuterRowCountThisIteration; - } -} - // from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) { int i = 0, j = 0; - // uint32_t len; uint32_t domainlen = 0; char *domain = NULL; if ((data == NULL) || (datalen == 0)) return NULL; - /* - * i: index into input data - * j: index into output string - */ while (datalen-- > 0) { - // printf("%d\n", len); uint32_t len = data[i++]; domainlen += (len + 1); domain = reallocf(domain, domainlen); @@ -136,4 +53,3 @@ int main() { memcpy(data, "\x04quildu\x03xyz\x00", 11); _mdns_parse_domain_name(data, datalen); } - diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index 011a269..69519ac 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -2,7 +2,7 @@ * @name Decrementation overflow when comparing * @id tob/cpp/dec-overflow-when-comparing * @description This query finds unsigned integer overflows resulting from unchecked decrementation during comparison. - * @kind graph + * @kind problem * @tags security * @problem.severity error * @precision high @@ -14,49 +14,24 @@ import cpp import semmle.code.cpp.ir.IR import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis -query predicate nodes(ControlFlowNode node, string key, string value) { - exists(Variable var, PostfixDecrExpr dec | - dec.getOperand() = var.getAnAccess().getExplicitlyConverted() and - var.getUnderlyingType().(IntegralType).isUnsigned() and - successorGuarded(node, _, var) and - key = node.toString() and - value = node.toString() + "-val" - ) -} - -query predicate edges(ControlFlowNode source, ControlFlowNode target, string key, string value) { - exists(Variable var, PostfixDecrExpr dec, VariableAccess acc | - var.getAnAccess() = acc and - dec.getOperand() = acc.getExplicitlyConverted() and - var.getUnderlyingType().(IntegralType).isUnsigned() and - - source.getASuccessor() = target and - - key = source.toString() + "-key" and - value = target.toString() + "-val" - ) -} - -query predicate graphProperties(string key, string value) { - key = "semmle.graphKind" and value = "graph" +/** + * Holds if `node` overwrites `var` (assignment or declaration with initializer). + */ +predicate isDefOf(ControlFlowNode node, Variable var) { + node = var.getAnAccess() and node.(VariableAccess).isLValue() + or + node.(DeclStmt).getADeclaration() = var and exists(var.getInitializer()) } /** - * Find CFG paths from start to end that do not cross over node that is var's lvalue access - * TODO: there must be an API for that... + * Find CFG paths from start to end that do not cross over a definition of var. */ predicate successorGuarded(ControlFlowNode start, ControlFlowNode end, Variable var) { start = end or exists(ControlFlowNode interm | start.getASuccessor() = interm and - - // break the path if variable is overwritten - not ( - interm = var.getAnAccess() and - interm.(VariableAccess).isLValue() - ) and - + not isDefOf(interm, var) and ( interm.getASuccessor() = end or @@ -65,7 +40,7 @@ predicate successorGuarded(ControlFlowNode start, ControlFlowNode end, Variable ) } -/* + from Variable var, VariableAccess varAcc, PostfixDecrExpr dec, VariableAccess varAccAfterOverflow, ComparisonOperation cmp where @@ -103,9 +78,7 @@ where // only if var may possibly be zero during comparison lowerBound(varAcc) = 0 - // skip tests etc - and not dec.getFile().getAbsolutePath().toLowerCase().matches(["%test%", "%vendor%", "%third_party%"]) + // skip vendor code + and not dec.getFile().getAbsolutePath().toLowerCase().matches(["%vendor%", "%third_party%"]) select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), varAccAfterOverflow, varAccAfterOverflow.toString() - -*/ \ No newline at end of file diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index 68fc367..c8eb914 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -7,19 +7,14 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) { int i = 0, j = 0; - uint32_t len; uint32_t domainlen = 0; char *domain = NULL; if ((data == NULL) || (datalen == 0)) return NULL; - /* - * i: index into input data - * j: index into output string - */ while (datalen-- > 0) { - len = data[i++]; + uint32_t len = data[i++]; domainlen += (len + 1); domain = reallocf(domain, domainlen); @@ -53,7 +48,8 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) } int main() { - uint8_t data[128] = {0}; + const uint16_t datalen = 128; + uint8_t data[datalen] = {}; memcpy(data, "\x04quildu\x03xyz\x00", 11); - _mdns_parse_domain_name(data, 128); + _mdns_parse_domain_name(data, datalen); } diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected index e69de29..4b11830 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected @@ -0,0 +1 @@ +| DecOverflowWhenComparing.c:30:31:30:39 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:30:26:30:39 | ... != ... | ... != ... | DecOverflowWhenComparing.c:15:9:15:15 | datalen | datalen | From 233dfa9e8029959debce6a1613379c387345a542 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 17:47:43 +0100 Subject: [PATCH 6/9] more tests --- .../DecOverflowWhenComparing.ql | 5 +- .../DecOverflowWhenComparing.c | 105 +++++++++++++++++- .../DecOverflowWhenComparing.expected | 8 +- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index 69519ac..6a3ecac 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -21,6 +21,8 @@ predicate isDefOf(ControlFlowNode node, Variable var) { node = var.getAnAccess() and node.(VariableAccess).isLValue() or node.(DeclStmt).getADeclaration() = var and exists(var.getInitializer()) + or + node.(Assignment).getLValue().(VariableAccess).getTarget() = var } /** @@ -55,9 +57,10 @@ where cmp.getAnOperand().getAChild*() = varAcc and cmp.getAnOperand() instanceof Zero and - // only if the variable is used after the comparison + // only if the variable is used (not just overwritten) after the comparison successorGuarded(cmp, varAccAfterOverflow, var) and cmp.getAnOperand().getAChild*() != varAccAfterOverflow and + not exists(AssignExpr ae | ae.getLValue() = varAccAfterOverflow) and // var-- > 0 (0 < var--) then accesses only in false branch // var-- >= 0 then accesses in all branches diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index c8eb914..989b6e2 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -3,6 +3,7 @@ #include "../../../include/libc/unistd.h" #include "../../../include/libc/stdint.h" +// BAD // from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) { @@ -20,18 +21,18 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) if (domain == NULL) return NULL; - if (len == 0) break; // DNS root (NUL) + if (len == 0) break; if (j > 0) { domain[j++] = datalen ? '.' : '\0'; } + // bug is in datalen decrementation here while ((len-- > 0) && (0 != datalen--)) { if (data[i] == '.') { - /* special case: escape the '.' with a '\' */ domain = reallocf(domain, ++domainlen); if (domain == NULL) return NULL; @@ -47,6 +48,106 @@ char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) return domain; } +// BAD: n used after loop without overwrite (overflowed on last iteration) +void simple_use_after_loop(uint32_t n, int *buf) { + while (n-- > 0) { + buf[0] = 1; + } + buf[n] = 0; // BAD: n is UINT32_MAX here +} + +// BAD: use in false branch of != 0 +void use_after_neq(uint32_t n, int *buf) { + if (n-- != 0) { + buf[0] = 1; + } + buf[n] = 0; // BAD +} + +// GOOD: variable overwritten by assignment before use +void overwrite_by_assignment(uint32_t n, int *buf) { + while (n-- > 0) { + buf[0] = 1; + } + n = 42; + buf[n] = 0; +} + +// GOOD: signed integer - not unsigned +void signed_decrement(int n, int *buf) { + while (n-- > 0) { + buf[n] = 0; + } +} + +// GOOD: variable not used after comparison +void no_use_after(uint32_t n, int *buf) { + while (n-- > 0) { + buf[0] = 1; + } +} + +// BAD: reversed comparison form 0 < n-- +void reversed_lt(uint32_t n, int *buf) { + while (0 < n--) { + buf[0] = 1; + } + buf[n] = 0; // BAD +} + +// BAD: n-- == 0, use in true branch where n just wrapped to UINT32_MAX +void eq_zero(uint32_t n, int *buf) { + if (n-- == 0) { + buf[n] = 0; // BAD + } +} + +// BAD: do-while, use after loop +void do_while(uint32_t n, int *buf) { + do { + buf[0] = 1; + } while (n-- > 0); + buf[n] = 0; // BAD +} + +// BAD: conditional overwrite (only one branch overwrites) +void conditional_overwrite(uint32_t n, int *buf, int cond) { + if (n-- != 0) { + buf[0] = 1; + } + if (cond) { + n = 42; + } + buf[n] = 0; // BAD: path exists where n is not overwritten +} + +// GOOD: use only in true branch of > 0 (n was confirmed > 0) +void use_in_true_branch(uint32_t n, int *buf) { + if (n-- > 0) { + buf[n] = 0; + } +} + +// GOOD: prefix decrement (query only checks postfix) +void prefix_decrement(uint32_t n, int *buf) { + while (--n > 0) { + buf[n] = 0; + } +} + +// GOOD: overwrite in all branches before use +void overwrite_all_branches(uint32_t n, int *buf, int cond) { + while (n-- > 0) { + buf[0] = 1; + } + if (cond) { + n = 10; + } else { + n = 20; + } + buf[n] = 0; +} + int main() { const uint16_t datalen = 128; uint8_t data[datalen] = {}; diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected index 4b11830..9e5a3de 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected @@ -1 +1,7 @@ -| DecOverflowWhenComparing.c:30:31:30:39 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:30:26:30:39 | ... != ... | ... != ... | DecOverflowWhenComparing.c:15:9:15:15 | datalen | datalen | +| DecOverflowWhenComparing.c:32:31:32:39 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:32:26:32:39 | ... != ... | ... != ... | DecOverflowWhenComparing.c:16:9:16:15 | datalen | datalen | +| DecOverflowWhenComparing.c:53:12:53:14 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:53:12:53:18 | ... > ... | ... > ... | DecOverflowWhenComparing.c:56:9:56:9 | n | n | +| DecOverflowWhenComparing.c:61:9:61:11 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:61:9:61:16 | ... != ... | ... != ... | DecOverflowWhenComparing.c:64:9:64:9 | n | n | +| DecOverflowWhenComparing.c:92:16:92:18 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:92:12:92:18 | ... < ... | ... < ... | DecOverflowWhenComparing.c:95:9:95:9 | n | n | +| DecOverflowWhenComparing.c:100:9:100:11 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:100:9:100:16 | ... == ... | ... == ... | DecOverflowWhenComparing.c:101:13:101:13 | n | n | +| DecOverflowWhenComparing.c:109:14:109:16 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:109:14:109:20 | ... > ... | ... > ... | DecOverflowWhenComparing.c:110:9:110:9 | n | n | +| DecOverflowWhenComparing.c:115:9:115:11 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:115:9:115:16 | ... != ... | ... != ... | DecOverflowWhenComparing.c:121:9:121:9 | n | n | From ab2450edad3569075c79306f1aa62104848430a6 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 18:01:48 +0100 Subject: [PATCH 7/9] use StackVariableReachability --- .../DecOverflowWhenComparing.ql | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index 6a3ecac..2f7ffb1 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -11,13 +11,13 @@ */ import cpp -import semmle.code.cpp.ir.IR import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis +import semmle.code.cpp.controlflow.StackVariableReachability /** * Holds if `node` overwrites `var` (assignment or declaration with initializer). */ -predicate isDefOf(ControlFlowNode node, Variable var) { +predicate isDefOf(ControlFlowNode node, StackVariable var) { node = var.getAnAccess() and node.(VariableAccess).isLValue() or node.(DeclStmt).getADeclaration() = var and exists(var.getInitializer()) @@ -26,62 +26,74 @@ predicate isDefOf(ControlFlowNode node, Variable var) { } /** - * Find CFG paths from start to end that do not cross over a definition of var. + * Identifies an unsigned postfix decrement inside a comparison against zero. */ -predicate successorGuarded(ControlFlowNode start, ControlFlowNode end, Variable var) { - start = end - or - exists(ControlFlowNode interm | - start.getASuccessor() = interm and - not isDefOf(interm, var) and - ( - interm.getASuccessor() = end - or - successorGuarded(interm, end, var) - ) - ) -} - - -from Variable var, VariableAccess varAcc, PostfixDecrExpr dec, - VariableAccess varAccAfterOverflow, ComparisonOperation cmp -where - // get unsigned variable that is decremented +pragma[nomagic] +predicate isDecInComparison( + PostfixDecrExpr dec, VariableAccess varAcc, + ComparisonOperation cmp, StackVariable var +) { varAcc = var.getAnAccess() and - varAccAfterOverflow = var.getAnAccess() and - varAcc != varAccAfterOverflow and dec.getOperand() = varAcc.getExplicitlyConverted() and varAcc.getUnderlyingType().(IntegralType).isUnsigned() and - - // only decrementations inside comparisons cmp.getAnOperand().getAChild*() = varAcc and cmp.getAnOperand() instanceof Zero and + lowerBound(varAcc) = 0 +} + +/** + * Identifies a non-assignment read of a variable + * (i.e., a use that could observe an overflowed value). + */ +pragma[nomagic] +predicate isReadOf(VariableAccess va, StackVariable var) { + va = var.getAnAccess() and + not exists(AssignExpr ae | ae.getLValue() = va) +} - // only if the variable is used (not just overwritten) after the comparison - successorGuarded(cmp, varAccAfterOverflow, var) and - cmp.getAnOperand().getAChild*() != varAccAfterOverflow and - not exists(AssignExpr ae | ae.getLValue() = varAccAfterOverflow) and +/** + * Basic-block-level reachability from a decrement comparison to a use + * of the same variable, blocked by any definition of the variable. + */ +class DecOverflowReach extends StackVariableReachability { + DecOverflowReach() { this = "DecOverflowReach" } - // var-- > 0 (0 < var--) then accesses only in false branch - // var-- >= 0 then accesses in all branches - // var-- == 0 then accesses in all branches - // var-- != 0 then accesses in all branches - if ( - (cmp instanceof GTExpr and cmp.getRightOperand() instanceof Zero) - or - (cmp instanceof LTExpr and cmp.getLeftOperand() instanceof Zero) - ) - then - cmp.getAFalseSuccessor().getASuccessor*() = varAccAfterOverflow - else - any() + override predicate isSource(ControlFlowNode node, StackVariable v) { + isDecInComparison(_, _, node, v) + } - and + override predicate isSink(ControlFlowNode node, StackVariable v) { + isReadOf(node, v) + } - // only if var may possibly be zero during comparison - lowerBound(varAcc) = 0 + override predicate isBarrier(ControlFlowNode node, StackVariable v) { + isDefOf(node, v) + } +} +from + SemanticStackVariable var, VariableAccess varAcc, PostfixDecrExpr dec, + VariableAccess varAccAfterOverflow, ComparisonOperation cmp +where + isDecInComparison(dec, varAcc, cmp, var) and + isReadOf(varAccAfterOverflow, var) and + varAcc != varAccAfterOverflow and + // reachable without intervening overwrite (basic-block-level analysis) + any(DecOverflowReach r).reaches(cmp, var, varAccAfterOverflow) and + // exclude accesses that are part of the comparison expression itself + not cmp.getAnOperand().getAChild*() = varAccAfterOverflow and + // var-- > 0 (0 < var--) then only accesses in false branch matter + ( + if + ( + cmp instanceof GTExpr and cmp.getRightOperand() instanceof Zero + or + cmp instanceof LTExpr and cmp.getLeftOperand() instanceof Zero + ) + then cmp.getAFalseSuccessor().getASuccessor*() = varAccAfterOverflow + else any() + ) and // skip vendor code - and not dec.getFile().getAbsolutePath().toLowerCase().matches(["%vendor%", "%third_party%"]) - -select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), varAccAfterOverflow, varAccAfterOverflow.toString() + not dec.getFile().getAbsolutePath().toLowerCase().matches(["%vendor%", "%third_party%"]) +select dec, "Unsigned decrementation in comparison ($@) - $@", cmp, cmp.toString(), + varAccAfterOverflow, varAccAfterOverflow.toString() From 6ac65337901b44adf9435659f578127f543a1448 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 18:18:47 +0100 Subject: [PATCH 8/9] support globals --- .../DecOverflowWhenComparing.ql | 61 ++++++++++++++++--- .../DecOverflowWhenComparing.c | 48 +++++++++++++++ .../DecOverflowWhenComparing.expected | 3 + 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql index 2f7ffb1..8a94088 100644 --- a/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql +++ b/cpp/src/security/DecOverflowWhenComparing/DecOverflowWhenComparing.ql @@ -17,7 +17,7 @@ import semmle.code.cpp.controlflow.StackVariableReachability /** * Holds if `node` overwrites `var` (assignment or declaration with initializer). */ -predicate isDefOf(ControlFlowNode node, StackVariable var) { +predicate isDefOf(ControlFlowNode node, Variable var) { node = var.getAnAccess() and node.(VariableAccess).isLValue() or node.(DeclStmt).getADeclaration() = var and exists(var.getInitializer()) @@ -31,7 +31,7 @@ predicate isDefOf(ControlFlowNode node, StackVariable var) { pragma[nomagic] predicate isDecInComparison( PostfixDecrExpr dec, VariableAccess varAcc, - ComparisonOperation cmp, StackVariable var + ComparisonOperation cmp, Variable var ) { varAcc = var.getAnAccess() and dec.getOperand() = varAcc.getExplicitlyConverted() and @@ -46,14 +46,14 @@ predicate isDecInComparison( * (i.e., a use that could observe an overflowed value). */ pragma[nomagic] -predicate isReadOf(VariableAccess va, StackVariable var) { +predicate isReadOf(VariableAccess va, Variable var) { va = var.getAnAccess() and not exists(AssignExpr ae | ae.getLValue() = va) } /** * Basic-block-level reachability from a decrement comparison to a use - * of the same variable, blocked by any definition of the variable. + * of the same stack variable, blocked by any definition of the variable. */ class DecOverflowReach extends StackVariableReachability { DecOverflowReach() { this = "DecOverflowReach" } @@ -71,15 +71,62 @@ class DecOverflowReach extends StackVariableReachability { } } +/** + * BB-level reachability for non-stack variables (globals, static locals). + * Holds if `sink` is reachable from the entry of `bb` without crossing + * a definition of `var`. + */ +private predicate nonStackBBEntryReaches( + BasicBlock bb, Variable var, ControlFlowNode sink +) { + exists(int n | + sink = bb.getNode(n) and + isReadOf(sink, var) and + not exists(int m | m < n | isDefOf(bb.getNode(m), var)) + ) + or + not isDefOf(bb.getNode(_), var) and + nonStackBBEntryReaches(bb.getASuccessor(), var, sink) +} + +/** + * BB-level reachability from `source` to `sink` for non-stack variables, + * without crossing a definition of `var`. + */ +pragma[nomagic] +predicate nonStackReaches( + ComparisonOperation source, Variable var, ControlFlowNode sink +) { + not var instanceof StackVariable and + exists(BasicBlock bb, int i | + bb.getNode(i) = source and + not bb.isUnreachable() + | + exists(int j | + j > i and + sink = bb.getNode(j) and + isReadOf(sink, var) and + not exists(int k | k in [i + 1 .. j - 1] | isDefOf(bb.getNode(k), var)) + ) + or + not exists(int k | k > i | isDefOf(bb.getNode(k), var)) and + nonStackBBEntryReaches(bb.getASuccessor(), var, sink) + ) +} + from - SemanticStackVariable var, VariableAccess varAcc, PostfixDecrExpr dec, + Variable var, VariableAccess varAcc, PostfixDecrExpr dec, VariableAccess varAccAfterOverflow, ComparisonOperation cmp where isDecInComparison(dec, varAcc, cmp, var) and isReadOf(varAccAfterOverflow, var) and varAcc != varAccAfterOverflow and - // reachable without intervening overwrite (basic-block-level analysis) - any(DecOverflowReach r).reaches(cmp, var, varAccAfterOverflow) and + // reachable without intervening overwrite + ( + any(DecOverflowReach r).reaches(cmp, var, varAccAfterOverflow) + or + nonStackReaches(cmp, var, varAccAfterOverflow) + ) and // exclude accesses that are part of the comparison expression itself not cmp.getAnOperand().getAChild*() = varAccAfterOverflow and // var-- > 0 (0 < var--) then only accesses in false branch matter diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c index 989b6e2..9c87254 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.c @@ -148,6 +148,54 @@ void overwrite_all_branches(uint32_t n, int *buf, int cond) { buf[n] = 0; } +// --- Non-stack variable tests --- + +uint32_t g_count; + +// BAD: global variable used after loop without overwrite +void global_use_after_loop(int *buf) { + while (g_count-- > 0) { + buf[0] = 1; + } + buf[g_count] = 0; // BAD +} + +// BAD: global variable in if-neq pattern +void global_use_after_neq(int *buf) { + if (g_count-- != 0) { + buf[0] = 1; + } + buf[g_count] = 0; // BAD +} + +// GOOD: global variable overwritten before use +void global_overwrite(int *buf) { + while (g_count-- > 0) { + buf[0] = 1; + } + g_count = 42; + buf[g_count] = 0; +} + +// BAD: static local variable used after loop +void static_local_use(int *buf) { + static uint32_t s_count; + while (s_count-- > 0) { + buf[0] = 1; + } + buf[s_count] = 0; // BAD +} + +// GOOD: static local variable overwritten before use +void static_local_overwrite(int *buf) { + static uint32_t s_count; + while (s_count-- > 0) { + buf[0] = 1; + } + s_count = 42; + buf[s_count] = 0; +} + int main() { const uint16_t datalen = 128; uint8_t data[datalen] = {}; diff --git a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected index 9e5a3de..5a43789 100644 --- a/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected +++ b/cpp/test/query-tests/security/DecOverflowWhenComparing/DecOverflowWhenComparing.expected @@ -5,3 +5,6 @@ | DecOverflowWhenComparing.c:100:9:100:11 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:100:9:100:16 | ... == ... | ... == ... | DecOverflowWhenComparing.c:101:13:101:13 | n | n | | DecOverflowWhenComparing.c:109:14:109:16 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:109:14:109:20 | ... > ... | ... > ... | DecOverflowWhenComparing.c:110:9:110:9 | n | n | | DecOverflowWhenComparing.c:115:9:115:11 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:115:9:115:16 | ... != ... | ... != ... | DecOverflowWhenComparing.c:121:9:121:9 | n | n | +| DecOverflowWhenComparing.c:157:12:157:20 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:157:12:157:24 | ... > ... | ... > ... | DecOverflowWhenComparing.c:160:9:160:15 | g_count | g_count | +| DecOverflowWhenComparing.c:165:9:165:17 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:165:9:165:22 | ... != ... | ... != ... | DecOverflowWhenComparing.c:168:9:168:15 | g_count | g_count | +| DecOverflowWhenComparing.c:183:12:183:20 | ... -- | Unsigned decrementation in comparison ($@) - $@ | DecOverflowWhenComparing.c:183:12:183:24 | ... > ... | ... > ... | DecOverflowWhenComparing.c:186:9:186:15 | s_count | s_count | From 568145ce40f8cb6f7c1292ebe59fca8c97af7823 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Thu, 12 Feb 2026 18:26:19 +0100 Subject: [PATCH 9/9] fix readme --- README.md | 9 ++- .../DecOverflowWhenComparing.md | 72 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 cpp/src/docs/security/DecOverflowWhenComparing/DecOverflowWhenComparing.md diff --git a/README.md b/README.md index 660a339..3669d3c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif - | Name | Description | Severity | Precision | | --- | ----------- | :----: | :--------: | +|[BN_CTX_free called before BN_CTX_end](./cpp/src/docs/crypto/BnCtxFreeBeforeEnd.md)|Detects BN_CTX_free called before BN_CTX_end, which violates the required lifecycle|error|medium| +|[Unbalanced BN_CTX_start and BN_CTX_end pair](./cpp/src/docs/crypto/UnbalancedBnCtx.md)|Detects if one call in the BN_CTX_start/BN_CTX_end pair is missing|warning|medium| |[Crypto variable initialized using static key](./cpp/src/docs/crypto/StaticKeyFlow.md)|Finds crypto variables initialized using static keys|error|high| |[Crypto variable initialized using static password](./cpp/src/docs/crypto/StaticPasswordFlow.md)|Finds crypto variables initialized using static passwords|error|high| |[Crypto variable initialized using weak randomness](./cpp/src/docs/crypto/WeakRandomnessTaint.md)|Finds crypto variables initialized using weak randomness|error|high| @@ -46,9 +48,10 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif - | Name | Description | Severity | Precision | | --- | ----------- | :----: | :--------: | |[Async unsafe signal handler](./cpp/src/docs/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.md)|Async unsafe signal handler (like the one used in CVE-2024-6387)|warning|high| +|[Decrementation overflow when comparing](./cpp/src/docs/security/DecOverflowWhenComparing/DecOverflowWhenComparing.md)|This query finds unsigned integer overflows resulting from unchecked decrementation during comparison.|error|high| +|[Find all problematic implicit casts](./cpp/src/docs/security/UnsafeImplicitConversions/UnsafeImplicitConversions.md)|Find all implicit casts that may be problematic. That is, casts that may result in unexpected truncation, reinterpretation or widening of values.|error|high| |[Invalid string size passed to string manipulation function](./cpp/src/docs/security/CStrnFinder/CStrnFinder.md)|Finds calls to functions that take as input a string and its size as separate arguments (e.g., `strncmp`, `strncat`, ...) and the size argument is wrong|error|low| |[Missing null terminator](./cpp/src/docs/security/NoNullTerminator/NoNullTerminator.md)|This query finds incorrectly initialized strings that are passed to functions expecting null-byte-terminated strings|error|high| -|[Unsafe implicit integer conversion](./cpp/src/docs/security/UnsafeImplicitConversions/UnsafeImplicitConversions.md)|Finds implicit integer casts that may overflow or be truncated, with false positive reduction via Value Range Analysis|warning|low| ### Go @@ -63,7 +66,7 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif - | Name | Description | Severity | Precision | | --- | ----------- | :----: | :--------: | |[Invalid file permission parameter](./go/src/docs/security/FilePermsFlaws/FilePermsFlaws.md)|Finds non-octal (e.g., `755` vs `0o755`) and unsupported (e.g., `04666`) literals used as a filesystem permission parameter (`FileMode`)|error|medium| -|[Missing MinVersion in tls.Config](./go/src/docs/security/MissingMinVersionTLS/MissingMinVersionTLS.md)|This rule finds cases when you do not set the `tls.Config.MinVersion` explicitly for servers. By default version 1.0 is used, which is considered insecure. This rule does not mark explicitly set insecure versions|error|medium| +|[Missing MinVersion in tls.Config](./go/src/docs/security/MissingMinVersionTLS/MissingMinVersionTLS.md)|Finds uses of tls.Config where MinVersion is not set and the project's minimum Go version (from go.mod) indicates insecure defaults: Go < 1.18 for clients or Go < 1.22 for servers. Does not mark explicitly set versions (including explicitly insecure ones).|error|medium| |[Trim functions misuse](./go/src/docs/security/TrimMisuse/TrimMisuse.md)|Finds calls to `string.{Trim,TrimLeft,TrimRight}` with the 2nd argument not being a cutset but a continuous substring to be trimmed|error|low| ### Java-kotlin @@ -72,7 +75,7 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif - | Name | Description | Severity | Precision | | --- | ----------- | :----: | :--------: | -|[Recursive functions](./java-kotlin/src/docs/security/Recursion/Recursion.md)|Detects recursive calls|warning|low| +|[Recursive functions](./java-kotlin/src/docs/security/Recursion/Recursion.md)|Detects possibly unbounded recursive calls|warning|low| ## Query suites diff --git a/cpp/src/docs/security/DecOverflowWhenComparing/DecOverflowWhenComparing.md b/cpp/src/docs/security/DecOverflowWhenComparing/DecOverflowWhenComparing.md new file mode 100644 index 0000000..fd00dfd --- /dev/null +++ b/cpp/src/docs/security/DecOverflowWhenComparing/DecOverflowWhenComparing.md @@ -0,0 +1,72 @@ +# Decrementation overflow when comparing +Finds unsigned integer overflow issues with the following heuristic: \* variable is compared to 0 and decremented \* variable is used after the comparison and decrementation In such cases it is likely that the decrementation was not expected. + +You can read about a real-world vulnerability here: https://github.com/trailofbits/exploits/tree/main/obts-2025-macos-lpe + + +## Recommendation +Move the decrementation outside of comparison and/or add explicit checks for overflows. + + +## Example + +```c +#include +#include +#include +#include + +// from https://github.com/apple-oss-distributions/Libinfo/blob/9fce29e5c5edc15d3ecea55116ca17d3f6350603/lookup.subproj/mdns_module.c#L1033C1-L1079C2 +char* _mdns_parse_domain_name(const uint8_t *data, uint32_t datalen) +{ + int i = 0, j = 0; + uint32_t domainlen = 0; + char *domain = NULL; + + if ((data == NULL) || (datalen == 0)) return NULL; + + while (datalen-- > 0) + { + uint32_t len = data[i++]; + domainlen += (len + 1); + domain = reallocf(domain, domainlen); + + if (domain == NULL) return NULL; + + if (len == 0) break; // DNS root (NUL) + + if (j > 0) + { + domain[j++] = datalen ? '.' : '\0'; + } + + while ((len-- > 0) && (0 != datalen--)) + { + if (data[i] == '.') + { + /* special case: escape the '.' with a '\' */ + domain = reallocf(domain, ++domainlen); + if (domain == NULL) return NULL; + + domain[j++] = '\\'; + } + + domain[j++] = data[i++]; + } + } + + domain[j] = '\0'; + + return domain; +} + +int main() { + const uint16_t datalen = 128; + uint8_t data[datalen] = {}; + memcpy(data, "\x04quildu\x03xyz\x00", 11); + _mdns_parse_domain_name(data, datalen); +} + +``` +The `datalen` variable may overflow to UINT_MAX given a specific input. +