diff --git a/pkg/frost/signing/native_ffi_executor_adapter.go b/pkg/frost/signing/native_ffi_executor_adapter.go index c655c50d58..a839992f17 100644 --- a/pkg/frost/signing/native_ffi_executor_adapter.go +++ b/pkg/frost/signing/native_ffi_executor_adapter.go @@ -73,7 +73,20 @@ func (nefea *nativeExecutionFFIExecutorAdapter) Execute( ctx context.Context, logger log.StandardLogger, request *Request, -) (*Result, error) { +) (result *Result, err error) { + // Recover any panic originating along the cgo/FFI signing path (e.g. a + // malformed or oversized response from the native signer) and surface it as + // a failed attempt instead of crashing the whole signing process. The outer + // tBTC signingRetryLoop then handles this attempt cleanly. + defer func() { + if r := recover(); r != nil { + result = nil + err = fmt.Errorf( + "native FFI signing panicked at the cgo boundary: %v", r, + ) + } + }() + if request == nil { return nil, fmt.Errorf("request is nil") } diff --git a/pkg/frost/signing/native_ffi_executor_adapter_test.go b/pkg/frost/signing/native_ffi_executor_adapter_test.go index 8c4ddafccf..62d6a73d35 100644 --- a/pkg/frost/signing/native_ffi_executor_adapter_test.go +++ b/pkg/frost/signing/native_ffi_executor_adapter_test.go @@ -39,6 +39,47 @@ func (mnefsp *mockNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( mnefsp.lastChannel = channel } +// panickingFFISigningPrimitive panics in Sign, standing in for a panic raised +// along the cgo/FFI path (e.g. a C.GoBytes length-out-of-range on a malformed +// native-signer response). +type panickingFFISigningPrimitive struct{} + +func (panickingFFISigningPrimitive) Sign( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + panic("simulated cgo boundary panic") +} + +func (panickingFFISigningPrimitive) RegisterUnmarshallers(channel net.BroadcastChannel) {} + +func TestNativeExecutionFFIExecutorAdapter_Execute_RecoversCgoBoundaryPanic( + t *testing.T, +) { + executor, err := NewNativeExecutionFFIExecutorAdapter(panickingFFISigningPrimitive{}) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + result, err := executor.Execute(context.Background(), nil, &Request{ + Message: big.NewInt(1), + SignerMaterial: []byte{0x01}, + }) + if err == nil { + t.Fatal("expected an error from the recovered panic, got nil") + } + if result != nil { + t.Fatalf("expected a nil result on a recovered panic, got [%v]", result) + } + if !strings.Contains(err.Error(), "panicked at the cgo boundary") { + t.Fatalf( + "expected a cgo-boundary panic error, got: [%v]", + err, + ) + } +} + func TestNewNativeExecutionFFIExecutorAdapter_NilPrimitive(t *testing.T) { _, err := NewNativeExecutionFFIExecutorAdapter(nil) if err == nil { diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 348d0cabd7..26d2c70dfa 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -378,6 +378,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math" "unsafe" ) @@ -2636,7 +2637,15 @@ func callBuildTaggedTBTCSignerOperation( } requestPtr := C.CBytes(requestPayload) - defer C.free(requestPtr) + requestLen := len(requestPayload) + defer func() { + // Scrub the secret request bytes from the C heap before releasing them. + // The request payload can carry signing-share / nonce material, and a + // plain C.free does not overwrite; this mirrors the Go-side zeroBytes + // hygiene applied to the caller's own copy. + zeroBytes(unsafe.Slice((*byte)(requestPtr), requestLen)) + C.free(requestPtr) + }() result := call((*C.uint8_t)(requestPtr), C.size_t(len(requestPayload))) return parseBuildTaggedTBTCSignerResult(operation, result) @@ -2659,6 +2668,20 @@ func parseBuildTaggedTBTCSignerResult( var payload []byte if result.buffer.ptr != nil && result.buffer.len > 0 { + // Guard the size_t -> C.int narrowing in C.GoBytes: a length that does + // not fit in a C.int (>= 2^31) would overflow to a negative value and + // panic ("length out of range") at the cgo boundary, or silently + // truncate to a wrong length. A response that large is never valid, so + // reject it; the buffer is still released by the deferred free above. + if uint64(result.buffer.len) > uint64(math.MaxInt32) { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf( + "response buffer length [%d] exceeds maximum [%d]", + uint64(result.buffer.len), math.MaxInt32, + ), + ) + } payload = C.GoBytes(unsafe.Pointer(result.buffer.ptr), C.int(result.buffer.len)) }