-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Background
btcjson.StringOrArray
declares its own MarshalJSON
method with a value receiver that simply calls json.Marshal(h)
.
Because the value still satisfies json.Marshaler
, json.Marshal
re-invokes MarshalJSON
, triggering infinite recursion and eventually a stack‐overflow panic at runtime.
This manifests anywhere a StringOrArray
is marshalled (e.g. GetNetworkInfoResult.Warnings
), crashing downstream apps that depend on btcd/btcjson
.
Your environment
- btcd / btcjson version:
v0.24.3-0.20250506233109-1eb974aab6ef
- Go version:
go1.24.2
- OS:
Darwin 23.4.0 arm64
(Apple Silicon macOS 14.4) - Reproducible on Linux/amd64 as well.
Steps to reproduce
package main
import (
"encoding/json"
"github.com/btcsuite/btcd/btcjson"
)
func main() {
val := btcjson.StringOrArray{"boom"}
_, _ = json.Marshal(val) // panic: runtime: stack overflow
}
Or run any program that marshals btcjson.GetNetworkInfoResult
when the
warnings
field is non-nil (see attached stack trace snippet).
Expected behaviour
StringOrArray
should serialize to:
"foo"
when length == 1["foo","bar"]
when length > 1
without panicking.
Actual behaviour
json.Marshal
enters an infinite loop inside StringOrArray.MarshalJSON
, eventually crashing with:
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow
...
encoding/json.marshalerEncoder → btcjson.StringOrArray.MarshalJSON → json.Marshal → (repeat)
Proposed fix (PR incoming)
Change the receiver to a pointer (or remove the method entirely) and add unit tests:
func (h *StringOrArray) MarshalJSON() ([]byte, error) {
return json.Marshal([]string(*h))
}
Tests confirming safe round-trips for single and multiple values are included.