Skip to content

[EventHub][ServiceBus] Fix pyAMQP decode of performatives with omitted trailing null fields#47660

Open
j7nw4r wants to merge 7 commits into
Azure:mainfrom
j7nw4r:fix/pyamqp-decode-short-list
Open

[EventHub][ServiceBus] Fix pyAMQP decode of performatives with omitted trailing null fields#47660
j7nw4r wants to merge 7 commits into
Azure:mainfrom
j7nw4r:fix/pyamqp-decode-short-list

Conversation

@j7nw4r

@j7nw4r j7nw4r commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

The pyAMQP transport crashes when decoding an AMQP performative whose trailing null fields were omitted by the sender. The decoded performative list is now padded to its full field count so positional field access and namedtuple unpacking remain safe, with omitted trailing fields reading back as null.

Motivation

AMQP 1.0 section 1.4 permits a sender to omit the trailing null fields of a described list, so an incoming performative can arrive shorter than its full field count. The decoder in _decode.py built the field list from the element count on the wire, and downstream consumers then read fixed positions (for example frame[9] for Open properties, and OpenFrame(*frame) namedtuple unpacking). On a short list this raised IndexError or TypeError. This is the receiver side of Azure/amqpnetlite#645: a spec-compliant sender that omits trailing nulls triggers the crash, while brokers and other AMQP stacks that bound decoding by the wire count are unaffected.

Changes

  • Pad the decoded performative field list in decode_frame up to the performative's full field count, derived from each performative's _definition and excluding the transfer payload slot, before any positional access or namedtuple unpacking, so omitted trailing fields read back as None.
  • Handle the AMQP list0 (0x45) body encoding in decode_frame. A performative whose fields are all omitted may arrive as list0, which carries no count byte; the previous code read data[5] out of range and raised IndexError before padding. It is now decoded as zero fields and padded to the full field count, closing the fully-omitted case for End and Close.
  • Suppress the protected-access pylint warning and the mypy attr-defined error on the _PERFORMATIVE_FIELD_COUNT comprehension's access to _code/_definition, matching the existing suppressions on every such access in _encode.py.
  • Apply all decoder changes to both the Event Hubs and Service Bus vendored _pyamqp copies, which remain byte-identical.
  • Add regression tests covering a short Open, a short Transfer with a trailing payload, the list0 End and Close encodings, and the per-performative field counts.
  • Add changelog entries and bump the azure-eventhub version to 5.15.2.

Test plan

  • azure-eventhub: pytest tests/pyamqp_tests/unittest/test_decode.py, 17 passed.
  • azure-servicebus: pytest tests/unittests/test_pyamqp_decode.py, 13 passed.
  • pylint --enable=protected-access on _decode.py rates 10.00/10 with no useless-suppression warning.

@j7nw4r
j7nw4r marked this pull request as ready for review July 15, 2026 22:56
Copilot AI review requested due to automatic review settings July 15, 2026 22:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes pyAMQP decoding of performatives with omitted trailing fields across Event Hubs and Service Bus.

Changes:

  • Pads shortened performatives and supports list0.
  • Adds regression tests for Open and Transfer frames.
  • Updates changelogs and Event Hubs versioning.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sdk/servicebus/azure-servicebus/tests/unittests/test_pyamqp_decode.py Adds decoder regression tests.
sdk/servicebus/azure-servicebus/CHANGELOG.md Documents the fix.
sdk/servicebus/azure-servicebus/azure/servicebus/_pyamqp/_decode.py Implements performative padding and list0 decoding.
sdk/eventhub/azure-eventhub/tests/pyamqp_tests/unittest/test_decode.py Adds equivalent regression tests.
sdk/eventhub/azure-eventhub/CHANGELOG.md Adds the 5.15.2 release entry.
sdk/eventhub/azure-eventhub/azure/eventhub/_version.py Bumps version to 5.15.2.
sdk/eventhub/azure-eventhub/azure/eventhub/_pyamqp/_decode.py Mirrors the decoder fix.

Comment thread sdk/eventhub/azure-eventhub/azure/eventhub/_pyamqp/_decode.py Outdated
Comment thread sdk/servicebus/azure-servicebus/azure/servicebus/_pyamqp/_decode.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

j7nw4r added 5 commits July 16, 2026 17:10
AMQP 1.0 section 1.4 lets a sender omit trailing null fields, so an
incoming performative described-list can be shorter than the full
field count. The pyAMQP decoder built the field list from the wire
count, and consumers then accessed fixed indices (frame[9],
OpenFrame(*frame)), raising IndexError/TypeError on a short list.

decode_frame now pads the decoded list up to the performative's full
field count, so omitted trailing fields read back as None. Applied to
both the Event Hubs and Service Bus vendored copies, with regression
tests and changelog entries.
…ives

