Skip to content

EC: lift the 65535 children-per-tag wire-format ceiling (#199) - #570

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:feature/ec-large-tag-count
May 11, 2026
Merged

EC: lift the 65535 children-per-tag wire-format ceiling (#199)#570
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:feature/ec-large-tag-count

Conversation

@got3nks

@got3nks got3nks commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

The EC wire format encoded the number of children inside any tag as a uint16, silently truncating when a tag carried more than 65535 children. The most visible consequence was EC_OP_SHARED_FILES: amuled adds one child tag per shared file, so any library above 65535 files surfaced both as a counter capped at 65535 in amulegui and as silent EC corruption that broke the downloads-list refresh and other follow-on traffic. Reported by Stoatwblr in #199.

Fix

Two layered pieces, with capability negotiation for safe rollout:

  1. Sentinel-extended wire format. When the negotiated EC flag EC_FLAG_LARGE_TAG_COUNT is in effect, a count >= 0xFFFF is encoded as 0xFFFF followed by a uint32 carrying the real count. Smaller counts still emit the historical uint16 — wire-byte-identical for every existing nested tag in the codebase. CECTag::WriteChildren / ReadChildren branch on socket.m_tx_flags / m_rx_flags & EC_FLAG_LARGE_TAG_COUNT, and CECTag::GetTagLen takes a new useLargeCount parameter so writer and reader compute identical wire-size estimates and m_dataLen subtraction stays consistent.

  2. Capability negotiation through the existing auth handshake. New client sends EC_TAG_CAN_LARGE_TAG_COUNT in EC_OP_AUTH_REQ; new server records the capability and echoes the tag in EC_OP_AUTH_OK. ECSocket::WritePacket adds EC_FLAG_LARGE_TAG_COUNT to outgoing flags unconditionally — auto-stripped by flags &= m_my_flags when not negotiated. This is what keeps mixed-version EC safe: without negotiation, both sides stay on the historical wire format, and WriteChildren caps outgoing counts at 0xFFFE both to fit in uint16 and to avoid emitting 0xFFFF (which a fix peer without the negotiated flag would still treat as plain count, matching old behaviour).

Why sentinel-extended instead of always-uint32

Once EC_FLAG_LARGE_TAG_COUNT is negotiated both sides know the format, so the count field could in principle just always be a uint32. The sentinel choice optimises for the common case:

  • Wire compactness. Most tags in EC have a handful of children (5–20 typically). Sentinel emits uint16(count) for counts under 0xFFFF — 2 bytes — and only the rare large-count tag pays the extra 4 bytes (0xFFFF marker + uint32 real count, 6 bytes total). Always-uint32 would add 2 bytes to every tag-with-children. For a typical EC stats response with ~50 tags-that-have-children, that's +100 bytes per packet at no real benefit.
  • Byte-identical wire for typical packets. With sentinel, a packet whose tags all have <0xFFFF children produces wire bytes byte-for-byte identical to what an old peer would emit. Packet captures, debugging tools, third-party EC clients reading bytes manually keep working unchanged for the typical case; only when a tag actually needs >65535 children does the wire diverge.
  • Single branch in the hot path. The reader's first read is unchanged from old code (uint16); the only added cost is one comparison against 0xFFFF.

Always-uint32 would simplify the code at the cost of fatter wire on every packet. Sentinel was the chosen trade-off.

Compatibility matrix

End-to-end verified on Ubuntu 26.04 with a synthetic 70001-file shared library:

Daemon Client File rows received Result
pre-fix pre-fix 65534 historical baseline (capped by old AddTag)
fix fix 70000 sentinel format, full delivery
fix pre-fix 65534 plain uint16 capped at 0xFFFE, no parse error
pre-fix fix 65534 plain uint16, no sentinel collision

Mixed-version EC is functional in both directions with no protocol breaks; the worst-case behaviour on huge libraries is the same effective ceiling old code always had.

Caveats

External EC client implementations (e.g. amuleweb, third-party Node / web bindings) keep working unchanged — they don't advertise EC_TAG_CAN_LARGE_TAG_COUNT in auth, so the daemon falls back to the historical wire format for them. To opt in, an external client adds the empty EC_TAG_CAN_LARGE_TAG_COUNT (0x0011) tag to its EC_OP_AUTH_REQ, watches for the same tag in EC_OP_AUTH_OK, and on a hit handles the sentinel branch in its TAGCOUNT reader (~10–15 lines).

…t#199)

CECTag's children-count wire field was a uint16, silently truncating
when a tag carried more than 65535 children. The most visible
consequence was EC_OP_SHARED_FILES: amuled adds one child tag per
shared file, so any library above 65535 files surfaced both as a
counter capped at 65535 in amulegui and as silent EC corruption that
broke the downloads-list refresh and other follow-on traffic
(reported by Stoatwblr in amule-project#199).

Two layered fixes:

1. New sentinel-extended wire format. When the negotiated EC flag
   EC_FLAG_LARGE_TAG_COUNT is in effect, count >= 0xFFFF is encoded
   as 0xFFFF followed by a uint32 carrying the real count. Smaller
   counts still emit the historical uint16 — wire-byte-identical for
   every existing nested tag in the codebase. CECTag::WriteChildren /
   ReadChildren branch on socket.m_tx_flags / m_rx_flags &
   EC_FLAG_LARGE_TAG_COUNT, and CECTag::GetTagLen takes a new
   useLargeCount parameter so the wire-size estimate matches
   WriteChildren's iteration cap (writer and reader compute identical
   values, so m_dataLen subtraction stays consistent).

2. Capability negotiation through the existing auth handshake. New
   client sends EC_TAG_CAN_LARGE_TAG_COUNT in EC_OP_AUTH_REQ; new
   server records the capability and echoes the tag in EC_OP_AUTH_OK.
   ECSocket::WritePacket adds EC_FLAG_LARGE_TAG_COUNT to outgoing
   flags unconditionally — auto-stripped by 'flags &= m_my_flags'
   when not negotiated. This is what keeps mixed-version EC safe:
   without negotiation, both sides stay on the historical wire
   format, and WriteChildren caps outgoing counts at 0xFFFE both to
   fit in uint16 and to avoid emitting 0xFFFF (which a fix peer
   without the negotiated flag would still treat as plain count,
   matching old behaviour).

Compatibility matrix:
  fix daemon  <-> fix client  : sentinel format, no children-count cap
  pre-fix daemon <-> fix client: plain uint16, capped at 65535 (= today)
  fix daemon <-> pre-fix client: plain uint16, capped at 0xFFFE (silent
                                  truncation, matching the historical
                                  AddTag cap behaviour)
  pre-fix daemon <-> pre-fix    : unchanged

The historical AddTag cap ('cannot have more than 64k tags' at
m_tagList.size() >= 0xffff) is removed; in non-sentinel mode the
truncation is now done at WriteChildren time so the in-memory tag
list isn't artificially limited regardless of which peer the next
serialisation is bound for.

Verified end-to-end on Ubuntu 26.04: with a synthetic 70001-file
shared library, fix amulecmd 'show Shared' against fix amuled
returns the full 70001 entries (140002 row pairs). Pre-fix amuled
keeps capping at 65535, and a fix client connected to it falls back
to the plain-uint16 path with no ReadPacket parse errors.
@mrjimenez
mrjimenez merged commit 1a2e87d into amule-project:master May 11, 2026
12 checks passed
@got3nks
got3nks deleted the feature/ec-large-tag-count branch May 11, 2026 08:00
got3nks added a commit to got3nks/amule that referenced this pull request Jul 24, 2026
…mule-project#570)

The log views migrated to wxStyledTextCtrl (Scintilla) in amule-project#548 paint
their colours from the system theme once, in the constructor. The native
wxTextCtrl they replaced followed the platform appearance automatically;
Scintilla does not, so a live light/dark switch left the three log panes
(aMule Log, aMuleGUI Log, Server Info) stuck in the previous theme's
colours while the rest of the UI re-themed. Re-apply the styles on
wxEVT_SYS_COLOUR_CHANGED so the panes track the appearance.

Also guard against a foreground/background that resolve with too little
contrast to read: on macOS the window/text system colours are
appearance-aware and, on some wx builds, come back near-identical, which
paints the whole log invisible (amule-project#569). When the pair is unreadable, keep
the theme's background and force a legible foreground from its
brightness. Windows/GTK return static, well-contrasted colours and are
unaffected.
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.

2 participants