{
  "number": 5518,
  "title": "Svc/DpCompressProc: reject buffers smaller than MIN_PACKET_SIZE locally",
  "url": "https://github.com/nasa/fprime/pull/5518",
  "state": "closed",
  "merged": true,
  "merged_at": "2026-08-04T04:39:50Z",
  "merge_commit_sha": "1e91573bf3ae131eefb715f4df2b8ee7fb6e96b7",
  "checked_at": "2026-10-15",
  "body": "| | |\n|:---|:---|\n|**_Related Issue(s)_**| |\n|**_Has Unit Tests (y/n)_**| y |\n|**_Documentation Included (y/n)_**| y |\n|**_Generative AI was used in this contribution (y/n)_**| |\n\n---\n\n## Change Description\n\n`DpCompressProc::procRequest_handler` constructed a `Fw::DpContainer` directly from the port-supplied buffer and then computed:\n\n```cpp\nFw::Buffer data_buffer(fwBuffer.getData() + Fw::DpContainer::DATA_OFFSET,\n                       fwBuffer.getSize() - Fw::DpContainer::MIN_PACKET_SIZE);\n```\n\nThe subtraction is unsigned, and nothing in this handler checked the size first.\n\nThis adds an early-return guard before the container is constructed, a dedicated event for the rejection, and an off-nominal unit test.\n\n```diff\n+    // Check that the buffer is large enough to hold a data product packet\n+    if (fwBuffer.getSize() < Fw::DpContainer::MIN_PACKET_SIZE) {\n+        this->log_WARNING_HI_BufferTooSmallForPacket(fwBuffer.getSize(), Fw::DpContainer::MIN_PACKET_SIZE);\n+        return;\n+    }\n     Fw::DpContainer container(0, fwBuffer);\n```\n\n**New event.** `BufferTooSmallForPacket(buffer_size, min_size)`, `warning high`, `throttle 10`, appended as id 3 so the existing event ids are unchanged. It is named and shaped after the event [`Svc/DpWriter`](https://github.com/nasa/fprime/blob/devel/Svc/DpWriter/DpWriter.fpp#L72) already raises for this identical `< MIN_PACKET_SIZE` check on the calling side \u2014 DpCompressProc appears to have been derived from DpWriter and inherited its `InvalidHeader` without the other half of the pair. `InvalidHeader` keeps its own meaning, so the two rejections are distinguishable in downlinked telemetry.\n\nTwo small departures from DpWriter, both to match this file rather than that one: snake_case argument names to match `InvalidHeader` directly above, and `FwSizeType` for `min_size` rather than DpWriter's `U32`, since `MIN_PACKET_SIZE` is declared `FwSizeType` and it avoids a cast.\n\n`docs/sdd.md` gains rows for this event and for `InvalidHeader`, which was already missing from that table.\n\n## Rationale\n\nThe invariant `getSize() >= MIN_PACKET_SIZE` was enforced only by [`FW_ASSERT` inside `Fw::DpContainer::setBuffer`](https://github.com/nasa/fprime/blob/devel/Fw/Dp/DpContainer.cpp#L128) \u2014 a different translation unit from where it is relied on here. With assertions compiled out, a buffer of size in `[Header::SIZE, MIN_PACKET_SIZE)` passes `setBuffer` silently, `deserializeHeader()` succeeds because the header bytes are all present, and the subtraction underflows to near `SIZE_MAX`. `data_buffer` is then constructed with an astronomically large size, and the loop at line 121 walks off the end of the real allocation.\n\nIndependently of assert level, this consumer depends structurally on an invariant enforced in another file. A future widening of `MIN_PACKET_SIZE` or a refactor of `setBuffer` could break it with no diff to `DpCompressProc.cpp`. `Svc/DpWriter` already performs this same check locally for the same reason.\n\n## Testing/Review Recommendations\n\n`fprime-util check Svc/DpCompressProc` \u2014 10/10 pass (9 existing plus the new off-nominal case).\n\n`DpCompressProcTester::test_undersized_buffer` allocates a `MIN_PACKET_SIZE + 1` backing array, writes a well-formed header with `dataSize = 1`, then hands `procRequest` a view of that same memory sized `MIN_PACKET_SIZE - 1`. `Header::SIZE` is well below `MIN_PACKET_SIZE`, so every header byte is present and the only defect is the length \u2014 which is what makes this the interesting case rather than a malformed-header test. It asserts `BufferTooSmallForPacket` once with both arguments, `InvalidHeader` zero times, zero `compressChunk` calls, zero completion events, and an unchanged buffer size.\n\nNegative control: removing only the guard aborts the suite; restoring it returns 10/10. Under `FW_NO_ASSERT` the same removal instead hangs in the oversized deserializer loop (reproduced by @lntutor).\n\nEvent ids confirmed against the generated component base:\n\n```\nEVENTID_COMPRESSIONCOMPLETE     = 0x0\nEVENTID_DIDNOTCOMPRESS          = 0x1\nEVENTID_INVALIDHEADER           = 0x2\nEVENTID_BUFFERTOOSMALLFORPACKET = 0x3\n```\n\n## Future Work\n\nThe new event carries `throttle 10` to match `InvalidHeader` and `DidNotCompress`, but this component declares no commands, so there is no `CLEAR_EVENT_THROTTLE` to reset it. That is the existing behaviour of both current events rather than a regression, but adding a command felt out of scope here.\n\nA requirement row for the new behaviour (`SVC-DPCOMPRESSPROC-004`) could be added to `docs/sdd.md` if wanted; left out to keep the change small.\n\n## Same shape as the previously-merged #5262\n\n[nasa/fprime#5262](https://github.com/nasa/fprime/pull/5262) (`FileUplink::File::open`, merged June 2026) hardened an identical shape: a `memcpy` whose size relied on a `U8` narrowing in a different translation unit. Upstream defenses that live in another file are load-bearing but invisible when reading this one; a local check makes the invariant surface where the arithmetic happens.\n\n## Provenance\n\nSurfaced by the v0.16 scj-hunt `/scan` verb, a geometric anomaly detector run over a fiber bundle of 2959 functions from `Svc/`. `procRequest_handler` ranked #2 of 148 flagged rows. The finding was then read, reproduced and tested by hand before filing.\n\n## AI Usage (see [policy](https://github.com/nasa/fprime/blob/devel/AI_POLICY.md))\n\n<!-- -->\n",
  "files": [
    {
      "filename": "Svc/DpCompressProc/DpCompressProc.cpp",
      "patch": "@@ -53,6 +53,12 @@ void DpCompressProc ::procRequest_handler(FwIndexType portNum, Fw::Buffer& fwBuf\n         return;\n     }\n \n+    // Check that the buffer is large enough to hold a data product packet\n+    if (fwBuffer.getSize() < Fw::DpContainer::MIN_PACKET_SIZE) {\n+        this->log_WARNING_HI_BufferTooSmallForPacket(fwBuffer.getSize(), Fw::DpContainer::MIN_PACKET_SIZE);\n+        return;\n+    }\n+\n     Fw::DpContainer container(0, fwBuffer);\n     const Fw::SerializeStatus desStat = container.deserializeHeader();\n     if (desStat != Fw::FW_SERIALIZE_OK) {",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2FDpCompressProc.cpp"
    },
    {
      "filename": "Svc/DpCompressProc/DpCompressProc.fpp",
      "patch": "@@ -32,6 +32,14 @@ module Svc {\n             format \"Received buffer of size {}; deserialization of container header failed with error code {}\" \\\n             throttle 10\n \n+        @ Received buffer is too small to hold a data product packet\n+        event BufferTooSmallForPacket(buffer_size: FwSizeType @< The incoming buffer size\n+                                      min_size: FwSizeType @< The minimum required size\n+                                      ) \\\n+            severity warning high \\\n+            format \"Received buffer has size {}; minimum required size is {}\" \\\n+            throttle 10\n+\n         @ Record ID used to mark compressed records in a data product\n         product record CompressionRecord: U8 array\n ",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2FDpCompressProc.fpp"
    },
    {
      "filename": "Svc/DpCompressProc/docs/sdd.md",
      "patch": "@@ -71,6 +71,8 @@ Add requirements in the chart below\n |-----------------------|--------------------------------------------------------------------|\n | CompressionComplete   | DIAGNOSTIC event when DpCompressProc compresses a buffer           |\n | DidNotCompress        | ACTIVITY\\_LO event when DpCompressProc can not compress any chunks |\n+| InvalidHeader         | WARNING\\_HI event when the container header cannot be deserialized |\n+| BufferTooSmallForPacket | WARNING\\_HI event when the incoming buffer is smaller than `Fw::DpContainer::MIN_PACKET_SIZE` |\n \n \n ## Detailed Documentation",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2Fdocs%2Fsdd.md"
    },
    {
      "filename": "Svc/DpCompressProc/test/ut/DpCompressProcTestMain.cpp",
      "patch": "@@ -9,6 +9,10 @@\n #include \"STest/STest/Random/Random.hpp\"\n #include \"Svc/DpCompressProc/test/ut/Rules/Testers.hpp\"\n \n+TEST(OffNominal, BufferSmallerThanMinPacketSize) {\n+    Svc::Testers::procRequest.testState.test_undersized_buffer();\n+}\n+\n TEST(Nominal, Compressible) {\n     const FwSizeType chunk_size = 4096;\n     std::vector<Svc::AbstractState::Chunk> chunks = {Svc::AbstractState::Chunk(Svc::AbstractState::COMPRESSED, 0xA5),",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2Ftest%2Fut%2FDpCompressProcTestMain.cpp"
    },
    {
      "filename": "Svc/DpCompressProc/test/ut/DpCompressProcTester.cpp",
      "patch": "@@ -222,6 +222,45 @@ void DpCompressProcTester::test_chunks_helper(const FwSizeStoreType chunk_size,\n     abstractState.success_ = true;\n }\n \n+void DpCompressProcTester::test_undersized_buffer() {\n+    // Hand procRequest a view one byte below MIN_PACKET_SIZE into a larger,\n+    // well-formed allocation: every header byte is present, only the length is wrong.\n+    this->clearHistory();\n+    this->component.log_WARNING_HI_BufferTooSmallForPacket_ThrottleClear();\n+\n+    paramSet_CHUNK_SIZE(4096, Fw::ParamValid::VALID);\n+    paramSet_ENABLE(Fw::Enabled::ENABLED, Fw::ParamValid::VALID);\n+    this->component.loadParameters();\n+\n+    const FwSizeType backing_size = Fw::DpContainer::MIN_PACKET_SIZE + 1;\n+    U8* mem = new U8[backing_size]();\n+\n+    Fw::Buffer backing_buf(mem, backing_size);\n+    Fw::DpContainer container(0, backing_buf);\n+    container.setDataSize(1);\n+    container.serializeHeader();\n+\n+    // One byte short of the minimum\n+    const FwSizeType short_size = Fw::DpContainer::MIN_PACKET_SIZE - 1;\n+    Fw::Buffer short_buf(mem, short_size);\n+\n+    this->invoke_to_procRequest(0, short_buf);\n+\n+    // Rejected on size, not mistaken for a header deserialization failure\n+    ASSERT_EVENTS_BufferTooSmallForPacket_SIZE(1);\n+    ASSERT_EVENTS_BufferTooSmallForPacket(0, short_size, Fw::DpContainer::MIN_PACKET_SIZE);\n+    ASSERT_EVENTS_InvalidHeader_SIZE(0);\n+\n+    // Nothing forwarded downstream, no completion claimed.\n+    ASSERT_from_compressChunk_SIZE(0);\n+    ASSERT_EVENTS_CompressionComplete_SIZE(0);\n+\n+    // Buffer left untouched.\n+    ASSERT_EQ(short_buf.getSize(), short_size);\n+\n+    delete[] mem;\n+}\n+\n void DpCompressProcTester::test_chunks_disabled(const FwSizeStoreType chunk_size,\n                                                 std::vector<AbstractState::Chunk> chunks) {\n     Fw::Buffer container_buf = this->abstractState.build_compress_buffer(chunk_size, chunks);",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2Ftest%2Fut%2FDpCompressProcTester.cpp"
    },
    {
      "filename": "Svc/DpCompressProc/test/ut/DpCompressProcTester.hpp",
      "patch": "@@ -42,6 +42,9 @@ class DpCompressProcTester : public DpCompressProcGTestBase {\n \n     void test_chunks_disabled(const FwSizeStoreType chunk_size, std::vector<AbstractState::Chunk> chunks);\n \n+    //! Drive procRequest with a buffer shorter than MIN_PACKET_SIZE\n+    void test_undersized_buffer();\n+\n   public:\n     // ----------------------------------------------------------------------\n     // Tests",
      "blob_url": "https://github.com/nasa/fprime/blob/cb17b6c12f745bb4beacce78c1bf9c85503882ca/Svc%2FDpCompressProc%2Ftest%2Fut%2FDpCompressProcTester.hpp"
    }
  ],
  "reviews": [
    {
      "user": "lntutor",
      "state": "COMMENTED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4822163767"
    },
    {
      "user": "nurdymuny",
      "state": "COMMENTED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4823787395"
    },
    {
      "user": "lntutor",
      "state": "APPROVED",
      "body": "Re-reviewed at afe69bd. The new off-nominal case matches the requested repro and checks the InvalidHeader arguments, zero downstream calls, zero completion events, and unchanged buffer size. I built Svc_DpCompressProc_ut_exe locally and ran both the focused test and the full component suite (10/10 passed). I also temporarily removed only the guard and verified the focused test fails at the DpContainer size assertion, then restored the exact head and reran the suite successfully. Thanks for adding the regression coverage.",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4823867259"
    },
    {
      "user": "nurdymuny",
      "state": "COMMENTED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4824134059"
    },
    {
      "user": "LeStarch",
      "state": "CHANGES_REQUESTED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4848345654"
    },
    {
      "user": "LeStarch",
      "state": "APPROVED",
      "body": "",
      "url": "https://github.com/nasa/fprime/pull/5518#pullrequestreview-4849914681"
    }
  ],
  "comments": [
    {
      "user": "nurdymuny",
      "body": "All four addressed in `cb17b6c`.\n\n**Dedicated EVR.** You're right, and `Svc/DpWriter` had already answered this \u2014 it performs the identical `< Fw::DpContainer::MIN_PACKET_SIZE` check on the calling side and raises its own `BufferTooSmallForPacket` for it. This component was clearly derived from DpWriter (its `InvalidHeader` is DpWriter's, with \"packet header\" changed to \"container header\") and only half the pair came across. So I've taken DpWriter's event rather than invent one: same name, same format string, same `warning high`, same `throttle 10`.\n\n`InvalidHeader` keeps its own meaning \u2014 header deserialization failed \u2014 and the test now asserts `ASSERT_EVENTS_InvalidHeader_SIZE(0)` on the short-buffer path, so the two rejections are provably distinct rather than just differently named. The `FW_DESERIALIZE_SIZE_MISMATCH` argument is gone; it was a status code invented to fit the reused signature, which was the tell that the EVR was wrong.\n\nTwo small departures from DpWriter, both to match this file rather than that one: the arguments are `buffer_size`/`min_size` in snake_case (matching `InvalidHeader` two lines above) rather than DpWriter's camelCase, and `min_size` is `FwSizeType` rather than DpWriter's `U32`, since `MIN_PACKET_SIZE` is declared `FwSizeType` and it avoids the `static_cast<U32>` DpWriter needs. Happy to align either way if you'd rather have cross-component symmetry.\n\n**Comments.** Cut 48 lines across the handler, the tester and the test main. The handler comment is now DpWriter's one-liner verbatim. I also removed a 4-line comment on the `TEST` in `DpCompressProcTestMain.cpp` that you hadn't flagged \u2014 no other `TEST` in that file carries one, so it was the same finding. The underflow analysis belongs in this PR description, not in the source; I've moved it there.\n\n**Verification.** `fprime-util check Svc/DpCompressProc`: 10/10 pass. Ran the negative control again since the emit path changed \u2014 removing only the guard aborts the suite (`Fw::defaultSwAssert`, 0% passed); restoring it returns 10/10.\n\nChecked the event IDs explicitly, since appending in the wrong place would silently renumber:\n\n```\nEVENTID_COMPRESSIONCOMPLETE     = 0x0\nEVENTID_DIDNOTCOMPRESS          = 0x1\nEVENTID_INVALIDHEADER           = 0x2   <- unchanged\nEVENTID_BUFFERTOOSMALLFORPACKET = 0x3   <- new\n```\n\n**Docs.** `docs/sdd.md` gains rows for the new event and for `InvalidHeader`, which was already missing from that table before this PR.\n\nOne thing worth flagging rather than leaving for you to find: the new event carries `throttle 10` to match `InvalidHeader` and `DidNotCompress`, but this component declares no commands, so there is no `CLEAR_EVENT_THROTTLE` to reset it. After ten undersized buffers it goes quiet for the rest of the mission. That's the existing behaviour of both current events, so this isn't a regression, but I didn't want to add a command in this PR without asking.\n",
      "url": "https://github.com/nasa/fprime/pull/5518#issuecomment-5172189690"
    }
  ],
  "review_comments": [
    {
      "user": "lntutor",
      "body": "Could you add an off-nominal unit test for this exact guard? I reproduced the path in a true FW_NO_ASSERT build by serializing a header with dataSize = 1 into a MIN_PACKET_SIZE + 1 backing array, then shrinking the Fw::Buffer view to MIN_PACKET_SIZE - 1 before invoking procRequest. With this guard removed, the test hangs in the oversized deserializer loop; with the guard present, it returns immediately, emits InvalidHeader(buffer_size, FW_DESERIALIZE_SIZE_MISMATCH), and makes zero compressChunk calls. The existing 9 DpCompressProc tests all construct valid containers, so they pass without exercising this new branch. Since this failure mode is the reason for the change, checking those event arguments and zero downstream calls would make the fix regression-safe.",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3685361774"
    },
    {
      "user": "nurdymuny",
      "body": "Added in `afe69bd`. Thank you for the repro \u2014 I used your construction exactly.\n\n`DpCompressProcTester::test_undersized_buffer` allocates a `MIN_PACKET_SIZE + 1` backing array, writes a well-formed header with `dataSize = 1`, then hands `procRequest` a view of that same memory sized `MIN_PACKET_SIZE - 1`. As you noted, `Header::SIZE` is well below `MIN_PACKET_SIZE`, so the header is fully present and the only thing wrong with the buffer is its length \u2014 which is what makes this the interesting case rather than a plain malformed-header test.\n\nAsserts, matching what you checked:\n\n- `InvalidHeader` emitted once, with the size we were handed and `FW_DESERIALIZE_SIZE_MISMATCH`\n- zero `compressChunk` calls\n- zero `CompressionComplete` events\n- buffer size unchanged\n\nYou're right that the existing nine all construct valid containers \u2014 I confirmed none of them reach the new branch, so it was going in untested.\n\nTwo notes:\n\n**Placement.** I put the body on the Tester rather than in the `TEST` block because `paramSet_*` isn't publicly accessible from outside the harness; this matches the existing `test_chunks` / `test_chunks_disabled` idiom. The `TEST` is a one-liner that calls it.\n\n**Verification.** I should be straight with you: I could not build locally \u2014 this machine has no `cmake`/`fprime-util` and I didn't want to sit on the review while I stood up a toolchain. I verified statically instead: every symbol used is one already used in this repo (`ASSERT_from_<port>_SIZE`, `ASSERT_EVENTS_<Event>(idx, args\u2026)`, and `log_WARNING_HI_<Event>_ThrottleClear()` against other components' tests; the event signature, `warning high` severity, and `compressChunk` port name against the FPP; `invoke_to_procRequest` / `paramSet_*` / `setDataSize` / `serializeHeader` against this file and `AbstractState.cpp`). So I'm relying on CI for the compile. If an assertion name or argument order is off I'll fix it immediately \u2014 and if you'd rather I get a local build going and confirm the without-guard failure myself before you spend more time on it, say the word and I will.\n\nOne thing worth flagging since you ran it under `FW_NO_ASSERT`: the test is protective in both configurations, but it fails *differently*. With the guard removed, `FW_NO_ASSERT` hangs in the oversized deserializer loop (what you saw), while an assert-enabled build aborts earlier in the `Fw::DpContainer` constructor's own `FW_ASSERT` \u2014 since the guard runs before construction. Either way removing the guard fails the test, but the assert-enabled failure is an abort rather than the hang, which is worth knowing if it ever trips in CI.\n",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3686713668"
    },
    {
      "user": "nurdymuny",
      "body": "Following up on my note that I hadn't built this locally \u2014 I have now, and the test is verified end to end.\n\nStood up the toolchain (venv from `requirements.txt`, so `fprime-util` 4.3.0a5 / `fprime-fpp` 3.3.0a12 / cmake 3.26.0) and ran `fprime-util check` in `Svc/DpCompressProc`:\n\n```\n[==========] Running 10 tests from 2 test suites.\n[----------] 1 test from OffNominal\n[ RUN      ] OffNominal.BufferSmallerThanMinPacketSize\n[       OK ] OffNominal.BufferSmallerThanMinPacketSize (0 ms)\n[----------] 9 tests from Nominal\n...\n[  PASSED  ] 10 tests.\n```\n\nYour nine still pass, so no regression.\n\nI also ran the negative control, since a test that passes with and without the fix isn't worth much. Reverting `DpCompressProc.cpp` to the pre-fix version and rebuilding:\n\n```\nexit code: 1\nSvc_DpCompressProc_ut_exe: Fw/Types/Assert.cpp:128:\n  Fw::defaultSwAssert(...): Assertion `false' failed.\n```\n\nRestoring the guard puts it back to `[  PASSED  ] 10 tests.` (exit 0). So the test genuinely pins the new behavior.\n\nOne correction to what I speculated earlier, now that I've actually observed it. I'd said the assert-enabled failure would come from `Fw::DpContainer`'s own `FW_ASSERT` firing before ours. The abort does happen in `Fw::defaultSwAssert` as predicted, but I'd treat the specific origin as unconfirmed \u2014 I didn't instrument which assertion trips first, only that the suite aborts. The load-bearing part holds either way: **remove the guard and this test fails in an assert-enabled build, and hangs in the `FW_NO_ASSERT` build you exercised.** Two different symptoms, same protection.\n\nHappy to add a second case if you want the `FW_NO_ASSERT` path pinned explicitly rather than implied \u2014 though that needs the suite built with assertions disabled, which I don't think this component's CI config currently does.\n",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3687012982"
    },
    {
      "user": "LeStarch",
      "body": "We should not reuse this EVR.  We should specifically state that the buffer was too short.",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3707501137"
    },
    {
      "user": "LeStarch",
      "body": "This comment is way to long.  The invariant being enforced is correct, so no need for a long comment.",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3707506767"
    },
    {
      "user": "LeStarch",
      "body": "Again a long comment is unnecessary here.",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3707510243"
    },
    {
      "user": "LeStarch",
      "body": "Fix this event to be a new dedicated event.",
      "url": "https://github.com/nasa/fprime/pull/5518#discussion_r3707511875"
    }
  ]
}
