Repository: multiformats/go-multiaddr
File: protocols.go (onion / onion3 transcoder)
Severity: Medium
Affects: libp2p, IPFS, go-ethereum devp2p, Filecoin, Celestia, Arbitrum, and every Go project using go-multiaddr for P2P networking
What happened?
onionBtS and onion3BtS access fixed byte offsets in the input slice without checking len(b) first:
func onionBtS(b []byte) (string, error) {
host := b[:10] // ← panic if len(b) < 10
port := binary.BigEndian.Uint16(b[10:12]) // ← panic if len(b) < 12
return fmt.Sprintf("%s:%d", base32.StdEncoding.EncodeToString(host), port), nil
}
func onion3BtS(b []byte) (string, error) {
host := b[:35] // ← panic if len(b) < 35
port := binary.BigEndian.Uint16(b[35:37]) // ← panic if len(b) < 37
return fmt.Sprintf("%s:%d", base32.StdEncoding.EncodeToString(host), port), nil
}
The normal NewMultiaddrBytes code path validates lengths before calling these transcoders, but multiaddr.Cast(untrustedBytes) bypasses that validation. Calling .String() on a Cast-ed multiaddr with a short ONION/ONION3 payload then reaches the transcoder and panics.
Expected behavior
onionBtS and onion3BtS should return an error (not panic) when the input slice is shorter than the required fixed size.
Actual behavior
panic: runtime error: slice bounds out of range [:35] with length 2
Steps to reproduce
package main
import (
ma "github.com/multiformats/go-multiaddr"
"fmt"
)
func main() {
// 2-byte payload claiming to be ONION3 — too short
malicious := []byte{
0x00, 0x25, // varint for ONION3 protocol code
0x00, 0x01, // 2-byte payload (need 37)
}
m := ma.Cast(malicious) // bypasses length validation
fmt.Println(m.String()) // panics: slice bounds out of range [:35] with length 2
}
goroutine 1 [running]:
github.com/multiformats/go-multiaddr.onion3BtS(...)
protocols.go:...
panic: runtime error: slice bounds out of range [:35] with length 2
exit status 2
Suggested fix
Add a length guard at the top of each transcoder:
func onionBtS(b []byte) (string, error) {
if len(b) < 12 {
return "", fmt.Errorf("onion address too short: %d bytes, want 12", len(b))
}
host := b[:10]
port := binary.BigEndian.Uint16(b[10:12])
return fmt.Sprintf("%s:%d", base32.StdEncoding.EncodeToString(host), port), nil
}
func onion3BtS(b []byte) (string, error) {
if len(b) < 37 {
return "", fmt.Errorf("onion3 address too short: %d bytes, want 37", len(b))
}
host := b[:35]
port := binary.BigEndian.Uint16(b[35:37])
return fmt.Sprintf("%s:%d", base32.StdEncoding.EncodeToString(host), port), nil
}
Impact
Any application using go-multiaddr that calls Cast() on untrusted bytes (e.g., received from the P2P network), or any codec implementation that builds multiaddrs from deserialized data without replicating the exact validation logic, can be crashed remotely with a single malformed peer address advertisement.
Discovery method : Zorya concolic executor
This bug was found using Zorya, a concolic execution engine for Go binaries. Zorya symbolized the n (claimed payload length) parameter and used Z3 to produce satisfying assignments for all OOB paths in both transcoders.
onion3BtS : function 0x4b9040:
zorya zorya_onion_real \
--mode function 0x4b9040 \
--thread-scheduling main-only \
--lang go \
--compiler gc \
--arg "0c00000000000000" \
--negate-path-exploration
--arg "0c00000000000000" is the little-endian int64 encoding of 12 (a safe seed: full onion address length).
| Z3 Witness |
Opcode |
Elapsed |
Behaviour |
n = MinInt64 |
INT_SUB |
60 s |
Signed subtraction wraps → OOB |
n = 38 (> 37) |
CBRANCH |
68 s |
OOB beyond fixed buffer |
n = 0 |
LOAD |
90 s |
b[:35] on empty slice |
onionBtS : function 0x4b9280:
zorya zorya_onion_real \
--mode function 0x4b9280 \
--thread-scheduling main-only \
--lang go \
--compiler gc \
--arg "0c00000000000000" \
--negate-path-exploration
| Z3 Witness |
Opcode |
Elapsed |
Behaviour |
n = MinInt64 |
INT_SUB |
61 s |
Same signed-subtraction path |
n = 38 (> 12) |
CBRANCH |
68 s |
OOB beyond 12-byte expected |
n = 0 |
LOAD |
89 s |
b[:10] on empty slice |
Binary: targets/_third_party/go-multiaddr/src/cmd/zorya_onion/zorya_onion_real
Repository: multiformats/go-multiaddr
File:
protocols.go(onion / onion3 transcoder)Severity: Medium
Affects: libp2p, IPFS, go-ethereum devp2p, Filecoin, Celestia, Arbitrum, and every Go project using go-multiaddr for P2P networking
What happened?
onionBtSandonion3BtSaccess fixed byte offsets in the input slice without checkinglen(b)first:The normal
NewMultiaddrBytescode path validates lengths before calling these transcoders, butmultiaddr.Cast(untrustedBytes)bypasses that validation. Calling.String()on aCast-ed multiaddr with a short ONION/ONION3 payload then reaches the transcoder and panics.Expected behavior
onionBtSandonion3BtSshould return an error (not panic) when the input slice is shorter than the required fixed size.Actual behavior
panic: runtime error: slice bounds out of range [:35] with length 2Steps to reproduce
Suggested fix
Add a length guard at the top of each transcoder:
Impact
Any application using go-multiaddr that calls
Cast()on untrusted bytes (e.g., received from the P2P network), or any codec implementation that builds multiaddrs from deserialized data without replicating the exact validation logic, can be crashed remotely with a single malformed peer address advertisement.Discovery method : Zorya concolic executor
This bug was found using Zorya, a concolic execution engine for Go binaries. Zorya symbolized the
n(claimed payload length) parameter and used Z3 to produce satisfying assignments for all OOB paths in both transcoders.onion3BtS: function0x4b9040:--arg "0c00000000000000"is the little-endian int64 encoding of12(a safe seed: full onion address length).n = MinInt64n = 38(> 37)n = 0b[:35]on empty sliceonionBtS: function0x4b9280:n = MinInt64n = 38(> 12)n = 0b[:10]on empty sliceBinary:
targets/_third_party/go-multiaddr/src/cmd/zorya_onion/zorya_onion_real