Skip to content

Panic encoding when non-nil interfaces with nil values occur #2453

@jaltavilla

Description

@jaltavilla

Describe the bug
We've encountered panics when encoding composite types and arrays due to nil checks not handling non-nil interfaces with nil values. Since the nil check returns a false negative later code goes on to call a value receiver method and panic.
Here are a few places where we've encountered them:

To Reproduce
The below example shows cases that cause both the array and composite codecs to trigger a panic.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

type Child struct {
	field1 string
	field2 int32
}

type Parent struct {
	field1 int32
	field2 *Child
}

type ParentWithSlice struct {
	field1 int32
	field2 []*Child
}

func main() {
	conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close(context.Background())

	_, err = conn.Exec(context.Background(),
		`DROP TYPE IF EXISTS parent_with_slice_t CASCADE;
		 DROP TYPE IF EXISTS parent_t CASCADE;
		 DROP TYPE IF EXISTS child_t CASCADE;
		 CREATE TYPE child_t AS ( field1 text, field2 int4 );
		 CREATE TYPE parent_t AS ( field1 int4, field2 child_t );
		 CREATE TYPE parent_with_slice_t AS ( field1 int4, field2 child_t[] );
		 CREATE OR REPLACE PROCEDURE do_something(parent parent_t)
		 LANGUAGE plpgsql
		 AS $$
		 BEGIN
		 END;
		 $$;
		 CREATE OR REPLACE PROCEDURE do_something_else(parent parent_with_slice_t)
		 LANGUAGE plpgsql
		 AS $$
		 BEGIN
		 END;
		 $$;`)
	if err != nil {
		log.Fatal(err)
	}

	types, err := conn.LoadTypes(context.Background(), []string{"child_t", "parent_t", "parent_with_slice_t"})
	if err != nil {
		log.Fatal(err)
	}
	conn.TypeMap().RegisterTypes(types)

	// composite codec panic
	input1 := Parent{
		field1: 1,
		field2: nil,
	}
	_, err = conn.Exec(context.Background(), "CALL do_something( $1 )", input1)
	if err != nil {
		log.Fatal(err)
	}

	// array codec panic
	input2 := ParentWithSlice{
		field1: 1,
		field2: []*Child{nil},
	}
	_, err = conn.Exec(context.Background(), "CALL do_something_else( $1 )", input2)
	if err != nil {
		log.Fatal(err)
	}
}

func (value Child) IsNull() bool { return false }
func (value Child) Index(i int) any {
	switch i {
	case 0:
		return value.field1
	case 1:
		return value.field2
	}
	return fmt.Errorf("%s unknown field requested - %d is out of bounds", "Child", i)
}

var _ pgtype.CompositeIndexGetter = Child{}

func (value Parent) IsNull() bool { return false }
func (value Parent) Index(i int) any {
	switch i {
	case 0:
		return value.field1
	case 1:
		return value.field2
	}
	return fmt.Errorf("%s unknown field requested - %d is out of bounds", "Parent", i)
}

var _ pgtype.CompositeIndexGetter = Parent{}

func (value ParentWithSlice) IsNull() bool { return false }
func (value ParentWithSlice) Index(i int) any {
	switch i {
	case 0:
		return value.field1
	case 1:
		return value.field2
	}
	return fmt.Errorf("%s unknown field requested - %d is out of bounds", "ParentWithSlice", i)
}

var _ pgtype.CompositeIndexGetter = ParentWithSlice{}

Please run your example with the race detector enabled. For example, go run -race main.go or go test -race.

Expected behavior
The above program to be able to encode the input values and run successfully.

Actual behavior
Executing the above go program results in a panic

panic: value method main.Child.IsNull called using nil *Child pointer

goroutine 1 [running]:
main.(*Child).IsNull(0xc10e80?)
        <autogenerated>:1 +0x1b
github.com/jackc/pgx/v5/pgtype.(*encodePlanCompositeCodecCompositeIndexGetterToBinary).Encode(0xc000222190, {0x808960?, 0x0}, {0xc000236600, 0x18, 0x80})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/pgtype/composite.go:80 +0xba
github.com/jackc/pgx/v5/pgtype.(*CompositeBinaryBuilder).AppendValue(0xc0001779e0, 0x511d, {0x808960, 0x0})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/pgtype/composite.go:495 +0x36a
github.com/jackc/pgx/v5/pgtype.(*encodePlanCompositeCodecCompositeIndexGetterToBinary).Encode(0xc000222180, {0x8550e0?, 0xc000222170}, {0xc000236600, 0x0, 0x80})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/pgtype/composite.go:86 +0x1e6
github.com/jackc/pgx/v5/pgtype.(*Map).Encode(0xc000134600, 0x5120, 0x1, {0x8550e0, 0xc000222170}, {0xc000236600, 0x0, 0x80})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/pgtype/pgtype.go:1961 +0x142
github.com/jackc/pgx/v5.(*ExtendedQueryBuilder).encodeExtendedParamValue(0xc00015d980, 0xc000134600?, 0x5120?, 0x1?, {0x8550e0?, 0xc000222170?})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/extended_query_builder.go:125 +0xc6
github.com/jackc/pgx/v5.(*ExtendedQueryBuilder).appendParam(0xc00015d980, 0xc0001a2540?, 0x5120?, 0x1, {0x8550e0?, 0xc000222170?})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/extended_query_builder.go:79 +0x189
github.com/jackc/pgx/v5.(*ExtendedQueryBuilder).appendParam(0xc00015d980, 0xc000134600, 0x5120, 0xc0?, {0x8550e0, 0xc000222170})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/extended_query_builder.go:59 +0x10a
github.com/jackc/pgx/v5.(*ExtendedQueryBuilder).Build(0xc00015d980, 0xc000134600, 0xc00024e1e0, {0xc000222160, 0x1, 0xc000232600?})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/extended_query_builder.go:40 +0x3d0
github.com/jackc/pgx/v5.(*Conn).execPrepared(0xc00015d8c0, {0x938ed0, 0xc43360}, 0xc00024e1e0, {0xc000222160?, 0x892104?, 0x17?})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/conn.go:581 +0x5f
github.com/jackc/pgx/v5.(*Conn).exec(0xc00015d8c0, {0x938ed0, 0xc43360}, {0x892104, 0x17}, {0xc000222160?, 0x10?, 0x10?})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/conn.go:523 +0x370
github.com/jackc/pgx/v5.(*Conn).Exec(0xc00015d8c0, {0x938ed0?, 0xc43360?}, {0x892104, 0x17}, {0xc000222160, 0x1, 0x1})
        /home/jaltavilla/go/pkg/mod/github.com/jackc/pgx/v5@v5.7.6/conn.go:466 +0x117
main.main()
        /home/jaltavilla/dev/MGP-Server/pastegres-repro/main.go:55 +0x2ad

Version

  • Go: go version go1.23.5 linux/amd64
  • PostgreSQL: PostgreSQL 17.6 (Debian 17.6-1.pgdg11+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
  • pgx: require github.com/jackc/pgx/v5 v5.7.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions