Fail POP_SERIALIZABLE instead of asserting on a downstream deserialize mismatch - #5869
Moferanoluwa wants to merge 2 commits into
Conversation
…e mismatch Fixes nasa#5859. popSerializable_directiveHandler() FW_ASSERTed on the status returned by serialOut_out(). When serialOut[portIndex] is connected to a typed input port, that status also reports whether the popped bytes deserialized into that port's arguments, so a sequence that pops a size too small for the connected port's type crashed the process instead of failing the directive the way an invalid port index or an unconnected port already do. Added DirectiveErrorCode::SERIAL_PORT_WRITE_FAILURE and return stmtResponse_failure with it when serialOut_out() reports anything other than FW_SERIALIZE_OK, mirroring the existing SERIAL_PORT_NOT_CONNECTED / SERIAL_PORT_INVALID_INDEX checks in the same handler. Testing: added popSerializable_typedPortDeserializeMismatch, which connects serialOut[2] (otherwise unused by these tests) to a real Fw::InputTimePort instead of the harness's generic serial capture -- via the same Fw::InputPortBase-based registerSerialPort() connection WasmSequencerTester already uses for a different purpose -- so popping 1 byte against Fw::Time's multi-byte serialized size exercises a genuine FW_DESERIALIZE_SIZE_MISMATCH from the port's own autocoded deserialization, not a simulated one. Verified via a WSL build (this dev machine can't natively build F'): all 300 Svc_FpySequencer_ut_exe tests pass, including the new one. AI-assisted development (Claude Code) — I traced the bug to the specific assert, designed and implemented the fix and the typed-port test, and verified the build and full test suite in a WSL environment myself.
|
Hello @Moferanoluwa, and thank you for contributing to F´! This pull request is signed At the moment the linked issue(s) have not yet been approved by the CCB: #5859 (CCB Resolution: not yet set). Recommendation to maintainers: close this PR for now, and reopen it once the CCB has weighed in on the linked issue(s). If the CCB approves, the work here can be picked back up immediately. This is an automated notice from |
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | ||
| deserialize into that port's arguments (e.g. `size` too small for the connected type): | ||
| `SERIAL_PORT_WRITE_FAILURE` |
There was a problem hiding this comment.
[Documentation] suggestion New error path leaves the bytes on the stack, but the doc does not say so.
With the assert replaced by a directive failure, SERIAL_PORT_WRITE_FAILURE is the only error that fires after the Semantics list says the bytes were popped (step 3 "Pop" precedes step 4 "Send"), yet the handler only decrements stack.size after serialOut_out() succeeds (the new UT asserts stack.size unchanged). A reader of the Semantics list would infer the data is consumed on this failure. State the stack effect on the new bullet (or reorder steps 3/4 to send-then-pop).
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | |
| deserialize into that port's arguments (e.g. `size` too small for the connected type): | |
| `SERIAL_PORT_WRITE_FAILURE` | |
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | |
| deserialize into that port's arguments (e.g. `size` too small for the connected type): | |
| `SERIAL_PORT_WRITE_FAILURE`. The stack is left unchanged; the `size` bytes are not popped. |
| Fw::InputTimePort typedPort; | ||
| typedPort.init(); | ||
| typedPort.addCallComp(&this->cmp, &unusedTimePortCallback); | ||
| this->component.set_serialOut_OutputPort(2, &typedPort); |
There was a problem hiding this comment.
[Maintainability] suggestion maint-inconsistent-local-convention: this->component here vs this->cmp one line above for the same object.
component is the compatibility alias (FpySequencerTester.hpp:57); every other reference in this file and FpySequencerTester.cpp uses cmp. Two names for one object in adjacent lines makes a reader check whether they differ.
| this->component.set_serialOut_OutputPort(2, &typedPort); | |
| this->cmp.set_serialOut_OutputPort(2, &typedPort); |
|
|
||
| namespace { | ||
| // Only invoked if deserialization succeeds; the mismatch test below is constructed so it never is | ||
| void unusedTimePortCallback(Fw::PassiveComponentBase* callComp, FwIndexType portNum, Fw::Time& time) { |
There was a problem hiding this comment.
[Maintainability] could fix maint-unclear-naming: unusedTimePortCallback is used — it is registered via addCallComp and fails the test if it ever runs.
"unused" reads as dead code to the next engineer scanning the file (candidate for deletion), when the name should say the callback is expected not to fire. Rename at the definition and the addCallComp call site.
| void unusedTimePortCallback(Fw::PassiveComponentBase* callComp, FwIndexType portNum, Fw::Time& time) { | |
| void failIfInvokedTimePortCallback(Fw::PassiveComponentBase* callComp, FwIndexType portNum, Fw::Time& time) { |
| // Call output port. When connected to a typed input port, this status also reflects whether | ||
| // the payload deserialized into that port's arguments, which is untrusted sequence content | ||
| // (e.g. a size too small for the connected port's type) rather than a condition to assert on. |
There was a problem hiding this comment.
[Maintainability] could fix maint-misleading-comment: three-line rationale comment where the file's sibling checks use one line each.
The why-not-assert rationale already lives in the PR/issue (#5859); inline comments in this handler are one-liners. Keep it to what a reader needs at the call site.
| // Call output port. When connected to a typed input port, this status also reflects whether | |
| // the payload deserialized into that port's arguments, which is untrusted sequence content | |
| // (e.g. a size too small for the connected port's type) rather than a condition to assert on. | |
| // Call output port; a typed downstream port reports deserialize failures here, which is | |
| // untrusted sequence content (e.g. undersized payload), not an invariant to assert on |
| // Port 2 is otherwise unused by these tests. Connect it to a genuine typed input port instead | ||
| // of the harness's generic serial capture, so an undersized payload triggers a real deserialize | ||
| // mismatch the way a typed serialOut connection does in a real deployment (nasa/fprime#5859), | ||
| // rather than the FW_ASSERT this used to hit. |
There was a problem hiding this comment.
[Maintainability] could fix maint-misleading-comment: four-line comment; the last clause ("rather than the FW_ASSERT this used to hit") describes removed behavior and goes stale immediately.
History belongs in the PR/issue; the comment should state only the test setup rationale.
| // Port 2 is otherwise unused by these tests. Connect it to a genuine typed input port instead | |
| // of the harness's generic serial capture, so an undersized payload triggers a real deserialize | |
| // mismatch the way a typed serialOut connection does in a real deployment (nasa/fprime#5859), | |
| // rather than the FW_ASSERT this used to hit. | |
| // Port 2 is unused by other tests. Connect it to a real typed input port (not the harness's | |
| // serial capture) so an undersized payload produces a genuine deserialize mismatch (#5859). |
| CMD_FAIL = 17 | ||
| SERIAL_PORT_NOT_CONNECTED = 18 | ||
| SERIAL_PORT_INVALID_INDEX = 19 | ||
| SERIAL_PORT_WRITE_FAILURE = 20 |
There was a problem hiding this comment.
[Design] could fix SERIAL_PORT_WRITE_FAILURE names the wrong side of the failure.
The only non-OK status serialOut_out can return is the receiving typed port's deserialize status (InputSerializePort::invokeSerial always returns OK), so this code always means "payload did not deserialize into the connected port's arguments", as docs/directives.md describes. "Write failure" reads as a transport/send error. Since Fpy.DirectiveErrorCode is ground-visible through LastDirectiveError telemetry and renaming later is a dictionary break, consider a name that matches the sibling CMD_SERIALIZE_FAILURE, e.g. SERIAL_PORT_DESERIALIZE_FAILURE (update the .cpp, docs, and UT together).
There was a problem hiding this comment.
[Operational] Concur — also in scope for operational consequences: an operator seeing LastDirectiveError = 20 ("WRITE_FAILURE") will triage the port/transport when the cause is a sequence-payload mismatch; renaming after dictionary release is a ground-tool break.
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | ||
| deserialize into that port's arguments (e.g. `size` too small for the connected type): | ||
| `SERIAL_PORT_WRITE_FAILURE` |
There was a problem hiding this comment.
[Operational] could fix ops-doc-reality: only the undersized-size half of the mismatch space is caught; an oversized size is delivered silently.
The generated typed-port deserializePortArgs stops after the last argument and never checks for leftover bytes, so size > the connected type's serialized width returns FW_SERIALIZE_OK, the handler pops the full size, and the connected component receives a value built from the first bytes of the range with no error, event, or telemetry change. Concrete case: Fw::InputTimePort (11 bytes) with size = 15 sends a Fw::Time decoded from 4 stray bytes plus the first 7 of the intended value. Operational judgment call: the remedy is a doc statement of the worst case so sequence authors do not read this bullet as full validation.
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | |
| deserialize into that port's arguments (e.g. `size` too small for the connected type): | |
| `SERIAL_PORT_WRITE_FAILURE` | |
| - If `serialOut[port_index]` is connected to a typed input port and the popped bytes fail to | |
| deserialize into that port's arguments (`size` too small for the connected type): | |
| `SERIAL_PORT_WRITE_FAILURE`. A `size` larger than the connected type is **not** detected: the | |
| leading bytes are deserialized, the trailing bytes are discarded, and all `size` bytes are popped. |
lestarch-autobot
left a comment
There was a problem hiding this comment.
Automated review summary (run 1)
Per-agent results
| Agent | must fix | suggestion | could fix | future work | outstanding | Verdict |
|---|---|---|---|---|---|---|
| Security Vulnerabilities | 0 | 0 | 0 | 0 | 0 | Go |
| Supply Chain / Runner Safety | 0 | 0 | 0 | 0 | 0 | Go |
| F Prime C/C++ Design | 0 | 0 | 0 | 0 | 0 | Go |
| Documentation Currency | 0 | 1 | 0 | 0 | 1 | Go |
| Design | 0 | 0 | 1 | 0 | 1 | Go |
| Architecture | 0 | 0 | 0 | 0 | 0 | Go |
| Test Quality | 0 | 0 | 0 | 0 | 0 | Go |
| Correctness | 0 | 0 | 0 | 0 | 0 | Go |
| Operational | 0 | 0 | 2 | 0 | 2 | Go |
| Maintainability | 0 | 1 | 3 | 0 | 4 | Go |
| CI safety | — | — | — | — | — | Go |
| Totals | 0 | 2 | 6 | 0 | 8 | Go |
Supply-chain surfaces
| Surface | Outstanding |
|---|---|
| Dependencies | clean |
| Vendored / submodule | clean |
| Build / test infrastructure | clean |
| Workflows / actions / scripts | clean |
| Generator output | clean |
| Prompt-injection | clean |
| Review-system integrity | clean |
Merge readiness
Merge readiness: Go — all ten reviewers completed with zero outstanding must-fix findings; 8 non-blocking items (2 suggestion, 6 could-fix) remain open for the author's consideration.
Assert swapped for a graceful directive failure — a small course correction that keeps the sequencer flying. Nice work.
Coverage report — base
|
| Module | Line | Δ | Function | Δ | Branch | Δ |
|---|---|---|---|---|---|---|
Svc/FprimeFramer |
86.49 | -13.51 | 100.00 | +0.00 | 72.22 | -22.90 |
Svc/Ccsds/SpacePacketFramer |
86.36 | -11.56 | 100.00 | +0.00 | 69.23 | -19.41 |
Svc/DpCatalog |
74.25 | -7.64 | 96.97 | +0.00 | 63.96 | -7.63 |
Os/Posix |
64.16 | -6.48 | 85.26 | -2.24 | 45.69 | -4.98 |
Utils |
44.07 | -2.61 | 42.86 | -2.85 | 47.14 | -2.30 |
Svc/ComAggregator |
95.71 | -2.46 | 93.75 | -2.40 | 96.00 | +9.75 |
Svc/FprimeDeframer |
98.15 | -1.85 | 100.00 | +0.00 | 95.92 | -4.08 |
Modules changed
| Module | Line | Δ | Function | Δ | Branch | Δ |
|---|---|---|---|---|---|---|
Svc/FprimeFramer |
86.49 | -13.51 | 100.00 | +0.00 | 72.22 | -22.90 |
Svc/Ccsds/SpacePacketFramer |
86.36 | -11.56 | 100.00 | +0.00 | 69.23 | -19.41 |
Svc/DpCatalog |
74.25 | -7.64 | 96.97 | +0.00 | 63.96 | -7.63 |
Os/Posix |
64.16 | -6.48 | 85.26 | -2.24 | 45.69 | -4.98 |
Utils |
44.07 | -2.61 | 42.86 | -2.85 | 47.14 | -2.30 |
Svc/ComAggregator |
95.71 | -2.46 | 93.75 | -2.40 | 96.00 | +9.75 |
Svc/FprimeDeframer |
98.15 | -1.85 | 100.00 | +0.00 | 95.92 | -4.08 |
Os/Generic/Types |
91.79 | -0.35 | 92.86 | +0.00 | 73.83 | -0.94 |
Svc/WasmSequencer |
97.41 | -0.34 | 100.00 | +0.00 | 91.55 | -0.62 |
Os |
19.58 | -0.19 | 22.48 | +0.03 | 17.40 | -0.01 |
Svc/ComQueue |
97.99 | -0.18 | 100.00 | +0.00 | 88.51 | -2.84 |
Svc/Ccsds/TcDeframer |
94.92 | -0.16 | 100.00 | +0.00 | 86.79 | -0.48 |
Fw/Types |
55.44 | -0.11 | 60.69 | +0.00 | 34.70 | -0.73 |
Svc/FpySequencer |
86.32 | -0.08 | 98.59 | +0.00 | 77.68 | -0.06 |
Svc/PrmDb |
93.00 | -0.07 | 94.74 | +0.00 | 88.80 | -0.12 |
Svc/Version |
96.23 | -0.02 | 100.00 | +0.00 | 86.96 | -2.33 |
Os/Generic |
90.34 | +0.10 | 89.13 | +0.00 | 75.29 | +0.19 |
New modules
| Module | Line | Function | Branch |
|---|---|---|---|
Fw/Prm |
0.00 | 0.00 | 0.00 |
Modules without UTs
CFDP/Checksum/GTest, Drv/LinuxGpioDriver, Drv/LinuxI2cDriver, Drv/LinuxSpiDriver, Drv/Ports/DataTypes, Drv/PosixUartDriver, FppTestProject/FppTest/topology/async, FppTestProject/FppTest/topology/components/Comp, FppTestProject/FppTest/topology/components/Framework, FppTestProject/FppTest/topology/components/Receiver, FppTestProject/FppTest/topology/components/Sender, FppTestProject/FppTest/topology/guarded, FppTestProject/FppTest/topology/sync, FppTestProject/FppTest/topology/top_ports, FppTestProject/FppTest/topology/types, Fw/Com, Fw/Comp, Fw/FilePacket/GTest, Fw/Fpy, Fw/Obj, Fw/Port, Fw/Sm, Fw/Test, Fw/Types/GTest, Os/Models, Svc/Ccsds/Types, Svc/FatalHandler, Svc/Subtopologies/CdhCore, Svc/Subtopologies/ComCcsds, Svc/Subtopologies/ComCcsdsSdls, Svc/Subtopologies/ComFprime, Svc/Subtopologies/ComLoggerTee, Svc/Subtopologies/DataProducts, Svc/Subtopologies/DpCompression, Svc/Subtopologies/FileHandling, Svc/Subtopologies/FileHandlingCfdp, Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig, TestDeploymentsProject/Ref/DpDemo, TestDeploymentsProject/Ref/PingReceiver, TestDeploymentsProject/Ref/RecvBuffApp, TestDeploymentsProject/Ref/SendBuffApp, TestDeploymentsProject/Ref/Top, TestDeploymentsProject/Ref/TypeDemo, cmake/test/data/TestConfigDeployment, cmake/test/data/TestDeployment/TestBuildAutocoder, cmake/test/data/TestDeployment/TestHeaderAutocoder, cmake/test/data/TestDeployment/TestLinkDepends, cmake/test/data/TestDeployment/TestRelative, cmake/test/data/test-fprime-library/TestLibrary/TestComponent, cmake/test/data/test-fprime-library2/TestLibrary2/TestComponent
Renamed SERIAL_PORT_WRITE_FAILURE -> SERIAL_PORT_DESERIALIZE_FAILURE. Fw::InputSerializePort::invokeSerial always returns FW_SERIALIZE_OK (it says so in its own comment), so the only non-OK status serialOut_out can produce is a receiving typed port's deserialize status. "Write failure" would send an operator triaging LastDirectiveError after the transport rather than the sequence payload, and the code is ground-visible, so renaming after a dictionary release would be a ground-tool break. directives.md: - Reordered the Semantics list to send-then-pop, matching the handler. The old list claimed the pop happened first, which would have implied the bytes were consumed on this new failure path; they are not. - Documented that an oversized `size` is not detected. A typed port's generated deserializePortArgs stops after its last argument and never checks for leftover bytes, so the send succeeds, all `size` bytes pop, and the component decodes from the leading bytes of a window that an oversized `size` has shifted down the stack. Test/comment cleanups: renamed unusedTimePortCallback -> failIfInvokedTimePortCallback (it is registered and fails the test if it runs), used cmp rather than the component alias, and trimmed two comments that carried PR history rather than call-site rationale. Svc_FpySequencer_ut_exe: 300/300 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks @zimri-leisher for the approval, and thanks for the thorough automated pass — all 8 non-blocking items are addressed in 894c176. Two of them were substantive enough that I'd rather take them now than leave them. Renamed // The normal input ports perform deserialize() on the passed buffer,
// which is what this status is based on. This is not the case for the
// InputSerializePort, so just return an okay status
return FW_SERIALIZE_OK;So a serial→serial connection can never produce a non-OK status; the only source is a receiving typed port's failed deserialize. "Write failure" would point an operator triaging Documented the oversized- Also fixed (Documentation + Maintainability): reordered the Semantics list to send-then-pop so it matches the handler — the old list implied the bytes were consumed on this failure path, which they aren't; renamed
On the coverage report — the flagged regressions are in 🤖 Generated with Claude Code |
lestarch-autobot
left a comment
There was a problem hiding this comment.
Automated review summary (run 2)
Per-agent results
| Agent | must fix | suggestion | could fix | future work | outstanding | Verdict |
|---|---|---|---|---|---|---|
| Security Vulnerabilities | 0 | 0 | 0 | 0 | 0 | Go |
| Supply Chain / Runner Safety | 0 | 0 | 0 | 0 | 0 | Go |
| F Prime C/C++ Design | 0 | 0 | 0 | 0 | 0 | Go |
| Documentation Currency | 0 | 1 | 0 | 0 | 0 | Go |
| Design | 0 | 0 | 1 | 0 | 0 | Go |
| Architecture | 0 | 0 | 0 | 0 | 0 | Go |
| Test Quality | 0 | 0 | 0 | 0 | 0 | Go |
| Correctness | 0 | 0 | 0 | 0 | 0 | Go |
| Operational | 0 | 0 | 2 | 0 | 0 | Go |
| Maintainability | 0 | 1 | 3 | 0 | 0 | Go |
| CI safety | — | — | — | — | — | Go |
| Totals | 0 | 2 | 6 | 0 | 0 | Go |
Since last run
| Agent | resolved | still open | newly added | incorrect-fix follow-ups | improperly resolved | disagreements escalated |
|---|---|---|---|---|---|---|
| Security Vulnerabilities | 0 | 0 | 0 | 0 | 0 | 0 |
| Supply Chain / Runner Safety | 0 | 0 | 0 | 0 | 0 | 0 |
| F Prime C/C++ Design | 0 | 0 | 0 | 0 | 0 | 0 |
| Documentation Currency | 1 | 0 | 0 | 0 | 0 | 0 |
| Design | 1 | 0 | 0 | 0 | 0 | 0 |
| Architecture | 0 | 0 | 0 | 0 | 0 | 0 |
| Test Quality | 0 | 0 | 0 | 0 | 0 | 0 |
| Correctness | 0 | 0 | 0 | 0 | 0 | 0 |
| Operational | 2 | 0 | 0 | 0 | 0 | 0 |
| Maintainability | 4 | 0 | 0 | 0 | 0 | 0 |
Duplicates consolidated this run: 0 (threads closed by the §5h post-pass)
Supply-chain surfaces
| Surface | Outstanding |
|---|---|
| Dependencies | clean |
| Vendored / submodule | clean |
| Build / test infrastructure | clean |
| Workflows / actions / scripts | clean |
| Generator output | clean |
| Prompt-injection | clean |
| Review-system integrity | clean |
Merge readiness
Merge readiness: Go — all ten reviewers completed with zero outstanding must-fix findings; all 8 prior non-blocking items were addressed in 894c176 (each thread carries the reviewer's "Fixed in" reply; threads remain visually open only because the review token cannot resolve them).
Every open item cleared on the second pass — the sequencer is go for docking with devel.
Change Description
FpySequencer::popSerializable_directiveHandler()FW_ASSERTed on the status returned byserialOut_out():When
serialOut[portIndex]is connected to a typed input port, that status also reports whether the popped bytes deserialized into that port's arguments. APOP_SERIALIZABLEwith asizetoo small for the connected port's type is untrusted sequence content, not an internal invariant violation, so it should fail the directive the same way the handler's existingSERIAL_PORT_NOT_CONNECTED/SERIAL_PORT_INVALID_INDEXchecks do — instead of aborting the process.Fpy::DirectiveErrorCode::SERIAL_PORT_WRITE_FAILURE.popSerializable_directiveHandler()now checksportStatusand returnsSignal::stmtResponse_failurewith that error instead of asserting.docs/directives.md'sPOP_SERIALIZABLEerror-conditions list.Rationale
Fixes #5859. A ground-supplied sequence controls both the
port_indexandsizearguments toPOP_SERIALIZABLE; a size mismatch against whatever type is connected on the flight side is exactly the kind of bad input the sequencer is expected to reject with a directive error, not something that should be able to bring down the process.Testing/Review Recommendations
Added
popSerializable_typedPortDeserializeMismatchtoSvc/FpySequencer/test/ut. To exercise a genuine deserialize mismatch (rather than simulate the status), it connectsserialOut[2](unused by any other test in this suite) to a realFw::InputTimePortviaFw::OutputPortBase::registerSerialPort()— the same connection mechanismWasmSequencerTesteralready uses elsewhere for a different purpose — instead of the harness's generic serial-capture stub. Popping 1 byte againstFw::Time's multi-byte serialized size then hits the port's own autocodedFW_DESERIALIZE_SIZE_MISMATCH, not a stand-in value.This dev environment can't natively build F' (no Windows platform support in the CMake build), so I verified in WSL Ubuntu: all 300
Svc_FpySequencer_ut_exetests pass, including the new one, and the existingpopSerializable_success/popSerializable_portIndexOutOfBounds/popSerializable_portNotConnected/popSerializable_stackUnderflow/popSerializable_multipleTypes/popSerializable_differentPortstests are unaffected.Future Work
None.
AI Usage (see policy)
FpySequencerDirectives.cpp,FpySequencerTypes.fpp(new enum value),docs/directives.md, and the new unit test.IAMAI