Skip to content

Panic in scanJSON causes deadlock #1306

@meetmorrowsolonmars

Description

@meetmorrowsolonmars

Go version: go1.25.0 darwin/arm64.
Bun version: v1.2.16
PostgreSQL version: 17

I worked with a nullable JSONB column in PostgreSQL and found a case where an uninitialized empty interface in the model definition causes the scanJSON (code) function to panic. In this case , theExec and Scan methods stay blocked and the database connection has idle in transaction state.

In the code snippet below, I want to insert two rows: one with JSONB data and one without (expecting NULL in that column). The nullzero annotation forces query executor to select data from the table (query will have RETURNING data statement). We can see that the Data field of the second slice element is uninitialized, so it is empty interface. Empty interface is a unaddressable value, so in scanJSON function we can not call dest.Addr().Interface() for it.

package main

import (
	"context"
	"database/sql"
	"log"
	"time"

	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/pgdialect"
	"github.com/uptrace/bun/driver/pgdriver"
)

func main() {
	dsn := "postgres://postgres:@localhost:55432/postgres?sslmode=disable"
	sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))

	db := bun.NewDB(sqldb, pgdialect.New())

	log.Println("Connected to database")

	_, err := db.Exec("CREATE TABLE IF NOT EXISTS test (id BIGINT PRIMARY KEY, data JSONB); TRUNCATE test;")
	if err != nil {
		log.Fatalln(err)
	}

	log.Println("Created \"test\" table")

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	rows := []model{
		{
			ID: 1,
			Data: map[string]any{
				"foo": "bar",
			},
		},
		{
			ID: 2,
		},
	}

	_, err = db.NewInsert().
		Model(&rows).
		Exec(ctx)
	if err != nil {
		log.Fatalln(err)
	}

	log.Println(rows)
}

type model struct {
	bun.BaseModel `bun:"table:test"`

	ID   int64 `bun:"id,pk"`
	Data any   `bun:"data,nullzero,type:jsonb"`
}

Stack trace:

2025/11/25 21:52:20 Connected to database
2025/11/25 21:52:20 Created "test" table
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.RWMutex.Lock]:
sync.runtime_SemacquireRWMutex(0x10?, 0x60?, 0x14000002301?)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/runtime/sema.go:105 +0x28
sync.(*RWMutex).Lock(0x104b54a78?)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/sync/rwmutex.go:155 +0xf4
database/sql.(*Rows).close(0x1400011a280, {0x0, 0x0})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3443 +0x80
database/sql.(*Rows).Close(0x1400011a280)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3439 +0x30
panic({0x1047ad6c0?, 0x10481aef0?})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/runtime/panic.go:783 +0x120
reflect.Value.Addr({0x1047ca980?, 0x1400011e1b0?, 0x104819df0?})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/reflect/value.go:268 +0x64
github.com/uptrace/bun/schema.scanJSON({0x1047ca980?, 0x1400011e1b0?, 0x104b54a78?}, {0x1047a9500?, 0x14000138198?})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/schema/scan.go:361 +0x78
github.com/uptrace/bun/schema.scanJSONIntoInterface({0x1047badc0?, 0x1400011e1e8?, 0x1047e6620?}, {0x1047a9500, 0x14000138198})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/schema/scan.go:535 +0x140
github.com/uptrace/bun/schema.(*Field).ScanWithCheck(0x1047e6620?, {0x1047badc0?, 0x1400011e1e8?, 0x104780f70?}, {0x1047a9500?, 0x14000138198?})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/schema/field.go:132 +0x88
github.com/uptrace/bun/schema.(*Field).ScanValue(0x14000142300, {0x1047e6620?, 0x1400011e1e0?, 0x0?}, {0x1047a9500, 0x14000138198})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/schema/field.go:125 +0xc4
github.com/uptrace/bun.(*structTableModel).scanColumn(0x14000150000, {0x14000154000, 0x4}, {0x1047a9500, 0x14000138198})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:314 +0xc0
github.com/uptrace/bun.(*structTableModel).ScanColumn(0x14000150000, {0x14000154000, 0x4}, {0x1047a9500?, 0x14000138198?})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:294 +0x30
github.com/uptrace/bun.(*structTableModel).Scan(0x104a13430?, {0x1047a9500?, 0x14000138198?})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:290 +0x98
database/sql.convertAssignRows({0x10480b380, 0x14000150000}, {0x1047a9500, 0x14000138198}, 0x1400011a280)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/convert.go:394 +0x1a48
database/sql.(*Rows).scanLocked(0x1400011a280, {0x14000023bf8, 0x1, 0x14000023b28?})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3399 +0x1f0
database/sql.(*Rows).Scan(0x1400011a280, {0x14000023bf8, 0x1, 0x1400011a280?})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3374 +0x9c
github.com/uptrace/bun.(*structTableModel).scanRow(0x14000150000, {0x10481eeb0, 0x1400013c000}, 0x1400011a280, {0x14000023bf8, 0x1, 0x1})
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/model_table_struct.go:275 +0x58
github.com/uptrace/bun.(*sliceTableModel).ScanRows(0x14000150000, {0x10481eeb0, 0x1400013c000}, 0x1400011a280)
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/model_table_slice.go:76 +0x234
github.com/uptrace/bun.(*baseQuery)._scan(0x1400000c288?, {0x10481eeb0, 0x1400013c000}, {0x10481ef90?, 0x14000140000?}, {0x14000152000, 0x5c}, {0x10481e1e0, 0x14000150000}, 0x0)
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/query_base.go:622 +0xbc
github.com/uptrace/bun.(*baseQuery).scan(0x14000140000, {0x10481eeb0?, 0x1400013c000?}, {0x10481ef90, 0x14000140000}, {0x14000152000, 0x5c}, {0x10481e1e0, 0x14000150000}, 0x0)
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/query_base.go:604 +0xa0
github.com/uptrace/bun.(*InsertQuery).scanOrExec(0x14000140000, {0x10481eeb0, 0x1400013c000}, {0x0, 0x0, 0x0}, 0x0)
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/query_insert.go:614 +0x28c
github.com/uptrace/bun.(*InsertQuery).Exec(...)
	/Users/gopher/go/pkg/mod/github.com/uptrace/[email protected]/query_insert.go:569
main.main()
	/Users/gopher/Projects/sql-db-issue/bun/main.go:46 +0x318

goroutine 34 [sync.Mutex.Lock]:
internal/sync.runtime_SemacquireMutex(0x104a23700?, 0x50?, 0x14000049e18?)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/runtime/sema.go:95 +0x28
internal/sync.(*Mutex).lockSlow(0x1400011a2b8)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/internal/sync/mutex.go:149 +0x170
internal/sync.(*Mutex).Lock(...)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/internal/sync/mutex.go:70
sync.(*Mutex).Lock(...)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/sync/mutex.go:46
sync.(*RWMutex).Lock(0x1400011a2b8)
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/sync/rwmutex.go:150 +0x6c
database/sql.(*Rows).close(0x1400011a280, {0x10481d000, 0x104a49940})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3443 +0x80
database/sql.(*Rows).awaitDone(0x1400011a280, {0x10481eeb0, 0x1400013c000}, {0x0, 0x0}, {0x10481ee40, 0x14000158050})
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:3020 +0x19c
created by database/sql.(*Rows).initContextClose in goroutine 1
	/Users/gopher/go/pkg/mod/golang.org/[email protected]/src/database/sql/sql.go:2996 +0x12c

As we can see, this code snippet causes panic in the scanJSON function and blocks the goroutine and the database connection. This case is difficult to catch because it does not return any error.

Related issue golang/go#76465.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions