Skip to content

Fork RPC: refuse stock peers whose op numbers differ - #6

Draft
LeoBorcherding wants to merge 1 commit into
night-shift-tester/audit-pin-inkling-534c321from
night-shift-tester/audit-fork-rpc-op-numbers
Draft

LeoBorcherding wants to merge 1 commit into
night-shift-tester/audit-pin-inkling-534c321from
night-shift-tester/audit-fork-rpc-op-numbers

Conversation

@LeoBorcherding

@LeoBorcherding LeoBorcherding commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Audit finding GL-3 (P2, confirmed-ran). Fork RPC: a fork build and a stock one passed each other's handshake while numbering their ops differently.

Base of this PR. The op renumbering comes from the Inkling pin (ggml-org/llama.cpp#25731, pinned in scripts/unsloth/pr-set.json at 534c32182, which is also that PR's current head), not the fork's master tree, and that pin also edits the ggml-rpc.h lines this touches. So this PR targets night-shift-tester/audit-pin-inkling-534c321 (that pin commit) rather than night-shift-tester/audit-base, which keeps the diff to the fix. Landing it means a separate fix pin (an unslothai/llama.cpp PR listed right after the Inkling pin), not a commit on the Inkling PR: the minor bump below is a fork marker, and upstream wouldn't take it. Drop the fix pin together with the Inkling pin once a base tag carries Inkling.

What's wrong

The Inkling pin inserts GGML_OP_FLASH_ATTN_EXT_BANDED after GGML_OP_FLASH_ATTN_EXT (ggml/include/ggml.h:576 at 534c32182), so every op after it moves up by one, and only bumps the patch version (ggml/include/ggml-rpc.h:9-11):

#define RPC_PROTO_MAJOR_VERSION    8
#define RPC_PROTO_MINOR_VERSION    0
#define RPC_PROTO_PATCH_VERSION    1

The client only rejects a different major or a newer minor, and the server never checks the client at all (ggml/src/ggml-rpc/ggml-rpc.cpp:427):

    if (response.major != RPC_PROTO_MAJOR_VERSION || response.minor > RPC_PROTO_MINOR_VERSION) {

The server then copies the op number as-is (ggml-rpc.cpp:1661, result->op = (ggml_op) tensor->op;). So a fork llama-server --rpc with a stock rpc-server, or the reverse, connects and runs the wrong ops (stock UNARY 91 is DSV4_HC_POST on the fork). Every Unsloth bundle is built with GGML_RPC=ON and Unsloth Studio passes --rpc through, and the mix reports the same build number as stock, so nothing tells the user the two sides differ.

Fix

  • RPC_PROTO_MINOR_VERSION goes to 1. Stock clients already refuse a server with a newer minor, so they now refuse a fork server.
  • The client requires the server's minor and patch to equal its own (major already had to). Patch is what upstream bumps when GGML_OP_COUNT changes (the static_assert right below says so), so a matching patch is the only sign the op numbers line up. That makes a fork client refuse a stock server, and the error now prints both versions.

I picked this over moving GGML_OP_FLASH_ATTN_EXT_BANDED to just before GGML_OP_COUNT: the move would also need the positional name and symbol tables in ggml.c reordered inside upstream's PR, it would still leave a stock server receiving op 101 (its GGML_OP_COUNT), and every later upstream push to that PR would conflict with it.

Tradeoff: fork and stock RPC peers can no longer be mixed at all (they never worked correctly), and two fork builds with different patch versions now refuse each other where they used to connect.

Test

No RPC test builds on Windows (test-rpc-multi-server is UNIX-only), and a single binary can't stand in for a stock peer, so the check is two real builds talking to each other: stock ggml-rpc-server and llama-cli from fcac19079 (the ggml-org master commit the Inkling pin last merged), and the pin's, with llama-cli --rpc 127.0.0.1:<port> --list-devices as the client. CPU, Windows 11, MSVC static builds with -DGGML_RPC=ON.

Red, at the pin. Both directions connect:

== pin client -> stock server (v8.0.0)
Available devices:
  RPC0: 127.0.0.1:50611 (49102 MiB, 27706 MiB free)
== stock client -> pin server (v8.0.1)
Available devices:
  RPC0: 127.0.0.1:50612 (49102 MiB, 26252 MiB free)

Green, with the fix (server now reports v8.1.1). Both mixed directions refuse at connect, fork to fork still connects:

== fix client -> stock server
0.00.029.289 E RPC server version mismatch: 8.0.0, client is 8.1.1
...ggml-rpc.cpp:651: RPC handshake failed for 127.0.0.1:50611
== stock client -> fix server
0.00.033.934 E RPC server version mismatch: 8.1.1
...ggml-rpc.cpp:648: RPC handshake failed for 127.0.0.1:50612
== fix client -> fix server
Available devices:
  RPC0: 127.0.0.1:50612 (49102 MiB, 26866 MiB free)

Testing on Linux, with a real model (Colab)

Colab L4 VM, CPU builds only (-DGGML_RPC=ON -DGGML_CUDA=OFF), gcc 13.3.0, cmake 3.31.10, Ubuntu 24.04.4, 12 vCPU. Stock fcac19079f (proto 8.0.0, GGML_OP_COUNT 101), pin 534c321824 (8.0.1, 102), fix 20fd7f90f9 (8.1.1). Every client and server pair with llama-cli --rpc 127.0.0.1:<port> --list-devices, then llama-completion on SmolLM2-135M-Instruct-Q8_0.gguf with -ngl 99 --device RPC0, so the whole graph runs on the RPC server.

client \ server stock 8.0.0 pin 8.0.1 fix 8.1.1
stock connects connects (bug) refused: RPC server version mismatch: 8.1.1
pin connects (bug) connects refused: RPC server version mismatch: 8.1.1
fix refused: RPC server version mismatch: 8.0.0, client is 8.1.1 refused: 8.0.1, client is 8.1.1 connects

The mixed pair on a real model, pin client and stock server:

stock server: ggml/src/ggml-cpu/ggml-cpu.c:2502: fatal error   (rpc_server::graph_compute -> ggml_graph_plan)

ggml-cpu.c:2502 at fcac19079 is the case GGML_OP_COUNT: GGML_ABORT("fatal error") arm: the stock server got op 101, its own GGML_OP_COUNT, from the pin client. Fix client to fix server finished all 23 decode steps with no error.

Not tested

  • No macOS build, and no llama-server --rpc (the Linux run used llama-completion); the handshake is the same code for every client.
  • No GPU backend on either side of the RPC link.
  • A refused handshake aborts the client (GGML_ABORT in the existing connect path), as any version mismatch already did; turning that into a clean error is out of scope here.

GGML_OP_FLASH_ATTN_EXT_BANDED sits in the middle of enum ggml_op, so every op after it has a
different number than in stock llama.cpp, while both sides still reported RPC 8.0.x and the
client only compared major and an older minor. A fork client and a stock server (or the other
way round) passed the handshake and then ran the wrong ops.

Bump the minor version, which stock clients refuse as newer, and have the client require the
server's minor and patch to match its own, so a fork client refuses a stock server as well.
@LeoBorcherding

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T09:06:36.449375Z 20fd7f9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 20fd7f90f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant