{
  "number": 5544,
  "title": "codeql: add FprimeUnguardedUnsignedSubtraction query",
  "url": "https://github.com/nasa/fprime/pull/5544",
  "state": "closed",
  "merged": true,
  "merged_at": "2026-08-05T01:30:22Z",
  "merge_commit_sha": "8818e1572c3d7c40b8c25d9a9ffd80def1920f61",
  "checked_at": "2026-10-15",
  "body": "Draft \u2014 opening this for a design question before polishing (see **Open question** at the end), since the answer may change what the query reports.\n\n## What it finds\n\nUnsigned subtraction of a compile-time constant from a run-time size, where nothing in the same function establishes that the size is large enough. Because the operands are unsigned, an undersized value doesn't go negative \u2014 it wraps to near `SIZE_MAX`, and downstream length arithmetic walks off the buffer.\n\nThe rule that makes this worth its own query, rather than being covered by a stock CWE-191 check: **a comparison that appears only inside `FW_ASSERT` does not count as a guard.** `FW_ASSERT` is compiled out at lower `FW_ASSERT_LEVEL` settings, so code that reads as protected in review can be unprotected in a flight build.\n\nThat's the same concern the four existing `FprimeUseOfAssertions*` queries address, and this one reuses their `FprimeAssertions.qll`.\n\n## Results on this repository\n\nMeasured against a build-traced CodeQL database of `devel` (11,266 defined functions, googletest excluded). **Six results:**\n\n| Location | Function | Situation |\n|---|---|---|\n| `Fw/Dp/DpContainer.cpp:132` | `setBuffer` | assert-only guarded |\n| `Svc/Ccsds/AosFramer.cpp:103` | `compute_and_inject_fecf` | **no guard of any kind** |\n| `Svc/Ccsds/SpacePacketFramer.cpp:62` | `dataIn_handler` | assert-only guarded |\n| `Svc/DpCompressProc/DpCompressProc.cpp:79` | `procRequest_handler` | **the defect under review in #5518** |\n| `Svc/FileDownlink/FileDownlink.cpp:395` | `sendCancelPacket` | assert-only guarded |\n| `Svc/FileDownlink/FileDownlink.cpp:436` | `sendFilePacket` | assert-only guarded |\n\nTwo of these are worth calling out together. `DpCompressProc.cpp:79` is the bug in #5518; `DpContainer.cpp:132` is the *upstream half of the same issue* \u2014 the `FW_ASSERT` that #5518 was relying on across a translation-unit boundary. The query found both ends independently.\n\n`AosFramer.cpp:103` subtracts `AOSTrailer::SERIALIZED_SIZE` from a frame buffer size with no check at all, not even an assertion. I haven't traced whether that frame buffer can ever be shorter than the trailer in practice.\n\n## Design choices\n\nDeliberately conservative in three ways, to be usable as a CI gate:\n\n1. **Subtrahend must be a compile-time constant.** Variable-minus-variable is far more common and far more often intentional.\n2. **Minuend must come from a known size accessor** (`getSize`, `getDataSize`, `getDeserializeSizeLeft`, \u2026). A bare integer subtraction says nothing about intent; `getSize() - K` says the author is reasoning about a buffer.\n3. **Guard recognition is direction-agnostic with no dominance analysis.** Any relational comparison in the same function between the accessor and a large-enough constant is accepted, whether written as an early return (`size < K`) or positively (`size >= K`). This admits false negatives \u2014 a check on a path that doesn't actually reach the subtraction \u2014 in exchange for a low false-positive rate.\n\nOn the threshold: a comparison against constant `C` is accepted for `accessor - K` when `C >= K - 1`. The `- 1` matters \u2014 it credits the idiomatic `if (size > 0)` guard for `size - 1`. An earlier revision compared the two constants for equality and produced a false positive on exactly that shape; `test.cpp` pins the fix. A too-weak `if (size > 2)` guarding `size - MIN_PACKET_SIZE` is still reported.\n\n## Testing\n\n`test.cpp` covers six cases, following the existing pack convention:\n\n- reported: unguarded; assert-only guarded; a second accessor; a too-weak guard\n- silent: `> 0` guarding `- 1`; a stronger-than-required guard\n\nVerified locally with `codeql test run --additional-packs=.` \u2014 passing. Note I ran CLI 2.26.2 while this workflow pins 2.26.1, so please regenerate `.expected` with `--learn` if that one-patch difference matters.\n\n## Open question\n\n**Four of the six results say \"you're relying on `FW_ASSERT` here.\"** That may well be intentional in F Prime, in which case this query is too noisy as written and should only report sites with no check at all \u2014 a one-line change that would reduce this to `AosFramer` plus `DpCompressProc`.\n\nI don't think that call is mine to make. If assert-only reliance is a deliberate and accepted pattern, say so and I'll narrow it. If it's something you'd want visibility on, the current form gives you that.\n\nHappy to also split `AosFramer.cpp:103` into its own issue if you'd rather track the unguarded case separately from the query itself.\n",
  "files": [
    {
      "filename": ".github/codeql/fprime-queries-tests/FprimeUnguardedUnsignedSubtraction/FprimeUnguardedUnsignedSubtraction.expected",
      "patch": "@@ -0,0 +1,7 @@\n+| test.cpp:30:28:30:69 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:78:34:78:75 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:106:32:106:73 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:124:22:124:61 | ... - ... | Unsigned subtraction from getDataSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:136:28:136:69 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:146:28:146:69 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |\n+| test.cpp:155:28:155:69 | ... - ... | Unsigned subtraction from getSize() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small. |",
      "blob_url": "https://github.com/nasa/fprime/blob/a83e01637a55de41d465cf135d817150a99117ea/.github%2Fcodeql%2Ffprime-queries-tests%2FFprimeUnguardedUnsignedSubtraction%2FFprimeUnguardedUnsignedSubtraction.expected"
    },
    {
      "filename": ".github/codeql/fprime-queries-tests/FprimeUnguardedUnsignedSubtraction/FprimeUnguardedUnsignedSubtraction.qlref",
      "patch": "@@ -0,0 +1 @@\n+FprimeUnguardedUnsignedSubtraction.ql",
      "blob_url": "https://github.com/nasa/fprime/blob/a83e01637a55de41d465cf135d817150a99117ea/.github%2Fcodeql%2Ffprime-queries-tests%2FFprimeUnguardedUnsignedSubtraction%2FFprimeUnguardedUnsignedSubtraction.qlref"
    },
    {
      "filename": ".github/codeql/fprime-queries-tests/FprimeUnguardedUnsignedSubtraction/test.cpp",
      "patch": "@@ -0,0 +1,167 @@\n+namespace Fw {\n+void SwAssert(int line);\n+}\n+#define FW_ASSERT(cond) ((cond) ? static_cast<void>(0) : Fw::SwAssert(__LINE__))\n+\n+typedef unsigned long FwSizeType;\n+typedef unsigned char U8;\n+\n+// Minimal stand-ins for the F Prime buffer/container shapes.\n+class Buffer {\n+  public:\n+    FwSizeType getSize() const;\n+    U8* getData() const;\n+};\n+\n+class Container {\n+  public:\n+    static const FwSizeType MIN_PACKET_SIZE = 40;\n+    static const FwSizeType DATA_OFFSET = 24;\n+    FwSizeType getDataSize() const;\n+};\n+\n+void consume(U8* data, FwSizeType size);\n+\n+// Violation of cpp/fprime/unguarded-unsigned-subtraction (CWE-191):\n+// the buffer size comes from the caller and is never checked against\n+// MIN_PACKET_SIZE, so a short buffer underflows to near SIZE_MAX.\n+// This is the shape fixed in nasa/fprime#5518.\n+void unguarded(const Buffer& buf) {\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Compliant: FW_ASSERT is a valid check in F Prime. Projects choose their\n+// own assert level; a site that asserts the invariant is not unchecked.\n+void checkedByAssert(const Buffer& buf) {\n+    FW_ASSERT(buf.getSize() >= Container::MIN_PACKET_SIZE);\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Compliant: an explicit run-time check that survives with assertions\n+// disabled.\n+void guardedByEarlyReturn(const Buffer& buf) {\n+    if (buf.getSize() < Container::MIN_PACKET_SIZE) {\n+        return;\n+    }\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Compliant: the same check written the other way round.\n+void guardedByIf(const Buffer& buf) {\n+    if (buf.getSize() >= Container::MIN_PACKET_SIZE) {\n+        consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+    }\n+}\n+\n+// Compliant: the check is written on a local copy of the size. This is the\n+// Fw::DpContainer::setBuffer shape.\n+void checkedOnLocalCopy(const Buffer& buf) {\n+    const FwSizeType size = buf.getSize();\n+    FW_ASSERT(size >= Container::MIN_PACKET_SIZE);\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+FwSizeType requiredSize();\n+\n+// Compliant: the bound is computed at run time, so the query cannot show it\n+// is too small. This is the Svc::FileDownlink::sendFilePacket shape.\n+void checkedAgainstComputedBound(const Buffer& buf) {\n+    const FwSizeType needed = requiredSize() + Container::MIN_PACKET_SIZE;\n+    FW_ASSERT(buf.getSize() >= needed);\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Violation: the only comparison of the size is unrelated to the subtraction\n+// and far too weak to cover it. This is the shape of the FW_ASSERT that\n+// follows the subtraction in DpCompressProc; it must keep being reported.\n+void unrelatedCheckAfterSubtraction(const Buffer& buf) {\n+    const FwSizeType remaining = buf.getSize() - Container::MIN_PACKET_SIZE;\n+    FW_ASSERT(buf.getSize() > 0);\n+    consume(buf.getData(), remaining);\n+}\n+\n+// Compliant: `> 0` is the idiomatic guard for a `- 1` subtraction. The\n+// guard constant (0) is one less than the subtrahend (1), which is\n+// exactly strong enough. An earlier version of this query compared the\n+// two constants for equality and reported this, which was a false\n+// positive -- this case pins the fix.\n+void guardedByGreaterThanZero(const Buffer& buf) {\n+    if (buf.getSize() > 0) {\n+        consume(buf.getData(), buf.getSize() - 1);\n+    }\n+}\n+\n+// Compliant: a guard stronger than required is still a guard.\n+void guardedByStrongerBound(const Buffer& buf) {\n+    if (buf.getSize() < Container::MIN_PACKET_SIZE + 8) {\n+        return;\n+    }\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Violation: a guard that is too weak to cover the subtraction. Checking\n+// against 2 says nothing about whether the size reaches MIN_PACKET_SIZE.\n+void guardedTooWeakly(const Buffer& buf) {\n+    if (buf.getSize() > 2) {\n+        consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+    }\n+}\n+\n+// Compliant: subtracting a variable rather than a compile-time constant\n+// is out of scope -- too common and too often intentional.\n+void variableSubtrahend(const Buffer& buf, FwSizeType n) {\n+    consume(buf.getData(), buf.getSize() - n);\n+}\n+\n+// Compliant: not a size accessor, so the query says nothing about intent.\n+FwSizeType plainArithmetic(FwSizeType value) {\n+    return value - Container::MIN_PACKET_SIZE;\n+}\n+\n+// Violation: the accessor is a different one from the guarded family,\n+// and it is unchecked here.\n+void unguardedDataSize(const Container& c) {\n+    consume(nullptr, c.getDataSize() - Container::DATA_OFFSET);\n+}\n+\n+// Violation: the only size comparison in the function is on a different\n+// object, so nothing here constrains `buf`. This is the shape that let\n+// nasa/fprime#5518 through a passing test suite: `procRequest_handler` is\n+// ~330 lines and compares `compression_buffer` and `data_reser`, never the\n+// port-supplied `fwBuffer`. The bound is computed at run time, which the\n+// query must keep accepting for same-object checks -- so only the receiver\n+// separates this from `checkedAgainstComputedBound` above.\n+void checkOnOtherObject(const Buffer& buf, const Buffer& other) {\n+    const FwSizeType limit = requiredSize();\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+    FW_ASSERT(other.getSize() <= limit);\n+}\n+\n+// Violation: the bound is a constant and is strong enough, but it is\n+// checked on a different object. Isolates receiver identity from bound\n+// arithmetic: a fix that only tightened the bound handling passes the case\n+// above and fails this one.\n+void checkOnOtherObjectConstantBound(const Buffer& buf, const Buffer& other) {\n+    FW_ASSERT(other.getSize() >= Container::MIN_PACKET_SIZE);\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}\n+\n+// Violation: the check is on a local copy of a *different* object's size.\n+// The receiver has to be carried out through the local-copy form as well as\n+// the direct call; a fix that matches receivers only on direct calls still\n+// misses this. DpCompressProc has exactly this second, independent path.\n+void checkOnLocalCopyOfOtherObject(const Buffer& buf, const Buffer& other) {\n+    const FwSizeType otherSize = other.getSize();\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+    FW_ASSERT(otherSize >= Container::MIN_PACKET_SIZE);\n+}\n+\n+// Compliant: `buf` is checked, even though the function also compares a\n+// different buffer. One sufficient check is enough -- receiver matching\n+// must not degrade into requiring every comparison to be on the subtracted\n+// object.\n+void checkedAmongOtherObjectChecks(const Buffer& buf, const Buffer& other) {\n+    FW_ASSERT(other.getSize() > 0);\n+    FW_ASSERT(buf.getSize() >= Container::MIN_PACKET_SIZE);\n+    consume(buf.getData(), buf.getSize() - Container::MIN_PACKET_SIZE);\n+}",
      "blob_url": "https://github.com/nasa/fprime/blob/a83e01637a55de41d465cf135d817150a99117ea/.github%2Fcodeql%2Ffprime-queries-tests%2FFprimeUnguardedUnsignedSubtraction%2Ftest.cpp"
    },
    {
      "filename": ".github/codeql/fprime-queries/FprimeUnguardedUnsignedSubtraction.ql",
      "patch": "@@ -0,0 +1,142 @@\n+/**\n+ * @name Unguarded unsigned subtraction from a run-time size\n+ * @description Subtracting a compile-time constant from a size obtained at\n+ *              run time underflows to a very large value, rather than going\n+ *              negative, when the size is smaller than the constant. A check\n+ *              may be a condition or an FW_ASSERT; this reports sites with\n+ *              neither.\n+ * @kind problem\n+ * @id cpp/fprime/unguarded-unsigned-subtraction\n+ * @problem.severity warning\n+ * @tags correctness\n+ *       reliability\n+ *       external/cwe/cwe-191\n+ */\n+\n+import cpp\n+\n+/*\n+ * Motivated by nasa/fprime#5518, where `DpCompressProc::procRequest_handler`\n+ * computed `fwBuffer.getSize() - Fw::DpContainer::MIN_PACKET_SIZE` on a\n+ * port-supplied buffer with no check of any kind in the handler.\n+ *\n+ * Deliberately conservative, to be usable as a CI gate: the subtrahend must\n+ * be a compile-time constant, the minuend must come from a known size\n+ * accessor, and check recognition is direction-agnostic with no dominance\n+ * analysis. A bound of `C` is accepted for `accessor - K` when `C >= K - 1`,\n+ * so `if (size > 0)` credits `size - 1`; `test.cpp` pins that case.\n+ *\n+ * A check only counts when it reads the size of the *same object* that the\n+ * subtraction reads. Without that, any comparison of any object's size\n+ * anywhere in the enclosing function suppresses the finding, which is how\n+ * the original #5518 defect escaped: `procRequest_handler` is ~330 lines and\n+ * compares the sizes of three other buffers, never `fwBuffer`.\n+ */\n+\n+/**\n+ * A call returning a size or length determined at run time, rather than\n+ * one the calling function chose itself.\n+ */\n+predicate isSizeAccessor(Call c) {\n+  c.getTarget().getName() =\n+    [\n+      \"getSize\", \"getBuffLength\", \"getDataSize\", \"getBuffCapacity\",\n+      \"getDeserializeSizeLeft\", \"getSerializeSizeLeft\"\n+    ]\n+}\n+\n+/**\n+ * Gets the object that the size accessor call `c` reads from, identified by\n+ * the declaration its receiver resolves to: the variable or field accessed,\n+ * or the enclosing class when the call is on `this`. Comparing declarations\n+ * rather than expressions makes `this->m_buffer.getSize()` in a check match\n+ * `this->m_buffer.getSize()` in a subtraction.\n+ *\n+ * Has no result when the receiver is not one of those shapes, for example\n+ * `getBuffer().getSize()`.\n+ */\n+Declaration sizeAccessorReceiver(Call c) {\n+  isSizeAccessor(c) and\n+  exists(Expr recv | recv = c.getQualifier() |\n+    result = recv.(VariableAccess).getTarget()\n+    or\n+    recv instanceof ThisExpr and\n+    result = c.getTarget().(MemberFunction).getDeclaringType()\n+  )\n+}\n+\n+/**\n+ * Holds if the size accessor calls `a` and `b` read from the same object.\n+ *\n+ * When neither receiver resolves to a declaration the calls are treated as\n+ * matching. That keeps the previous name-only behaviour for shapes this\n+ * query cannot reason about, rather than reporting them.\n+ */\n+predicate sameReceiver(Call a, Call b) {\n+  sizeAccessorReceiver(a) = sizeAccessorReceiver(b)\n+  or\n+  not exists(sizeAccessorReceiver(a)) and\n+  not exists(sizeAccessorReceiver(b)) and\n+  isSizeAccessor(a) and\n+  isSizeAccessor(b)\n+}\n+\n+/**\n+ * Holds if `read` reads the same size, from the same object, as the accessor\n+ * call `minuend`: either a direct call, or an access to a local variable\n+ * whose initializer is such a call. The local-copy form is idiomatic --\n+ * `const FwSizeType n = buf.getSize();` then a check on `n` -- and a check\n+ * written that way is still a check; `Fw::DpContainer::setBuffer` does\n+ * exactly this.\n+ */\n+predicate readsSameSize(Expr read, Call minuend) {\n+  exists(Call acc |\n+    acc = read.(Call)\n+    or\n+    exists(LocalScopeVariable v |\n+      read = v.getAnAccess() and\n+      acc = v.getInitializer().getExpr()\n+    )\n+  |\n+    isSizeAccessor(acc) and\n+    acc.getTarget().getName() = minuend.getTarget().getName() and\n+    sameReceiver(acc, minuend)\n+  )\n+}\n+\n+/**\n+ * Holds if the function containing `minuend` compares the size that\n+ * `minuend` reads -- same accessor, same object -- against a bound that is\n+ * not demonstrably too small for `minuend - k`. An `FW_ASSERT` condition\n+ * counts: it is an ordinary comparison in the AST, and F Prime treats an\n+ * assertion as a valid check -- projects choose their own assert level. A\n+ * bound with no constant value is accepted, since the query cannot show it\n+ * is too small.\n+ */\n+bindingset[k]\n+predicate hasCheck(Call minuend, float k) {\n+  exists(RelationalOperation rop, Expr sizeSide, Expr bound |\n+    rop.getEnclosingFunction() = minuend.getEnclosingFunction() and\n+    sizeSide = rop.getAnOperand() and\n+    bound = rop.getAnOperand() and\n+    bound != sizeSide and\n+    exists(Expr read | read = sizeSide.getAChild*() and readsSameSize(read, minuend)) and\n+    (\n+      not exists(bound.getValue())\n+      or\n+      bound.getValue().toFloat() >= k - 1.0\n+    )\n+  )\n+}\n+\n+from SubExpr sub, Call minuend, Expr subtrahend, float k\n+where\n+  sub.getUnspecifiedType().(IntegralType).isUnsigned() and\n+  minuend.getParent*() = sub.getLeftOperand() and\n+  isSizeAccessor(minuend) and\n+  subtrahend = sub.getRightOperand() and\n+  k = subtrahend.getValue().toFloat() and\n+  not hasCheck(minuend, k)\n+select sub,\n+  \"Unsigned subtraction from \" + minuend.getTarget().getName() +\n+    \"() with no check that it is at least the subtracted constant; this underflows instead of going negative when the size is too small.\"",
      "blob_url": "https://github.com/nasa/fprime/blob/a83e01637a55de41d465cf135d817150a99117ea/.github%2Fcodeql%2Ffprime-queries%2FFprimeUnguardedUnsignedSubtraction.ql"
    }
  ],
  "reviews": [
    {
      "user": "LeStarch",
      "state": "CHANGES_REQUESTED",
      "body": "`FW_ASSERT` is considered a valid check in F Prime.  While, yes, asserts can be compiled out, it is up to a project to select their level of paranoia on this.  Typically:\n\n1. Test in the lab and let FW_ASSERT catch errors. Fix them.\n2. Compile out for flight to reduce FW_ASSERT traps.\n\nThus we should reduce this query to only cases where it is not checked at all.",
      "url": "https://github.com/nasa/fprime/pull/5544#pullrequestreview-4847556173"
    },
    {
      "user": "LeStarch",
      "state": "APPROVED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5544#pullrequestreview-4860042381"
    }
  ],
  "comments": [
    {
      "user": "nurdymuny",
      "body": "Understood, and thank you \u2014 that's the answer I was missing. Narrowed in `250b5a5`.\n\nOne thing worth flagging before the diff, because it's bigger than the one line your comment implies. Deleting the `FW_ASSERT` exclusion on its own does not reduce this to \"not checked at all\" \u2014 two of the four assert-guarded sites survive it for reasons that have nothing to do with assertions:\n\n**`Fw/Dp/DpContainer.cpp:132`** \u2014 the check is written on a local copy, so the guard predicate never matched a call there in the first place:\n```cpp\n127  const FwSizeType bufferSize = buffer.getSize();\n128  FW_ASSERT(bufferSize >= MIN_PACKET_SIZE, ...);\n132  const FwSizeType dataCapacity = buffer.getSize() - MIN_PACKET_SIZE;\n```\nLine 128 contains a variable access, not a call, so it failed on the accessor match rather than on the assertion.\n\n**`Svc/FileDownlink/FileDownlink.cpp:436`** \u2014 the bound is computed at run time:\n```cpp\n425  const U32 bufferSize = filePacket.bufferSize() + sizeof(FwPacketDescriptorType);\n427  FW_ASSERT(this->m_buffer.getSize() >= bufferSize, ...);\n```\n`bufferSize` has no constant value, so nothing satisfied the `>= k - 1` threshold.\n\nShipping the one-liner would have left both reported and earned this comment a second time, so `hasRuntimeGuard` is replaced by `hasCheck`, which reads the size through a local copy as well as directly and accepts a bound whose value the query cannot determine. `FprimeAssertions` is dropped entirely \u2014 an `FW_ASSERT` condition is an ordinary comparison in the AST, so it now counts with no special casing, which also brings this query into line with `FprimeDivisionGuard` and `FprimeCheckingParameterValues`, both of which already say a check may be a condition *or* an `FW_ASSERT`. This was the only query in the pack treating an assertion as a non-check.\n\n**Test cases.** `guardedOnlyByAssert` is renamed `checkedByAssert` and flips to compliant. Added `checkedOnLocalCopy` and `checkedAgainstComputedBound` for the two shapes above, and one more that must keep being reported:\n\n```cpp\nvoid unrelatedCheckAfterSubtraction(const Buffer& buf) {\n    const FwSizeType remaining = buf.getSize() - Container::MIN_PACKET_SIZE;\n    FW_ASSERT(buf.getSize() > 0);\n    consume(buf.getData(), remaining);\n}\n```\n\nThat's the shape of the `FW_ASSERT` that follows the subtraction in `DpCompressProc`. A predicate loosened to \"any comparison of this accessor anywhere in the function\" would accept it and silence the defect this query was written to find. The case pins that so CI catches it rather than a reviewer.\n\nTest passes; `.expected` regenerated with v2.26.1 to match `CODEQL_CLI_VERSION`. Reported set is now `unguarded`, `unrelatedCheckAfterSubtraction`, `guardedTooWeakly`, `unguardedDataSize`; the seven checked shapes are all silent.\n\n**Predicted effect on `devel`: 2 hits**, down from 6 \u2014 `Svc/Ccsds/AosFramer/AosFramer.cpp:103`, which has no relational comparison of a size at all, and `Svc/DpCompressProc/DpCompressProc.cpp:79`. Once #5518 merges that second one self-clears, leaving `AosFramer` alone. I have not re-measured against a build-traced database since narrowing, so treat those two as derived from reading the sources rather than observed; the added clauses can only suppress, so no new hits can appear. Happy to rebuild the database and confirm before you take this out of draft.\n\nCorrecting my own note in the PR description while I'm here: it predicted a one-line change would reduce this to `AosFramer` plus `DpCompressProc`. That was wrong, for the two reasons above.\n",
      "url": "https://github.com/nasa/fprime/pull/5544#issuecomment-5172316279"
    },
    {
      "user": "nurdymuny",
      "body": "Correcting myself before you spend time on this: the version I pushed as `250b5a5` had a false negative on the defect this query exists to find. Fixed in `a83e016`. Details, because the failure is instructive about the query's limits.\n\n**What was wrong.** I measured the narrowed query against a build-traced database of pre-fix `devel`. It reported `Svc/Ccsds/AosFramer/AosFramer.cpp:103` and **did not report** `Svc/DpCompressProc/DpCompressProc.cpp:79`.\n\n`procRequest_handler` is around 330 lines and compares the sizes of three buffers \u2014 none of them the port-supplied `fwBuffer`:\n\n```cpp\n 80  FW_ASSERT(data_buffer.getSize() > 0, ...);\n205  FW_ASSERT(compression_buffer.getSize() <= min_compression);\n336  FW_ASSERT(ser_loc <= deser_loc, ...);\n```\n\n`hasCheck` matched on accessor *name* only, so any of these vouched for `fwBuffer.getSize()`. Line 205 was sufficient on its own: `min_compression` is a local with no constant value, and the clause I had added to suppress `FileDownlink.cpp:436` accepts any bound it cannot evaluate. In a long function that clause is close to a blanket exemption.\n\n**The fix.** A comparison now only counts when it reads the size of the *same object* as the subtraction's minuend, identified by the declaration the receiver resolves to. The local-copy form carries the receiver out with it, so `Fw::DpContainer::setBuffer` \u2014 which checks `bufferSize`, a local initialised from `buffer.getSize()` \u2014 stays suppressed on the strength of the same receiver rather than a coincidence of names.\n\n**Measured on the pre-fix `devel` database:**\n\n| Site | Verdict |\n|---|---|\n| `Svc/DpCompressProc/DpCompressProc.cpp:79` | reported |\n| `Svc/Ccsds/AosFramer/AosFramer.cpp:103` | reported |\n| `Fw/Dp/DpContainer.cpp:132` | suppressed \u2014 same receiver, bound `MIN_PACKET_SIZE` |\n| `Svc/FileDownlink/FileDownlink.cpp:395`, `:436` | suppressed \u2014 same receiver, run-time bound |\n| `Svc/Ccsds/SpacePacketFramer/SpacePacketFramer.cpp:62` | suppressed \u2014 same receiver, bound 0 \u2265 k\u22121 = 0 |\n| `SpacePacketDeframer:87`, `:89`, `FileUplink:65`, `FprimeDeframer:108` \u00d72 | suppressed |\n\n**2 hits, up from 1.** Receiver identity only makes the check stricter, so the result set can only grow \u2014 the 1 \u2192 2 movement confirms nothing was lost. Now that #5518 has merged, `DpCompressProc:79` is fixed upstream and a scan of current `devel` should leave `AosFramer` alone.\n\n**On the tests.** The old suite passed on both the broken and the fixed query, which is how this reached a commit. Three cases added \u2014 a check on a different object with a run-time bound, one with a constant bound, and one on a local copy of a different object's size. Verified as regression detectors: the `250b5a5` query fails the new suite, missing exactly those three lines.\n\n**What this still does not cover**, since I would rather state it than have it found:\n\n1. **Two sites are suppressed solely because their bound has no constant value** (`FileDownlink:395`, `:436`). Any run-time bound on the right object is accepted, however unrelated. That clause is required to honour your ruling on those sites, so receiver identity narrows this hole rather than closing it.\n2. **The receiver key cannot distinguish two objects of the same class** \u2014 `a->m_buf` would vouch for `b->m_buf`. No site in `devel` hits this today, but it errs toward false negatives, which is the direction that produced this bug. A recursive receiver comparison closes it; I left it out because nothing exercises it and it adds stratification risk. Happy to add it if you would rather have the belt.\n3. **No dominance analysis** \u2014 a comparison after the subtraction still counts. At `SpacePacketDeframer:87` the subtraction is *inside* the comparison that suppresses it; it is genuinely guarded by an earlier return, but the query cannot tell those apart. Pre-existing and untouched.\n4. **Cross-accessor suppression is untested.** The query still requires accessor names to match, so `getDataSize() >= 24` cannot vouch for `getSize() - 65`, but no stub class exposes two accessors so nothing pins that. If someone later drops the name check as redundant now that receivers match, the suite stays green.\n\nTaking it out of draft. `.expected` regenerated with v2.26.1.\n",
      "url": "https://github.com/nasa/fprime/pull/5544#issuecomment-5174880237"
    }
  ],
  "review_comments": []
}
