Skip to content

Conversation

@Dimi1010
Copy link
Collaborator

@Dimi1010 Dimi1010 commented Oct 24, 2025

Summary

This PR intended to improve the encapsulation and clarity of Layer field data. As per the C++ core guidelines protected data fields are generally undesired as they represent difficulty in maintaining base class invariants across derived classes.

The main object of this PR is the introduction of a new helper struct LayerAllocationInfo in the internal namespace, to hold allocation information for Layer objects and provide behavior helper methods. The helper structure replaces is contained as a field in a Layer to represent its current allocation state.

LayerAllocationInfo and chosen fields.

The information structure contains 2 members:

  • Packet* attachedPacket, replacing m_Packet pointer inside Layer.
  • bool managedByPacket, replacing m_isAllocatedInPacket flag inside Layer.

The replacements were chosen due to their relevancy to the allocation behaviour of Layer and little else. Those fields were previously intermixed with other fields in the Layer object causing confusion when determining their use and scope.

  • attachedPacket (previously m_Packet) is exclusively used for indication if the layer's data is owned by a packet and to forward requests for structural data changes to the underlying packet buffer, if available. The new name was chosen to better represent that use.
  • managedByPacket (previously m_IsAllocatedInPacket) is exclusively used as flag to determine if the Layer object's lifecycle is managed by the Packet instance it is attached to. The field intricately tied to the state of attachedPacket and as such is included to the helper structure.

The new fields include extensive documentation regarding their use, improving the maintainability of the code. ( Think about the you in 5 months reading it 🙂 )

Behaviour methods instead of direct field mutation

The new helper structure adds two behavior methods:

  • attachPacket(Packet* packet, bool managed, bool force = false): This method encapsulates the behavior of attaching a layer to a Packet instance. It allows a Packet instance attaching a Layer to itself to update the information in one step instead of manually having to directly update private fields to the Layer instance. The method also provides integrated sanity checks if a Layer is being attached to a Packet instance twice.
  • detach(): Encapsulates the behaviour of detaching a Layer from Packet. It allows a Packet instance detaching a Layer to inform it of its new state in one step.

Protected data accessors and private fields

The new LayerAllocationInfo structure is held as a private field inside Layer. The change improves data integrity as the allocation information (packet or managed flag) is unavailable for accidental modification by derived classes.

To keep existing behaviour of a Layer instance forwarding its attachment to any other instance it constructs during parsing, a new protected accessor getAttachedPacket() has been added and instances of direct use of m_Packet are replaced by it.

Bug Fixes

  • Fixed bug due to lack of zeroing of m_IsAllocatedInPacket flag when detaching a layer. If said layer is attached to a new packet instance, the old implementation disregarded the ownership flag and considered the layer instance as owned by the packet, due to the layer flag being set from the previous packet.

Comment on lines +29 to 31
// Should this really always delete m_Data? What if the layer is attached to a packet?
if (m_Data != nullptr)
delete[] m_Data;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See comment? This would cause an error if called on a Layer that does not own its data span.

else
{
layer->m_Packet = nullptr;
layer->m_AllocationInfo.detach();
Copy link
Collaborator Author

@Dimi1010 Dimi1010 Oct 24, 2025

Choose a reason for hiding this comment

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

This line in particular created a subtle bug in the old implementation.

If a layer is originally owned by the packet instance, and detached, the m_IsAllocatedInPacket flag was not zeroed. If the layer has then been attached to another Packet. The attached layer was considered as if managed by the new packet even if attached with addLayer(layer, false) which should not transfer ownership of the layer object.

@Dimi1010 Dimi1010 added the bug label Oct 24, 2025
///
/// If 'false', the Layer object is considered unmanaged and the user is responsible for freeing it.
/// This is commonly the case for layers created on the stack and attached to a Packet.
bool managedByPacket = false;
Copy link
Collaborator Author

@Dimi1010 Dimi1010 Oct 24, 2025

Choose a reason for hiding this comment

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

Maybe ownedByPacket is a more descriptive name? 🤔

Comment on lines +330 to +332
PTF_ASSERT_TRUE(packetWithoutTunnel.addLayer(vxlanEthLayer, true));
PTF_ASSERT_TRUE(packetWithoutTunnel.addLayer(vxlanIP4Layer, true));
PTF_ASSERT_TRUE(packetWithoutTunnel.addLayer(vxlanIcmpLayer, true));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Or should we keep the test as is, but correctly delete the layers afterwards?

@codecov
Copy link

codecov bot commented Oct 24, 2025

Codecov Report

❌ Patch coverage is 63.48684% with 111 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.45%. Comparing base (0132d27) to head (3d64f18).

Files with missing lines Patch % Lines
Packet++/src/Sll2Layer.cpp 6.25% 13 Missing and 2 partials ⚠️
Packet++/src/IPv6Layer.cpp 40.90% 13 Missing ⚠️
Packet++/src/SllLayer.cpp 18.75% 12 Missing and 1 partial ⚠️
Packet++/src/GreLayer.cpp 29.41% 10 Missing and 2 partials ⚠️
Packet++/src/IPSecLayer.cpp 27.27% 7 Missing and 1 partial ⚠️
Packet++/src/NullLoopbackLayer.cpp 42.85% 7 Missing and 1 partial ⚠️
Packet++/src/VlanLayer.cpp 52.94% 7 Missing and 1 partial ⚠️
Packet++/src/IPv4Layer.cpp 72.00% 7 Missing ⚠️
Packet++/src/TcpLayer.cpp 76.00% 6 Missing ⚠️
Packet++/src/NflogLayer.cpp 20.00% 3 Missing and 1 partial ⚠️
... and 10 more
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #2004      +/-   ##
==========================================
+ Coverage   83.41%   83.45%   +0.03%     
==========================================
  Files         311      311              
  Lines       55019    54576     -443     
  Branches    11816    11806      -10     
==========================================
- Hits        45892    45544     -348     
- Misses       7852     8199     +347     
+ Partials     1275      833     -442     
Flag Coverage Δ
alpine320 75.89% <57.99%> (+<0.01%) ⬆️
fedora42 75.44% <58.36%> (-0.39%) ⬇️
macos-14 81.57% <60.13%> (+0.06%) ⬆️
macos-15 81.55% <60.13%> (+0.04%) ⬆️
mingw32 69.95% <47.52%> (-0.58%) ⬇️
mingw64 69.91% <47.52%> (-0.49%) ⬇️
npcap ?
rhel94 75.45% <58.20%> (-0.42%) ⬇️
ubuntu2004 59.48% <57.50%> (-0.65%) ⬇️
ubuntu2004-zstd 59.56% <57.50%> (-0.67%) ⬇️
ubuntu2204 75.41% <58.20%> (-0.39%) ⬇️
ubuntu2204-icpx 57.82% <34.47%> (-2.73%) ⬇️
ubuntu2404 75.51% <57.83%> (-0.38%) ⬇️
ubuntu2404-arm64 75.56% <57.83%> (-0.01%) ⬇️
unittest 83.45% <63.48%> (+0.03%) ⬆️
windows-2022 85.42% <74.11%> (+0.17%) ⬆️
windows-2025 85.45% <74.11%> (+0.11%) ⬆️
winpcap 85.45% <74.11%> (-0.09%) ⬇️
xdp 53.02% <55.97%> (-0.52%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Dimi1010 Dimi1010 marked this pull request as ready for review October 24, 2025 22:53
@Dimi1010 Dimi1010 requested a review from seladb as a code owner October 24, 2025 22:54
@seladb
Copy link
Owner

seladb commented Nov 7, 2025

@Dimi1010 I'm not sure why replacing 2 protected members (m_Packet, m_IsAllocatedInPacket) with 1 private struct (m_AllocationInfo) that contains both of them improves anything... if we want to avoid using protected methods (which I'm not sure why it is better), we can just introduce getAttachedPacket(), but TBH I'm not sure this change is needed also

@Dimi1010
Copy link
Collaborator Author

Dimi1010 commented Nov 7, 2025

@Dimi1010 I'm not sure why replacing 2 protected members (m_Packet, m_IsAllocatedInPacket) with 1 private struct (m_AllocationInfo) that contains both of them improves anything... [...]

@seladb It mitigates the risk of this issue #2004 (comment) which was a bug that can be hard to track and easy to happen and completely broke the memory ownership model.

Having them in the same struct indicates that the two variables are much more tightly related compared to let's say, m_Packet and m_Data. It makes it a bit harder to overwrite only one of the variables, forgetting to update the other.

If the allocation info needs to be updated, it is more likely to be done through behaviour actions (m_AllocInfo.attachPacket() / detach()) that conceptualize the entire operation or completely overwriting the allocation info object.

IMO, this is easier to understand and less bug-prone at the point of use, than having to remember to flip N variables manually every time the operation needs to be performed.

[...] if we want to avoid using protected methods (which I'm not sure why it is better), we can just introduce getAttachedPacket(), but TBH I'm not sure this change is needed also

Protected data make it a pain to ensure invariants in the base classes of an inheritance hierarchy. That is especially true for large hierarchies like Layer [1]. Let's take the allocation info for example. It is solely managed by the Layer base class (+ Packet). Derived classes shouldn't modify it, because they shouldn't deal with allocation logic. They should just add parsing logic of to the data buffer.

If it was protected data, I can at ANY POINT in ANY derived class, write m_AllocationInfo = AllocationInfo{} or [...] = AllocationInfo{ someOtherPacketPtr, true }. Congratulations, the invariant is broken. Everything that relied on m_AllocationInfo being synchronized with other logic (e.g the actual packet that holds the layer) can go and burn. It is the reason why it was added in the C++ core guidelines in the segment "avoid protected data".

Making it private with RO accessors (if RO access is needed), changes the situation from "It can happen, but shouldn't." to "It can't happen". Removing the need to scan every code change for if a bug of such nature was introduced.

PS:
[1] - IMO the Layer hierarchy is somewhat too large already, as separate payload messages should not be represented via inheritance as they are not truly a new network protocol type. I'm looking at HTTPRequest and HTTPRespose, etc. IMO, it should be HTTPLayer with HTTPRequestPayload[View] / HTTPResposePayload[View], you can bind to the HTTPLayer to mutate the data buffer.

@seladb
Copy link
Owner

seladb commented Nov 9, 2025

@seladb It mitigates the risk of this issue #2004 (comment) which was a bug that can be hard to track and easy to happen and completely broke the memory ownership model.

There are very few places that need to update these members, and most (if not all) of them reside in the Packet class. So if there's a bug it should not be very hard to find and fix it

Making it private with RO accessors (if RO access is needed), changes the situation from "It can happen, but shouldn't." to "It can't happen". Removing the need to scan every code change for if a bug of such nature was introduced.

I agree that layers shouldn't deal with the allocation logic, and I'm pretty sure none of them do. In practice, they never need to unless there's a very special use case. I guess we can leave the RO method although I don't think it's a real pain for people writing protocol parsers - I don't recall a case where I asked someone not to change these members...

@Dimi1010
Copy link
Collaborator Author

Dimi1010 commented Nov 11, 2025

There are very few places that need to update these members, and most (if not all) of them reside in the Packet class. So if there's a bug it should not be very hard to find and fix it

It wouldn't have been that simple to find. I found it only after I grouped the variables and a test suddenly started memory leaking due to being incorrect. It was attaching the layer without ownership and then not deleting the layer. Everything still appeared to run fine because the packet deleted it erroneously.

Also, IMO that is only treating the symptom. In memory ownership schemes, generally keeping things simple at the point of use is key to prevent errors. Having to manually remember which variables to flip is not simple at the point of use, even if its only a few places. Having the complex actions encapsulated in methods is better due to explicit requirement for all parameters to be provided before executing the action.

I agree that layers shouldn't deal with the allocation logic, and I'm pretty sure none of them do. In practice, they never need to unless there's a very special use case. I guess we can leave the RO method although I don't think it's a real pain for people writing protocol parsers - I don't recall a case where I asked someone not to change these members...

In practice, if the fields aren't needed by the derived classes then they shouldn't be exposed to them. I don't think we will have any special use cases where a derived class will need to change its owned packet from its internals. The only need they have for the packet is to forward the pointer to the next layer which is RO operation.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants