EC: lift the 65535 children-per-tag wire-format ceiling (#199) - #570
Merged
mrjimenez merged 1 commit intoMay 11, 2026
Merged
Conversation
…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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 wasEC_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:
Sentinel-extended wire format. When the negotiated EC flag
EC_FLAG_LARGE_TAG_COUNTis in effect, a count>= 0xFFFFis encoded as0xFFFFfollowed by auint32carrying the real count. Smaller counts still emit the historicaluint16— wire-byte-identical for every existing nested tag in the codebase.CECTag::WriteChildren/ReadChildrenbranch onsocket.m_tx_flags/m_rx_flags & EC_FLAG_LARGE_TAG_COUNT, andCECTag::GetTagLentakes a newuseLargeCountparameter so writer and reader compute identical wire-size estimates andm_dataLensubtraction stays consistent.Capability negotiation through the existing auth handshake. New client sends
EC_TAG_CAN_LARGE_TAG_COUNTinEC_OP_AUTH_REQ; new server records the capability and echoes the tag inEC_OP_AUTH_OK.ECSocket::WritePacketaddsEC_FLAG_LARGE_TAG_COUNTto outgoing flags unconditionally — auto-stripped byflags &= m_my_flagswhen not negotiated. This is what keeps mixed-version EC safe: without negotiation, both sides stay on the historical wire format, andWriteChildrencaps outgoing counts at0xFFFEboth to fit inuint16and to avoid emitting0xFFFF(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_COUNTis negotiated both sides know the format, so the count field could in principle just always be auint32. The sentinel choice optimises for the common case:uint16(count)for counts under 0xFFFF — 2 bytes — and only the rare large-count tag pays the extra 4 bytes (0xFFFFmarker +uint32real count, 6 bytes total). Always-uint32would 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.uint16); the only added cost is one comparison against0xFFFF.Always-
uint32would 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:
AddTag)uint16capped at0xFFFE, no parse erroruint16, no sentinel collisionMixed-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_COUNTin auth, so the daemon falls back to the historical wire format for them. To opt in, an external client adds the emptyEC_TAG_CAN_LARGE_TAG_COUNT(0x0011) tag to itsEC_OP_AUTH_REQ, watches for the same tag inEC_OP_AUTH_OK, and on a hit handles the sentinel branch in itsTAGCOUNTreader (~10–15 lines).