Skip to content

bug: Decoder panics on malformed indefinite-length map when decoding array into time.Time (v2.9.0) #809

Closed

Description

@hhellbentt

What version of fxamacker/cbor are you using?

github.com/fxamacker/cbor/v2 v2.9.0

Does this issue reproduce with the latest release?

No. On the latest release v2.9.3 the same 5-byte input no longer panics and returns:

cbor: cannot unmarshal array into Go struct field main.Record.d of type time.Time

The panic is reproducible on v2.9.0. It looks related to the fix in PR #803 / #802. This report includes a minimized, deterministic PoC, because the earlier crash files in #802 did not reproduce.

What OS and CPU architecture are you using (go env)?

go env Output
$ go env
set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=0
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\dearborrn\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\dearborrn\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=C:\Users\dearborrn\Desktop\oss\oss-fuzz-master\projects\kuber_api\go.mod
set GOMODCACHE=C:\Users\dearborrn\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\dearborrn\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\dearborrn\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.25.4
set GOWORK=
set PKG_CONFIG=pkg-config

What did you do?

Found by coverage-guided fuzzing of DecMode.Unmarshal() (target FuzzDecModeUnmarshalComplex) against untrusted CBOR.

Minimized crashing input (5 bytes):

bf 61 64 80 ff

CBOR diagnostic:

Offset Byte Meaning
0 bf start indefinite-length map
1 61 text string, length 1
2 64 'd' (struct field name)
3 80 empty array (invalid time.Time value)
4 ff break

This is an indefinite-length map { "d": [] } with a break marker.

Standalone reproducer (go.mod pin github.com/fxamacker/cbor/v2 v2.9.0):

package main

import (
	"fmt"
	"time"

	cbor "github.com/fxamacker/cbor/v2"
)

type Record struct {
	Time time.Time `cbor:"d"`
}

func main() {
	// indefinite-length map, text-string key "d", empty array, break
	data := []byte{0xbf, 0x61, 'd', 0x80, 0xff}

	var v Record
	if err := cbor.Unmarshal(data, &v); err != nil {
		fmt.Println("err:", err)
		return
	}
	fmt.Println("decoded:", v)
}
go run .

Default decoder options are enough. No special DecOptions are required. A single time.Time field is enough; nested/recursive structs are not needed.

The same panic also occurs with a byte-string field name (bf 41 64 80 ff) if FieldNameByteStringAllowed is enabled.

A definite-length map a1 61 64 80 ({"d": []}) does not panic on v2.9.0: it correctly returns UnmarshalTypeError.

What did you expect to see?

Malformed / type-invalid CBOR should be rejected with an error, for example:

cbor: cannot unmarshal array into Go struct field main.Record.d of type time.Time

The decoder should not panic on untrusted input.

What did you see instead?

Unmarshal panics with an out-of-bounds access:

panic: runtime error: index out of range [5] with length 5

github.com/fxamacker/cbor/v2.(*decoder).foundBreak(...)
	.../github.com/fxamacker/cbor/v2@v2.9.0/decode.go:3043
github.com/fxamacker/cbor/v2.(*decoder).parseMapToStruct(...)
	.../github.com/fxamacker/cbor/v2@v2.9.0/decode.go:2685
github.com/fxamacker/cbor/v2.(*decoder).parseToValue(...)
	.../github.com/fxamacker/cbor/v2@v2.9.0/decode.go:1692
github.com/fxamacker/cbor/v2.(*decoder).value(...)
	.../github.com/fxamacker/cbor/v2@v2.9.0/decode.go:1399
github.com/fxamacker/cbor/v2.(*decMode).Unmarshal(...)
	.../github.com/fxamacker/cbor/v2@v2.9.0/decode.go:1302

If an application unmarshals attacker-controlled CBOR into a struct that contains time.Time and does not recover from panics, this payload terminates the process (DoS).

Root cause

foundBreak() assumes the input is well-formed and does not check bounds:

// foundBreak assumes data is well-formed, and does not perform bounds checking.
func (d *decoder) foundBreak() bool {
	if isBreakFlag(d.data[d.off]) {
		d.off++
		return true
	}
	return false
}

The cursor is left pointing past the buffer because parseToTime() does not skip the current data item when decoding fails for arrays/maps (and for byte strings when ByteStringToTime is forbidden), unless timeTag == DecTagRequired.

Call chain:

Unmarshal
  -> value
    -> parseToValue          // struct / map
      -> parseMapToStruct
        -> parseToValue      // field Time time.Time
          -> parseToTime     // sees array 0x80
            -> returns UnmarshalTypeError WITHOUT d.skip()
        -> loop continues, foundBreak() reads d.data[d.off] with off == len(data)

In parseToTime() the default branch for unsupported types (array/map) is:

default:
    return time.Time{}, false, &UnmarshalTypeError{CBORType: t.String(), GoType: typeTime.String()}

Compare with DecTagRequired, which does call d.skip() before returning. After the error, parseMapToStruct() keeps iterating an indefinite-length map and eventually calls foundBreak() with off past the end of data.

RFC 8949 time values (tag 0 / tag 1) are not required to hit this path. The panic is triggered by an untagged array (or map/byte string under the conditions above) stored in a field whose Go type is time.Time.

Activity

  1. fxamacker commented on Aug 24, 2026

    @fxamacker
    Owner

    @hhellbentt Thanks for providing a small reproducer, I'm closing this because it is a duplicate of the issue fixed by PR #803 and it doesn't affect the latest release as mentioned in my comment to you about one week ago.

    To summarize, this issue was already fixed and released in cbor v2.9.3.

    Also as mentioned in the cbor v2.9.3 release notes, the fuzzing was expanded to detect this class of bugs and cbor v2.9.3 passed the updated fuzzing (I let fuzzing continue running for days after the release was tagged).

    Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already exists

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions