Skip to content

fix(Ccsds/AosDeframer): bound untrusted packet size before allocation#5145

Open
datqm wants to merge 4 commits into
nasa:develfrom
datqm:fix/aosdeframer-bound-packet-size
Open

fix(Ccsds/AosDeframer): bound untrusted packet size before allocation#5145
datqm wants to merge 4 commits into
nasa:develfrom
datqm:fix/aosdeframer-bound-packet-size

Conversation

@datqm
Copy link
Copy Markdown
Contributor

@datqm datqm commented May 12, 2026

Related Issue(s) (none - addresses FIXME comment in source)
Has Unit Tests (y/n) y
Documentation Included (y/n) n
Generative AI was used in this contribution (y/n) y (code generation, testing)

Change Description

AosDeframer::appendToSpanningPacket reads packetSize from the wire
(via sizePacket() applied to the spanning-packet header) and passes
it directly to allocate_out(). The current source explicitly flags
this with a FIXME at line 316 of Svc/Ccsds/AosDeframer/AosDeframer.cpp:

// FIXME: packetSize is untrusted (read straight from the wire) and
// could have unintended side effects, especially with EPP. We should
// skip if packetSize exceeds a mission-defined limit instead of
// letting the allocator handle any requests, which has a side
// effect of abandoning spanning packets on failure

This PR addresses that FIXME by introducing a mission-tunable upper
bound on a single deframed packet and rejecting wire-read sizes that
exceed it before any allocation is attempted.

The change set:

  • Adds ComCfg::AosMaxPacketSize (default 65536) as a mission-tunable
    constant in default/config/ComCfg.fpp. The default fits a maximally
    sized CCSDS Space Packet (header + 65535 byte data field) and is
    large enough for typical EPP traffic; missions that legitimately
    need larger packets can override the constant in their own config.
  • Adds a new warning event
    OversizedPacket(vcId, pvn, packetSize, maxSize) in
    AosDeframerEvents.fppi so operators can see the rejection in
    telemetry.
  • In appendToSpanningPacket, when sizePacket() returns a
    packetSize that exceeds ComCfg::AosMaxPacketSize, the patch
    logs OversizedPacket, abandons the spanning state, and seeks
    past the rejected packet bytes - all before any allocate_out
    call. The existing zero-size early return path is unchanged so the
    "need more header bytes" case still works.

Rationale

Without the bound, a malformed packet that declares a very large size
drives the buffer allocator to attempt arbitrarily large requests. The
result is a resource-exhaustion vector: a single AOS frame on any
virtual channel can request a buffer larger than the buffer manager
can satisfy, and repeated frames can degrade allocation service across
all virtual channels even after the affected spanning packet has been
abandoned.

The fix mirrors the pattern already established in PR #5085
(Fix pkt_length integer overflow and replaced ASSERT w/ event in
Svc/Ccsds/SpacePacketDeframer): reject untrusted size values with a
warning event rather than forwarding them to the allocator.

Testing/Review Recommendations

Verification performed locally:

  • fprime-util format --check on Svc/Ccsds/AosDeframer: clean.
  • Svc/Ccsds/AosDeframer unit test suite: 33/33 passed. The
    existing testSpanningPacketAllocFailureEvent case sends a
    65539-byte EPP packet and now asserts on the new OversizedPacket
    event because the bound check fires before the allocator is
    consulted; the m_failNextAlloc-driven test downstream still
    exercises the original allocation-failure path on a non-oversized
    packet.
  • Full framework unit test suite via fprime-util check: 107/107
    passed.

appendToSpanningPacket reads packetSize from the wire (via sizePacket
applied to the spanning-packet header) and passes it directly to
allocate_out. A malformed or hostile packet that declares a very large
size therefore drives the buffer allocator to attempt arbitrarily
large requests. This was previously called out in a FIXME comment in
the source but no bound check was in place.

The result was a resource-exhaustion vector: a single AOS frame on
any virtual channel could request a buffer larger than the buffer
manager can satisfy, and repeated frames could degrade allocation
service across all virtual channels even after the affected spanning
packet was abandoned.

This commit:

  * Adds ComCfg::AosMaxPacketSize (default 65536) as a mission-tunable
    upper bound on a single deframed packet. The default fits a
    maximally-sized CCSDS Space Packet (header + 65535 byte data
    field) and is large enough for typical EPP traffic; missions that
    legitimately need larger packets can override it.
  * Adds a new warning event OversizedPacket(vcId, pvn, packetSize,
    maxSize) so operators can see the rejection in telemetry.
  * In appendToSpanningPacket, when sizePacket returns a packetSize
    that exceeds ComCfg::AosMaxPacketSize, logs OversizedPacket,
    abandons the spanning state, and seeks past the rejected packet
    bytes before any allocate_out call.

