Skip to content

fast-path: kill memmove libcall in finalize VLAN code#48

Merged
lunarthegrey merged 1 commit intomainfrom
v0.2.6-no-memmove-libcall
May 5, 2026
Merged

fast-path: kill memmove libcall in finalize VLAN code#48
lunarthegrey merged 1 commit intomainfrom
v0.2.6-no-memmove-libcall

Conversation

@lunarthegrey
Copy link
Copy Markdown
Contributor

Summary

v0.2.6 still rejected on UniFi 5.15-ui-cn9670. The verifier walks ~4400 instructions before failing (vs ~545 in v0.2.5), so the v0.2.6 mem::zeroed → struct-literal fix demonstrably worked — fast_path got past the FIB-struct init. But there's a second bpf-to-bpf libcall hiding in finalize: core::ptr::copy(src, dst, 6) in vlan_push/vlan_pop lowers to @llvm.memmove.*, and the BPF backend emits memmove as a separate subprogram (no libc to link).

Replace both ptr::copy calls with twelve byte-loads followed by twelve byte-stores. Reading all source bytes before any store also handles source/destination overlap that memmove was doing for us — without the libcall.

How this trips the kernel guard

Even though finalize itself doesn't bpf_tail_call, it's the tail-call target of fast_path. On kernels where bpf_jit_supports_subprog_tailcalls() returns false (UniFi's patched arm64 5.15 build), the verifier rejects either side of the program-array linkage if the combined system has tail-call + bpf-to-bpf-call. v0.2.5 tripped this from fast_path's mem::zeroed memset. v0.2.6 fixed that. v0.2.7 fixes finalize's memmove.

Test plan

  • CI green.
  • Verifier dump after install: no `(85) call pc+N` instructions in either fast_path or finalize (only kernel helpers like `call bpf_map_lookup_elem#1`, `call bpf_redirect_map#NN`, `call bpf_tail_call#12`).
  • `sudo packetframe feasibility --human` on UniFi: all xdp.attach.ethN PASS.
  • After merge: cut v0.2.7.

Note on the user-supplied log

The journal-tail captured ~last 100 lines of the verifier dump (up through instruction 4397). The actual rejection message at the end of the dump wasn't in the captured window. If this fix doesn't take, the next step is journalctl -u packetframe -n 5000 --no-pager | tail -200 to capture the rejection text — it'll either be a different bpf-to-bpf libcall or a separate verifier check entirely.

🤖 Generated with Claude Code

v0.2.6 was still rejected on UniFi 5.15-ui-cn9670: removing the
mem::zeroed memset got fast_path's verifier walk past instruction
4400 (vs ~545 in v0.2.5), but rejection still fires.

Outside-the-box culprit: `core::ptr::copy(src, dst, 6)` in
vlan_push/vlan_pop lowers to LLVM's @llvm.memmove.* intrinsic. The BPF
backend has no libc to link against, so it emits the memmove body as
a separate bpf-to-bpf subprogram. fast_path tail-calls into finalize,
which on JIT builds where `bpf_jit_supports_subprog_tailcalls()`
returns false (UniFi's arm64 5.15 patches) trips the same
"tail_calls are not allowed in non-JITed programs with bpf-to-bpf
calls" guard via the program-array linkage.

Replace each `ptr::copy(src, dst, 6)` with twelve byte loads followed
by twelve byte stores. Reading all source bytes before any store
also handles the source/destination overlap that memmove was
handling — without the libcall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lunarthegrey lunarthegrey merged commit fdca005 into main May 5, 2026
10 checks passed
@lunarthegrey lunarthegrey deleted the v0.2.6-no-memmove-libcall branch May 5, 2026 14:55
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