PaperMC / PaperMC/Velocity

Intermittent "Unknown node type 3" decoding AvailableCommandsPacket for OP players — colliding argument-type ids resolved by non-deterministic identity-hash order

Open
#1,868 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
2.3k
Forks
960
Avg merge
5d 10h
Merged PRs (30d)
2

Description

Velocity — intermittent "Unknown node type 3" decoding AvailableCommandsPacket for OP players (per-JVM-start, cured by restart)

Summary

On Velocity 3.5.1, operator players on the proxy's highest native protocol (1.21.11 / 774)
are sometimes kicked when receiving the full command tree from any backend:

io.netty.handler.codec.CorruptedFrameException: Error decoding AvailableCommandsPacket
    Direction CLIENTBOUND Protocol 1.21.11 State PLAY ID 0x10
Caused by: java.lang.IllegalArgumentException: Unknown node type 3
    at ...AvailableCommandsPacket.deserializeNode(AvailableCommandsPacket.java:224)

Whether it happens is decided at proxy startup and stays fixed for the whole JVM session:
some starts are fine, some kick every OP on 774 from every 774 backend until restart.
A restart "cures" it (probabilistically). Non-OP players and lower (Via-translated) versions
are never affected.

Reproduction matrix (verified live): OP+1.21.11 → kick; deop+1.21.11 → fine; OP+1.17.1 → fine.
The captured failing packet decodes cleanly in a freshly-started instance (bytes are valid).

Root cause (confirmed from bytecode + two heap dumps)

ArgumentPropertyRegistry maps a numeric argument id → ArgumentIdentifier via:

public static ArgumentIdentifier readIdentifier(ByteBuf buf, ProtocolVersion v) {
    int id = readVarInt(buf);
    for (ArgumentIdentifier i : byIdentifier.keySet()) {   // HashMap iteration order
        Integer x = i.getIdByProtocolVersion(v);
        if (x == null || x != id) continue;
        return i;                                          // returns the FIRST match
    }
    throw new IllegalArgumentException("Argument type identifier " + id + " unknown.");
}

Two problems combine:

  1. Structural id collisions. Velocity's Forge/CrossStitch argument identifiers reuse the same
    registry ids as vanilla ones for recent protocols. Verified in the heap for both a healthy and a
    corrupted proxy:
    forge:enum   and minecraft:template_rotation  -> id 50   (protocols 771,773,774,776...)
    forge:modid  and minecraft:heightmap          -> id 51
    
  2. Non-deterministic resolution. ArgumentIdentifier does not override hashCode/equals,
    so byIdentifier.keySet() iteration order follows identity hashes, which differ on every JVM
    start. readIdentifier returns the first identifier whose id matches — so for a colliding id it
    returns forge:enum on some starts and minecraft:template_rotation on others.

When a start happens to iterate the Forge identifier first, readIdentifier(50, 774) returns
forge:enum instead of template_rotation → the wrong ArgumentPropertySerializer reads the
argument properties with the wrong length → the parser desyncs → the next node's flags byte is
garbage → node type == 3 → exception. This only surfaces for OP players (only the full command
tree contains a node using a colliding argument id) and on the proxy's top native version, and it is
global (shared static registry). A restart re-randomizes identity hashes, hence the ~1-in-N
"lucky vs unlucky" boot and why restarting cures it.

Why the daily restart doesn't fix it

The scheduled restart re-rolls the ordering each morning; most days are "lucky". On an "unlucky"
boot every OP is kicked until the next restart (up to ~24h). It's a probabilistic boot condition,
not a time-based degradation.

Suggested fix (upstream)

  • Make readIdentifier deterministic on id collisions (e.g. prefer the vanilla minecraft:*
    identifier over Forge/CrossStitch), and/or
  • give ArgumentIdentifier a stable hashCode/equals plus a deterministic tie-break, and/or
  • ensure Forge/CrossStitch VersionSets don't overlap vanilla ids for a given protocol.

Environment

  • Velocity 3.5.1, Java 25.0.3 (Eclipse Adoptium), Linux 6.1 amd64
  • ViaVersion 5.11.0 + ViaBackwards 5.11.0 (advertises up to 26.2/776)
  • Backends: Purpur 1.21.11 (protocol 774); mixed-version network

Evidence

  • Two heap dumps (an "unlucky/corrupt" session and a "lucky/healthy" one)
  • The exact failing AvailableCommandsPacket (decodes OK on a clean instance)
  • Extracted registry showing the structural id collisions (forge:enum/template_rotation=50, forge:modid/heightmap=51)
  • /velocity dump output

Related

  • #913 (modded command argument types causing decode errors)
  • #1365 (Error decoding AvailableCommandsPacket)

Heap dumps and the raw failing packet are available on request (kept off the public issue as they contain server data). Happy to run any additional diagnostics.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ArgumentPropertyRegistry.readIdentifier and inspect how ArgumentIdentifier entries are selected for protocol 774; then trace the result into AvailableCommandsPacket.deserializeNode. Confirm the handling of colliding identifiers is deterministic and verify that the full OP command tree decodes without the reported node-type error.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.