Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Svc/FpySequencer/FpySequencerDirectives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1791,9 +1791,13 @@ Signal FpySequencer::popSerializable_directiveHandler(const FpySequencer_PopSeri
Fw::SerializeStatus stat = buf.setBuffLen(directive.get_size());
FW_ASSERT(stat == Fw::SerializeStatus::FW_SERIALIZE_OK, static_cast<FwAssertArgType>(stat));

// Call output port and verify serialization succeeds
// 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
Fw::SerializeStatus portStatus = this->serialOut_out(portIndex, buf);
FW_ASSERT(portStatus == Fw::SerializeStatus::FW_SERIALIZE_OK, static_cast<FwAssertArgType>(portStatus));
if (portStatus != Fw::SerializeStatus::FW_SERIALIZE_OK) {
error = DirectiveError::SERIAL_PORT_DESERIALIZE_FAILURE;
return Signal::stmtResponse_failure;
}

// Pop data from stack
this->m_runtime.stack.size -= directive.get_size();
Expand Down
1 change: 1 addition & 0 deletions Svc/FpySequencer/FpySequencerTypes.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ module Svc {
CMD_FAIL = 17
SERIAL_PORT_NOT_CONNECTED = 18
SERIAL_PORT_INVALID_INDEX = 19
SERIAL_PORT_DESERIALIZE_FAILURE = 20
}

@ Maximum length for argument or type names in arg_specs
Expand Down
18 changes: 16 additions & 2 deletions Svc/FpySequencer/docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -1148,13 +1148,27 @@ Pops `size` bytes of serialized data from the stack and sends them to an externa
**Semantics:**
1. Validate the port index is within bounds.
2. Check that the specified port is connected.
3. Pop `size` bytes from the stack.
4. Send the popped bytes to the target component via `serialOut[port_index]`.
3. Check that the stack holds at least `size` bytes.
4. Send the top `size` bytes of the stack to the target component via `serialOut[port_index]`.
5. Pop those `size` bytes from the stack. The bytes are popped only once the send has succeeded,
so a failure in step 4 leaves the stack unchanged.

**Error Conditions:**
- If `port_index >= MAX_SERIAL_PORTS`: `SERIAL_PORT_INVALID_INDEX`
- If `serialOut[port_index]` is not connected: `SERIAL_PORT_NOT_CONNECTED`
- If `len(stack) < size`: `STACK_UNDERFLOW`
- If `serialOut[port_index]` is connected to a typed input port and the bytes fail to deserialize
into that port's arguments (`size` too small for the connected type):
`SERIAL_PORT_DESERIALIZE_FAILURE`. The bytes are left on the stack.

> [!WARNING]
> A `size` *larger* than the connected type is **not** detected. A typed port's generated
> `deserializePortArgs` stops after its last argument and never checks for leftover bytes, so the
> send returns success, all `size` bytes are popped, and the component receives a value decoded
> from the *leading* bytes of the popped range. Because the range is taken as the top `size` bytes
> of the stack, an oversized `size` shifts the window down and the value is built from whatever
> preceded the intended one. Sequence authors must size this argument to match the connected
> port's type exactly; this directive validates only the undersized case.

| Arg Name | Arg Type | Source | Description |
|--------------|----------------|------------|-------------|
Expand Down
28 changes: 28 additions & 0 deletions Svc/FpySequencer/test/ut/FpySequencerTestMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <cmath>
#include "FpySequencerTester.hpp"
#include "Fw/Com/ComPacket.hpp"
#include "Fw/Time/TimePortAc.hpp"
#include "Fw/Types/MallocAllocator.hpp"
#include "Os/FileSystem.hpp"
#include "Svc/FpySequencer/FppConstantsAc.hpp"
Expand All @@ -16,6 +17,13 @@ using Signal = FpySequencer_SequencerStateMachineStateMachineBase::Signal;
using State = FpySequencer_SequencerStateMachineStateMachineBase::State;
using DirectiveError = Fpy::DirectiveErrorCode;

namespace {
// Registered below; deserialization is expected to fail before this can run
void failIfInvokedTimePortCallback(Fw::PassiveComponentBase* callComp, FwIndexType portNum, Fw::Time& time) {
FAIL() << "Time port callback should not run when the payload is too small to deserialize";
}
} // namespace

TEST_F(FpySequencerTester, waitRel) {
FpySequencer_WaitRelDirective directive{};
Fw::Time testTime(100, 100);
Expand Down Expand Up @@ -5606,6 +5614,26 @@ TEST_F(FpySequencerTester, popSerializable_stackUnderflow) {
ASSERT_EQ(tester_get_m_runtime_ptr()->stack.size, 4); // Stack unchanged
}

TEST_F(FpySequencerTester, popSerializable_typedPortDeserializeMismatch) {
// 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).
Fw::InputTimePort typedPort;
typedPort.init();
typedPort.addCallComp(&this->cmp, &failIfInvokedTimePortCallback);
this->cmp.set_serialOut_OutputPort(2, &typedPort);

// Fw::Time's serialized size is well over 1 byte, so 1 byte fails to deserialize into it
tester_push<U8>(0xAB);

FpySequencer_PopSerializableDirective directive(2, 1);
DirectiveError err = DirectiveError::NO_ERROR;
Signal result = tester_popSerializable_directiveHandler(directive, err);

ASSERT_EQ(result, Signal::stmtResponse_failure);
ASSERT_EQ(err, DirectiveError::SERIAL_PORT_DESERIALIZE_FAILURE);
ASSERT_EQ(tester_get_m_runtime_ptr()->stack.size, 1); // Stack unchanged; nothing was popped
}

TEST_F(FpySequencerTester, popSerializable_multipleTypes) {
// Test popping different types - push all data first, then pop one at a time

Expand Down