The check is placed after the existing zero-size early return so the
"need more header bytes" path is unchanged.

Testing:
  fprime-util format --check on Svc/Ccsds/AosDeframer: clean
  AosDeframer unit test suite: 33/33 passed (existing
    testSpanningPacketAllocFailureEvent now asserts on the new
    OversizedPacket event for the same 65539-byte input, since the
    bound check fires up front; the alloc-failure path remains
    covered by the m_failNextAlloc-driven test downstream)
  Full framework unit test suite (fprime-util check): 107/107 passed
Signed-off-by: datqm <datqm@vingroup.net>
@thomas-bc thomas-bc requested a review from Willmac16 May 12, 2026 21:53
@Willmac16
Copy link
Copy Markdown
Collaborator

I would request that the limit be (additionally) override-able on a per instance basis via a configure() call like the rest of the parameters should be (I might've forgotten one on my original PR).

Copy link
Copy Markdown
Collaborator

@Willmac16 Willmac16 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change overall, a couple of comments on code dupe and configurability.

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
Comment on lines +331 to +340
const FwSizeType remainingBody = packetSize - vc.spanningPacket.bytesReceived;
this->abandonSpanningPacket(vc);

// Seek past the rejected packet (header bytes already consumed + remaining body)
const FwSizeType remainingLength = seekForward + remainingBody;
if (remainingLength > size) {
return 0;
} else {
return remainingLength;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code path should be shared w/ the allocate failure imo should be the same exact logic, just a diff check fail and event

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
// field would force allocate_out() to request arbitrarily large
// buffers, which exhausts the buffer manager and degrades downlink
// throughput across all virtual channels.
if (packetSize > ComCfg::AosMaxPacketSize) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a member var that defaults to the global ComCfg const but can be overridden via a configure call

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion, and for the sake of not having two ways of specifying the same thing, I would just make it a configure() option and remove the ComCfg config value.

Address two review threads on the prior packet-size bound commit:

  1. Configurability (Willmac16, thomas-bc): the global
     ComCfg::AosMaxPacketSize constant duplicates what a per-instance
     configure() option should provide. Remove the global and pass the
     bound through configure() instead, defaulting to a new
     AosDeframer.DefaultMaxPacketSize FPP constant (still 65536).
     Missions that need a different cap now set it explicitly when
     configuring the component, with one canonical place to override.

  2. Code duplication (Willmac16): the oversize-reject path and the
     allocator-failure-reject path had identical bodies — save the
     remaining-body offset, abandon the spanning packet, compute the
     seek-forward, and clamp it to the available data block. Factor
     this into a private helper abandonAndSeekPast() so the two call
     sites differ only in the warning event they emit.

New configure() signature:
  void configure(U32 fixedFrameSize,
                 bool frameErrorControlField,
                 U16 spacecraftId = ComCfg::SpacecraftId,
                 U8  vcId         = 0,
                 U8  pvnMask      = SPP_MASK | EPP_MASK,
                 FwSizeType maxPacketSize = AosDeframer_DefaultMaxPacketSize);

The OversizedPacket event format string and docstring no longer name
the (now-removed) ComCfg::AosMaxPacketSize constant.

Tests:
  - Existing testSpanningPacketAllocFailureEvent and
    testAllocFailureNextPacketExtracted continue to exercise the now-
    shared reject path through the alloc-failure branch.
  - New testConfiguredMaxPacketSizeOverride configures with a tight
    50-byte cap and verifies a 100-byte SPP packet is rejected via
    OversizedPacket with the configured cap reported in the event,
    proving the configure() override is wired through end-to-end.
  - 34/34 AosDeframer UTs pass (was 33; +1 new test).
Signed-off-by: datqm <datqm@vingroup.net>
@datqm
Copy link
Copy Markdown
Contributor Author

datqm commented May 13, 2026

Thanks for the thoughtful review — I fully agree with both points. Addressed in 81bbd42:

Configurability: Removed the global ComCfg::AosMaxPacketSize and added a per-instance override via configure(..., maxPacketSize), defaulting to a new AosDeframer.DefaultMaxPacketSize FPP constant (still 65536). One canonical place to set it.

Code dedup: Factored the common "log → abandon → seek-past" sequence into a private abandonAndSeekPast() helper. The two reject call sites now differ only in the warning event they emit.

Added testConfiguredMaxPacketSizeOverride to verify the override path end-to-end (configures a tight 50-byte cap, sends a 100-byte SPP packet, asserts OversizedPacket reports the configured value not the default).

Copy link
Copy Markdown
Collaborator

@Willmac16 Willmac16 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple more notes on the min & max bounds of default SPP packet size

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
Comment on lines +69 to +71
// maxPacketSize must be large enough to accommodate at least an empty packet header.
// We cannot enforce a fixed minimum here (SPP is 6 bytes, EPP is 1) so just require non-zero.
FW_ASSERT(maxPacketSize > 0, static_cast<FwAssertArgType>(maxPacketSize));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W/ the PVN mask we do actually have enough info to know if 1 (EPP) vs 7 (SPP) should be the minimum packet size.

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.fpp Outdated
Comment on lines +13 to +20
@ Default upper bound on the size of a single packet (Space Packet or
@ Encapsulation Packet) accepted by the deframer. The packet-length
@ field of a CCSDS Space Packet is U16 (max 65535 + 1 byte = 65536),
@ and Encapsulation Packets can in principle be larger. Capping at
@ 65536 by default keeps a malformed or hostile packet-length read
@ from the wire from being passed directly to the buffer allocator.
@ Override per-instance via configure().
constant DefaultMaxPacketSize = 65536
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image If your goal is to fit a whole, max-size SPP then 65542 is the right number since the header is not included in that 16 bit length count

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also rename the const to SPP Max Length (and prob move it to Ccsds/Types.fpp) if that’s what we’re deriving it from; that way looking at the default arg in the configure signature in the hpp its obvious whats driving the number.

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
Comment on lines +345 to +350
// packetSize is derived from the on-the-wire packet header. Cap it at
// the configured upper bound (m_maxPacketSize, set via configure())
// before passing to the allocator. Without this check a malformed or
// hostile size field would force allocate_out() to request arbitrarily
// large buffers, which exhausts the buffer manager and degrades
// downlink throughput across all virtual channels.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider slimming down this comment block. IMO:

Prevent large or hostile packets from starving all virtual channels of buffer manager resources

or similar is a sufficient description

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
vc.spanningPacket.context.set_pvn(ComCfg::Pvn::INVALID_UNINITIALIZED);
}

FwSizeType AosDeframer::abandonAndSeekPast(AosDeframerVc& vc,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this function does abandon; however, it only computes the seek, rather than performing it.

I’d prefer abandonAndComputeSeek or similar to make it clear we aren’t executing the seek just yet.

@datqm datqm force-pushed the fix/aosdeframer-bound-packet-size branch 2 times, most recently from d9596a5 to 7c9a64c Compare May 13, 2026 02:02
Comment on lines +76 to +80
if (pvnMask & PvnBitfield::EPP_MASK) {
minPacketSize = Svc::Ccsds::EncapsulationPacketMinLength;
} else if (pvnMask & PvnBitfield::SPP_MASK) {
minPacketSize = Svc::Ccsds::SpacePacketMinLength;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both can be enabled; however this will only check for EPP's min if both are enabled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in b3a0ba9.

@datqm
Copy link
Copy Markdown
Contributor Author

datqm commented May 13, 2026

@Willmac16 Updated in 7c9a64c:

  1. pvnMask-aware minimum: configure() derives min maxPacketSize from pvnMask (1 byte for EPP, 7 bytes for SPP-only).
  2. Default cap promoted: DefaultMaxPacketSizeSvc::Ccsds::SpacePacketMaxLength in Svc/Ccsds/Types/Types.fpp, value 65536 → 65542 per CCSDS 133.0-B-2 §4.1.2.2. Added companion SpacePacketMinLength = 7 and EncapsulationPacketMinLength = 1.
  3. Comment block trimmed to your suggested one-liner.
  4. Renamed abandonAndSeekPastabandonAndComputeSeek

… shared reject helper, pvnMask-aware min check

Plus: SPP constants moved to Ccsds/Types.fpp, slim oversize comment, rename helper to abandonAndComputeSeek:

  1. Pvn-aware minimum-packet-size validation in configure()
     (Willmac16): the prior `maxPacketSize > 0` assert was too loose.
     The pvnMask passed into configure() already encodes which packet
     protocols this deframer is being told to accept, so we can
     enforce a meaningful minimum: 1 byte if EPP is enabled (a single
     Encapsulation Idle Packet per CCSDS 133.1-B-3 §4.1.2.1), or 7
     bytes if only SPP is enabled (6-byte primary header + 1-byte
     minimum data field per CCSDS 133.0-B-2 §4.1.2.2).

  2. Promote the default maxPacketSize constant to a CCSDS-wide
     location and align its value with the standard
     (Willmac16, with CCSDS 133.0-B-2 §4.1.2.2 cited inline):
       * Renamed AosDeframer.DefaultMaxPacketSize (65536) to
         Svc.Ccsds.SpacePacketMaxLength (65542), reflecting that the
         number is driven by the CCSDS Space Packet specification
         (6-byte header + 65535+1 = 65536 byte data field = 65542
         octets total), not an AosDeframer-internal choice.
       * Moved the constant from
         Svc/Ccsds/AosDeframer/AosDeframer.fpp to
         Svc/Ccsds/Types/Types.fpp alongside the SpacePacketHeader
         struct, so reading the default arg in the configure()
         signature makes it obvious where the number comes from.
       * Added Svc.Ccsds.SpacePacketMinLength (7) and
         Svc.Ccsds.EncapsulationPacketMinLength (1) as companion
         constants used by the new minimum check in configure().

  3. Trim the oversize-reject comment block (Willmac16): the prior
     six-line explanation of why the cap exists has been replaced
     with a single line ("Prevent large or hostile packets from
     starving all virtual channels of buffer manager resources"),
     matching reviewer-suggested wording and the project's general
     comment density.

  4. Rename abandonAndSeekPast -> abandonAndComputeSeek (Willmac16):
     the helper abandons the spanning packet but only returns the
     seek distance; the caller chain is responsible for actually
     advancing the read pointer. The new name makes that contract
     explicit at the call site.

Tests:
  - testSpanningPacketAllocFailureEvent was originally exercising
    the oversize-reject path with a 2-byte EPP length field
    producing a 65539-byte declared packet. With the default cap
    raised from 65536 to 65542, that input now falls under the cap.
    Updated the test to use a 4-byte EPP length field (lengthOfLength
    = Four = 0b11) with a declared size of 65544 bytes (8-byte EPP
    header + 0x10000-byte body), which exceeds the new 65542-byte
    cap and triggers OversizedPacket as intended.
  - 34/34 AosDeframer UTs pass.
  - 107/107 framework UTs pass.
Signed-off-by: datqm <datqm@vingroup.net>
@datqm datqm force-pushed the fix/aosdeframer-bound-packet-size branch from 7c9a64c to b3a0ba9 Compare May 13, 2026 02:24
Copy link
Copy Markdown
Collaborator

@Willmac16 Willmac16 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good at this point!

In general I'd recommend against force pushing multiple copies of the same commit--it makes it harder to track what's changing w/o explicitly having gihtub/git cli diff the 2 commits vs the branch history.

If you do additional changes on this I would consider pruning some of the comments down in length, since they seem to accumulate more and more lines each iteration along with commentary that almost seems like an LLM berating itself for getting something wrong the first time.

@datqm
Copy link
Copy Markdown
Contributor Author

datqm commented May 13, 2026

Looks good at this point!

In general I'd recommend against force pushing multiple copies of the same commit--it makes it harder to track what's changing w/o explicitly having gihtub/git cli diff the 2 commits vs the branch history.

If you do additional changes on this I would consider pruning some of the comments down in length, since they seem to accumulate more and more lines each iteration along with commentary that almost seems like an LLM berating itself for getting something wrong the first time.

Thank you for the guidance! I’ve recently started my research in the satellite field and I'm really excited about it. I’m still learning the ropes, so I appreciate the tips. I hope to contribute more effectively to this field in the future !

Copy link
Copy Markdown
Collaborator

@thomas-bc thomas-bc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks fine, but I would like to push back on the LLM-generated comments. Please find a way to make it less verbose. Good code should not need that much commenting. This is hindering maintainability.

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.hpp Outdated
//! \param spacecraftId The spacecraft ID to accept (10 bits, per Section 4.1.2.2)
//! \param vcId The virtual channel ID to accept (6 bits, per Section 4.1.2.3)
//! \param pvnMask Bitmask of Packet Version Numbers to extract (SPP=0x01, EPP=0x80)
//! \param maxPacketSize Upper bound on a single deframed packet (Space Packet
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

way too verbose for a parameter description

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.hpp Outdated
//! responsible for emitting the appropriate warning event before calling
//! and for performing the returned seek by propagating it up through the
//! caller chain. This function abandons the spanning packet but does not
//! itself advance any read pointer.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too verbose, first and last sentence are essentially saying the same thing, and there's no point calling out where the helper is being used imo

Comment thread Svc/Ccsds/AosDeframer/AosDeframer.cpp Outdated
// EPP can be as small as a single idle byte (CCSDS 133.1-B-3 §4.1.2.1),
// SPP is 7 octets minimum (CCSDS 133.0-B-2 §4.1.2.2: 6-byte header +
// 1-byte minimum data field). When both are enabled the SPP minimum is
// the binding constraint.
Copy link
Copy Markdown
Collaborator

@thomas-bc thomas-bc May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also too verbose - no need to describe in that much depth what's going on in the code when it's trivial like this

@datqm
Copy link
Copy Markdown
Contributor Author

datqm commented May 14, 2026

Code looks fine, but I would like to push back on the LLM-generated comments. Please find a way to make it less verbose. Good code should not need that much commenting. This is hindering maintainability.
I've updated it in 6693ee4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants