fields: preserve containers when copying fields#5009
Conversation
eac2ff2 to
159afb7
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5009 +/- ##
==========================================
- Coverage 80.31% 80.04% -0.28%
==========================================
Files 381 384 +3
Lines 93630 95658 +2028
==========================================
+ Hits 75202 76565 +1363
- Misses 18428 19093 +665
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a longstanding copy bug for _FieldContainer wrappers (e.g., Emph) so that copying preserves the container type rather than collapsing to the wrapped field—particularly impacting Packet_metaclass default-replacement when fields_desc references another packet (issue #4657).
Changes:
- Add
_FieldContainer.copy()to return a copied container instance (including copying its stored attributes/slots). - Add
_FieldContainer.__setattr__()to delegate unsupported attribute assignment to the wrapped field (mirroring existing read delegation). - Add regression tests covering
Emph(...).copy()and the packet-metaclass copy/default override path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
scapy/fields.py |
Implements _FieldContainer.copy() and adds write-delegation via __setattr__ to preserve wrappers during copy/default replacement. |
test/fields.uts |
Adds regression coverage for direct container .copy() and packet metaclass field-copy behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def __setattr__(self, attr, value): | ||
| # type: (str, Any) -> None | ||
| try: | ||
| object.__setattr__(self, attr, value) | ||
| except AttributeError as ex: | ||
| try: | ||
| fld = object.__getattribute__(self, "fld") | ||
| except AttributeError: | ||
| raise ex | ||
| setattr(fld, attr, value) |
There was a problem hiding this comment.
Fixed — __setattr__ now checks whether attr is defined on the container class (via the MRO __dict__) before delegating to the wrapped field. Read-only descriptors like MultipleTypeField.fld will raise the original AttributeError instead of being forwarded to the selected underlying field.
| """ | ||
| __slots__ = ["fld"] | ||
|
|
||
| def copy(self): |
There was a problem hiding this comment.
What this function does looks very funky to me. Couldn't we just call the copy() of the sub field?
There was a problem hiding this comment.
Good point — I've simplified it significantly.
We can't just return self.fld.copy() though: that was the original bug. Without an explicit copy() on _FieldContainer, the call goes through __getattr__ and ends up as self.fld.copy(), which returns a bare ByteField instead of an Emph wrapper (see #4657).
The new implementation is much shorter: create a new container via __new__, iterate over __slots__, and call .copy() on slot values where available. This keeps the wrapper type while still deep-copying the contained field.
AI-Assisted: yes (Codex)
Address review feedback on PR secdev#5009: - Replace the MRO-based copy implementation with a simpler approach that creates a new container and copies each __slots__ entry, calling copy() on contained fields where available. This preserves the wrapper type (the bug was __getattr__ delegating copy to the wrapped field). - Guard __setattr__ delegation when the attribute is defined on the container class (e.g. MultipleTypeField's read-only fld property). Co-authored-by: Teppei.F <T3pp31@users.noreply.github.com>
159afb7 to
5443e61
Compare
[Conversation comment] Thanks for the review! I've pushed an update addressing both comments: - copy(): Simplified the implementation. The issue wasn't that we shouldn't call the sub-field's copy() at all — it's that without an explicit _FieldContainer.copy(), container.copy() was resolved via __getattr__ and returned only container.fld.copy() (a plain ByteField instead of Emph). The new version creates a new container instance and copies each __slots__ entry, calling .copy() on values that support it (e.g. the wrapped field). - __setattr__: Added a guard so delegation only happens for attributes that aren't defined on the container class. Assignments like mtf.fld = ... on MultipleTypeField now correctly raise AttributeError instead of silently mutating the selected underlying field. Rebased onto current master. All test/fields.uts tests pass, including the new regression test. [Reply to gpotter2 on copy()] Good point — I've simplified it significantly. We can't just return self.fld.copy() though: that was the original bug. Without an explicit copy() on _FieldContainer, the call goes through __getattr__ and ends up as self.fld.copy(), which returns a bare ByteField instead of an Emph wrapper (see secdev#4657). The new implementation is much shorter: create a new container via __new__, iterate over __slots__, and call .copy() on slot values where available. This keeps the wrapper type while still deep-copying the contained field. [Reply to Copilot on __setattr__] Fixed — __setattr__ now checks whether attr is defined on the container class (via the MRO __dict__) before delegating to the wrapped field. Read-only descriptors like MultipleTypeField.fld will raise the original AttributeError instead of being forwarded to the selected underlying field. Co-authored-by: Teppei.F <T3pp31@users.noreply.github.com>
|
Thanks for the review! I've pushed an update addressing both comments:
Rebased onto current |
Summary
Fix copying of
_FieldContainerinstances so wrappers such asEmphremain wrappers instead of being replaced by their contained field.The root cause was that
_FieldContainerdelegated missing attributes tofld, socontainer.copy()resolved tocontainer.fld.copy()when the container class did not define its owncopy()method. This also affected packet field copying whenfields_descreferences another packet class and overrides a copied field default.Fixes #4657.
Changes
_FieldContainer.copy()to copy the container instance and its stored attributes.Emph(...).copy()and the packet-metaclass copy path from the issue.Validation
python3 -m scapy.tools.UTscapy -t test/fields.uts -N -qq -f xUnit -o /tmp/scapy-fields.xml/tmp/scapy-flake8-venv/bin/flake8 scapy/Note:
toxis not installed in this local environment, so flake8 was run through a temporary venv using the repository-pinnedflake8<6.0.0version.