Fix nil pointer dereference when sending to binary channel type alias

## Description

When sending a value to a channel that is a type alias defined in a binary package, yaegi panics with a nil pointer dereference.

This PR was created with AI assistance (Claude) as I hit this issue in a downstream project using yaegi. 
The fix and tests have been reviewed and verified for correctness and functionality by myself.

## Root Cause

In the `send` function (`interp/run.go`), the code directly accesses `c0.typ.val` to get the channel element type:

```go
value1 := genDestValue(c0.typ.val, c1)
```

For source-defined channels (category `chanT`), `.val` contains the element type. But for binary channel type aliases (category `valueT`), `.val` is nil - the element type is only available via reflection in `.rtype`.

## Fix

Use the existing `elem()` method which correctly handles both cases:

```go
value1 := genDestValue(c0.typ.elem(), c1)
```

The `elem()` method (in `type.go`) already handles this properly:

```go
func (t *itype) elem() *itype {
    if t.cat == valueT {
        return valueTOf(t.rtype.Elem())  // Binary types: create from reflect
    }
    return t.val  // Source types: use existing itype with all metadata
}
```

This is a one-line fix that fixes binary channel type aliases by using reflection to get the element type

## Test Results

| Test | Without Fix | With Fix |
|------|-------------|----------|
| `TestSendToSourceDefinedChannel` | PASS | PASS |
| `TestSendToSourceDefinedChannelTypeAlias` | PASS | PASS |
| `TestSendToBinaryChannelTypeAlias` | FAIL | PASS |

## Minimal Reproduction

### mypkg/mypkg.go

```go
package mypkg

// IntChan is a channel type alias
type IntChan chan int

// NewIntChan creates a new IntChan
func NewIntChan() IntChan {
    return make(IntChan, 1)
}
```

### symbols.go

```go
package main

import (
    "reflect"
    "yaegi-repro/mypkg"
)

var Symbols = map[string]map[string]reflect.Value{}

func init() {
    Symbols["yaegi-repro/mypkg/mypkg"] = map[string]reflect.Value{
        "IntChan":    reflect.ValueOf((*mypkg.IntChan)(nil)),
        "NewIntChan": reflect.ValueOf(mypkg.NewIntChan),
    }
}
```

### main.go

```go
package main

import (
    "fmt"
    "log"
    "github.com/traefik/yaegi/interp"
)

func main() {
    i := interp.New(interp.Options{})
    if err := i.Use(Symbols); err != nil {
        log.Fatal(err)
    }

    code := `
package main

import "yaegi-repro/mypkg"

func main() {
    ch := mypkg.NewIntChan()
    ch <- 42  // This line panics without the fix
    val := <-ch
    println("Received:", val)
}
`
    _, err := i.Eval(code)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Success!")
}
```

## Results

**Without fix:**
```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x128 pc=0x...]

goroutine 1 [running]:
github.com/traefik/yaegi/interp.(*itype).refType(0x0?, ...)
    .../interp/type.go:2065 +0x7a
github.com/traefik/yaegi/interp.(*itype).TypeOf(...)
    .../interp/type.go:2223
github.com/traefik/yaegi/interp.genDestValue(0x0, ...)
    .../interp/value.go:157 +0x25
github.com/traefik/yaegi/interp.send(...)
    .../interp/run.go:3766 +0x...
```

**With fix:**
```
Received: 42
Success!
```
This commit is contained in:
pk910
2026-02-09 09:56:05 +01:00
committed by GitHub
parent bdb988490e
commit fcb76d1ece
2 changed files with 93 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
package interp_test
import (
"reflect"
"testing"
"github.com/traefik/yaegi/interp"
)
// IntChan is a channel type alias for testing.
type IntChan chan int
// NewIntChan creates a new IntChan.
func NewIntChan() IntChan {
return make(IntChan, 1)
}
func TestSendToBinaryChannelTypeAlias(t *testing.T) {
i := interp.New(interp.Options{})
err := i.Use(interp.Exports{
"mypkg/mypkg": {
"IntChan": reflect.ValueOf((*IntChan)(nil)),
"NewIntChan": reflect.ValueOf(NewIntChan),
},
})
if err != nil {
t.Fatal(err)
}
_, err = i.Eval(`
package main
import "mypkg"
func main() {
ch := mypkg.NewIntChan()
ch <- 42
val := <-ch
if val != 42 {
panic("unexpected value")
}
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestSendToSourceDefinedChannel(t *testing.T) {
i := interp.New(interp.Options{})
// Test with a channel defined purely in interpreted code
_, err := i.Eval(`
package main
func main() {
ch := make(chan int, 1)
ch <- 42
val := <-ch
if val != 42 {
panic("unexpected value")
}
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestSendToSourceDefinedChannelTypeAlias(t *testing.T) {
i := interp.New(interp.Options{})
// Test with a channel type alias defined in interpreted code
_, err := i.Eval(`
package main
type MyChan chan int
func main() {
ch := make(MyChan, 1)
ch <- 42
val := <-ch
if val != 42 {
panic("unexpected value")
}
}
`)
if err != nil {
t.Fatal(err)
}
}
+1 -1
View File
@@ -3763,7 +3763,7 @@ func send(n *node) {
next := getExec(n.tnext)
c0, c1 := n.child[0], n.child[1]
value0 := genValue(c0) // Send channel.
value1 := genDestValue(c0.typ.val, c1)
value1 := genDestValue(c0.typ.elem(), c1)
if !n.interp.cancelChan {
// Send is non-cancellable, has the least overhead.