Add the # type: ignore # pylint: disable=protected-access suppressions on the
_PERFORMATIVE_FIELD_COUNT comprehension, matching every other access to the
protected _code/_definition attributes in this engine (see _encode.py), so the
pylint and mypy CI gates stay green.

Also handle the AMQP list0 (0x45) body encoding in decode_frame: a performative
whose fields are all omitted may arrive as list0, which carries no count byte.
Previously this read data[5] out of range and raised IndexError before the
padding ran. It is now treated as zero fields and padded to the full field
count, closing the "omitted trailing null fields" case for End and Close.

Add list0 regression tests to both the eventhub and servicebus suites.
…drift

Extend the decode regression tests for the trailing-null-omission fix:

- Parametrized short-list8 decode over Begin, Attach, Disposition, and Detach,
  the no-default namedtuples that raised TypeError on a short unpack pre-fix.
  Only Open was previously covered.
- Short performative encoded as a list32 (0xd0) body, exercising the count/offset
  branch that no existing test reached (all fixtures used list8).
- Short SASLInit and SASLOutcome frames, which also have required fields.
- A skip-guarded test asserting the eventhub and servicebus _pyamqp copies of
  _decode.py, _encode.py, and performatives.py stay byte-identical, so a fix
  applied to one copy but not the other is caught. It skips when the sibling
  package source is not present (the packages ship separately).

Applied identically to both the eventhub and servicebus decode test suites.
Decoding a short performative padded every omitted trailing field with
None. For a field whose AMQP-defined default is non-null this is wrong:
a minimal Open frame legitimately omits max_frame_size (default
4294967295), and _connection._incoming_open reads it positionally and
numerically (`frame[2] < 512`), which raises TypeError on None.

Pad each omitted field with its field default from the performative
_definition instead of None, via a _PERFORMATIVE_FIELD_DEFAULTS map;
_PERFORMATIVE_FIELD_COUNT is now derived from it so the two stay in
lockstep. Applied to both the Event Hubs and Service Bus copies of the
vendored _pyamqp engine.

Tests exercise the incoming-Open numeric path (max_frame_size/channel_max
materialize to their defaults and the `< 512` comparison does not raise),
and the no-default-performative and transfer tests now assert the padded
tail equals each field's spec default (e.g. Disposition.batchable is
False, Transfer.message_format is 0).
The _PERFORMATIVE_FIELD_DEFAULTS comprehension reads _code and
_definition off the performative classes directly. Those attributes are
assigned at import time in performatives.py, so pylint 4.0.4 with
azure-pylint-guidelines-checker cannot resolve them statically and
raised 28 E1101(no-member) errors, failing the Analyze job with exit 2.
Extend the existing protected-access disable on that line to also cover
no-member. Comment-only, behavior unchanged; applied to both
byte-identical _pyamqp copies.
@j7nw4r
j7nw4r force-pushed the fix/pyamqp-decode-short-list branch from fbd200d to 50c8857 Compare July 16, 2026 21:10
Copilot AI review requested due to automatic review settings July 16, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

The api-consistency workflow requires a committed api.md and
api.metadata.yml for every package a PR touches. azure-eventhub had
neither, so the check reported "Missing required API files" once this
branch touched the package. Generate both with azpysdk apistub
(apiview-stub-generator 0.3.28, the version eng/apiview_reqs.txt pins),
which the workflow diff-gates against a fresh regeneration.

The change touches only the private _pyamqp engine, so the public API
surface is unchanged; this is the package's existing public API captured
for the first time. Verified the local toolchain reproduces the
committed azure-servicebus api.md byte-for-byte (matching apiMdSha256),
so the generated eventhub file matches what CI regenerates.
Copilot AI review requested due to automatic review settings July 16, 2026 21:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread sdk/eventhub/azure-eventhub/azure/eventhub/_pyamqp/_decode.py Outdated
Comment thread sdk/servicebus/azure-servicebus/azure/servicebus/_pyamqp/_decode.py Outdated
…ault

Only trailing fields of a performative may be omitted, so a sender that
sets a later field while wanting an earlier one's default must encode
that earlier field as an explicit null. A decoded null for a field whose
AMQP default is non-null therefore also means that default. Decoding now
normalizes those nulls so an explicit null reads back identically to an
omitted field: an Open that nulls max_frame_size comes back as
4294967295 rather than None, and _connection._incoming_open's
frame[2] < 512 no longer raises TypeError. Fields whose declared default
is null are left as None.

Applied to both byte-identical _pyamqp copies with a regression test that
nulls max_frame_size while setting channel_max after it.
Copilot AI review requested due to automatic review settings July 16, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@EldertGrootenboer EldertGrootenboer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

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.

3 participants