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/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/sema.go:105 +0x28
sync.(*RWMutex).Lock(0x104b54a78?)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/rwmutex.go:155 +0xf4
database/sql.(*Rows).close(0x1400011a280, {0x0, 0x0})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3443 +0x80
database/sql.(*Rows).Close(0x1400011a280)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3439 +0x30
panic({0x1047ad6c0?, 0x10481aef0?})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/panic.go:783 +0x120
reflect.Value.Addr({0x1047ca980?, 0x1400011e1b0?, 0x104819df0?})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/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/bun@v1.2.16/schema/scan.go:361 +0x78
github.com/uptrace/bun/schema.scanJSONIntoInterface({0x1047badc0?, 0x1400011e1e8?, 0x1047e6620?}, {0x1047a9500, 0x14000138198})
	/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/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/bun@v1.2.16/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/bun@v1.2.16/schema/field.go:125 +0xc4
github.com/uptrace/bun.(*structTableModel).scanColumn(0x14000150000, {0x14000154000, 0x4}, {0x1047a9500, 0x14000138198})
	/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/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/bun@v1.2.16/model_table_struct.go:294 +0x30
github.com/uptrace/bun.(*structTableModel).Scan(0x104a13430?, {0x1047a9500?, 0x14000138198?})
	/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_struct.go:290 +0x98
database/sql.convertAssignRows({0x10480b380, 0x14000150000}, {0x1047a9500, 0x14000138198}, 0x1400011a280)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/convert.go:394 +0x1a48
database/sql.(*Rows).scanLocked(0x1400011a280, {0x14000023bf8, 0x1, 0x14000023b28?})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3399 +0x1f0
database/sql.(*Rows).Scan(0x1400011a280, {0x14000023bf8, 0x1, 0x1400011a280?})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/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/bun@v1.2.16/model_table_struct.go:275 +0x58
github.com/uptrace/bun.(*sliceTableModel).ScanRows(0x14000150000, {0x10481eeb0, 0x1400013c000}, 0x1400011a280)
	/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/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/bun@v1.2.16/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/bun@v1.2.16/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/bun@v1.2.16/query_insert.go:614 +0x28c
github.com/uptrace/bun.(*InsertQuery).Exec(...)
	/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/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/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/sema.go:95 +0x28
internal/sync.(*Mutex).lockSlow(0x1400011a2b8)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/internal/sync/mutex.go:149 +0x170
internal/sync.(*Mutex).Lock(...)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/internal/sync/mutex.go:70
sync.(*Mutex).Lock(...)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/mutex.go:46
sync.(*RWMutex).Lock(0x1400011a2b8)
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/rwmutex.go:150 +0x6c
database/sql.(*Rows).close(0x1400011a280, {0x10481d000, 0x104a49940})
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/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/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3020 +0x19c
created by database/sql.(*Rows).initContextClose in goroutine 1
	/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions