Repository: fxamacker/cbor (go)

Issue:
bug: RawTag.UnmarshalCBOR() panic with truncated tag
### What version of fxamacker/cbor are you using?

v2.7.0

### Does this issue reproduce with the latest release?

I haven’t tested.

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

```console
$ go env

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/tho/Library/Caches/go-build'
GOENV='/Users/tho/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/tho/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/tho/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.0'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/tho/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/Users/tho/Code/github.com/veraison/cmw/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/st/1wblnvg95bg4p9hnzc6dwjzh0000gn/T/go-build1346977790=/tmp/go-build -gno-record-gcc-switches -fno-common'
```

### What did you do?

```go
{
    var v cbor.RawTag

    truncatedTag := []byte{0xda}

    err := v.UnmarshalCBOR(truncatedTag)

    checkErr(err)
}
  
```

### What did you expect to see?

An error from `UnmarshalCBOR`

### What did you see instead?

The following panic trace:

```
panic: runtime error: slice bounds out of range [:5] with capacity 1 [recovered]
	panic: runtime error: slice bounds out of range [:5] with capacity 1

panic({0x102b34b00?, 0x140000b61c8?})
	/usr/local/go/src/runtime/panic.go:785 +0x124
github.com/fxamacker/cbor/v2.(*decoder).getHead(0x140000fbaa8?)
	/Users/tho/go/pkg/mod/github.com/fxamacker/cbor/v2@v2.7.0/decode.go:2921 +0x1cc
github.com/fxamacker/cbor/v2.(*RawTag).UnmarshalCBOR(0x140000fbac8, {0x140000fbac7?, 0x140000fbad8?, 0x10291cd5c?})
	/Users/tho/go/pkg/mod/github.com/fxamacker/cbor/v2@v2.7.0/tag.go:39 +0x88
[...]
```


Pull request description:
Update error handling in RawTag.UnmarshalCBOR(), etc. to match cbor.Unmarshal()
Closes #634

`RawTag.UnmarshalCBOR()` is intended to be called by the codec internally and the codec checks for malformed data before calling it.  However, it is possible for user apps to directly call it, so user apps might provide malformed data which can cause panic.

This PR updates these 3 functions to use same error handling as `cbor.Unmarshal()`:
- `ByteString.UnmarshalCBOR(data)`
- `RawTag.UnmarshalCBOR(data)`
- `SimpleValue.UnmarshalCBOR(data)`

Basically, this adds the same well-formedness checks on input data already done by `cbor.Unmarshal()`, so `UnmarshalCBOR()` will return same error if input data is malformed (not panic).

### Caveats

Unfortunately, this approach means the same data is checked twice for the intended case of the codec calling `UnmarshalCBOR()` internally.  This can be revisited and maybe optimized in the future.

This PR passed very brief fuzzing on Sunday, March 16, 2025.  However, the fuzzing needs to be run for longer duration before release tagging v2.7.1.

### Thanks

Thanks @thomas-fossati for reporting issue #634.  While looking into RawTag, I found the same issue in ByteString and SimpleValue.


Patch:
diff --git a/simplevalue.go b/simplevalue.go
index de175ce..df6ad07 100644
--- a/simplevalue.go
+++ b/simplevalue.go
@@ -52,6 +52,21 @@ func (sv *SimpleValue) UnmarshalCBOR(data []byte) error {
 
 	d := decoder{data: data, dm: defaultDecMode}
 
+	// Check well-formedness of CBOR data item.
+	// NOTE: well-formedness check here is redundant when
+	// Unmarshal() invokes SimpleValue.UnmarshalCBOR().
+	// However, SimpleValue.UnmarshalCBOR() is exported, so
+	// the codec needs to support same behavior for:
+	// - Unmarshal(data, *SimpleValue)
+	// - SimpleValue.UnmarshalCBOR(data)
+	err := d.wellformed(false, false)
+	if err != nil {
+		return err
+	}
+
+	// Restore decoder offset after well-formedness check.
+	d.off = 0
+
 	typ, ai, val := d.getHead()
 
 	if typ != cborTypePrimitives {
diff --git a/tag.go b/tag.go
index 5c4d2b7..a5b258f 100644
--- a/tag.go
+++ b/tag.go
@@ -35,6 +35,21 @@ func (t *RawTag) UnmarshalCBOR(data []byte) error {
 
 	d := decoder{data: data, dm: defaultDecMode}
 
+	// Check if data is a well-formed CBOR data item.
+	// NOTE: well-formedness check here is redundant when
+	// Unmarshal() invokes RawTag.UnmarshalCBOR().
+	// However, RawTag.UnmarshalCBOR() is exported, so
+	// the codec needs to support same behavior for:
+	// - Unmarshal(data, *RawTag)
+	// - RawTag.UnmarshalCBOR(data)
+	err := d.wellformed(false, false)
+	if err != nil {
+		return err
+	}
+
+	// Restore decoder offset after well-formedness check.
+	d.off = 0
+
 	// Unmarshal tag number.
 	typ, _, num := d.getHead()
 	if typ != cborTypeTag {


Test patch:
diff --git a/bytestring.go b/bytestring.go
index 823bff1..6bc081f 100644
--- a/bytestring.go
+++ b/bytestring.go
@@ -52,6 +52,21 @@ func (bs *ByteString) UnmarshalCBOR(data []byte) error {
 
 	d := decoder{data: data, dm: defaultDecMode}
 
+	// Check well-formedness of CBOR data item.
+	// NOTE: well-formedness check here is redundant when
+	// Unmarshal() invokes ByteString.UnmarshalCBOR().
+	// However, ByteString.UnmarshalCBOR() is exported, so
+	// the codec needs to support same behavior for:
+	// - Unmarshal(data, *ByteString)
+	// - ByteString.UnmarshalCBOR(data)
+	err := d.wellformed(false, false)
+	if err != nil {
+		return err
+	}
+
+	// Restore decoder offset after well-formedness check.
+	d.off = 0
+
 	// Check if CBOR data type is byte string
 	if typ := d.nextCBORType(); typ != cborTypeByteString {
 		return &UnmarshalTypeError{CBORType: typ.String(), GoType: typeByteString.String()}
diff --git a/bytestring_test.go b/bytestring_test.go
index 2f4920e..ae208ae 100644
--- a/bytestring_test.go
+++ b/bytestring_test.go
@@ -3,7 +3,11 @@
 
 package cbor
 
-import "testing"
+import (
+	"io"
+	"strings"
+	"testing"
+)
 
 func TestByteString(t *testing.T) {
 	type s1 struct {
@@ -99,3 +103,110 @@ func TestByteString(t *testing.T) {
 	dm, _ := DecOptions{}.DecMode()
 	testRoundTrip(t, testCases, em, dm)
 }
+
+func TestUnmarshalByteStringOnBadData(t *testing.T) {
+	testCases := []struct {
+		name   string
+		data   []byte
+		errMsg string
+	}{
+		// Empty data
+		{
+			name:   "nil data",
+			data:   nil,
+			errMsg: io.EOF.Error(),
+		},
+		{
+			name:   "empty data",
+			data:   []byte{},
+			errMsg: io.EOF.Error(),
+		},
+
+		// Wrong CBOR types
+		{
+			name:   "uint type",
+			data:   hexDecode("01"),
+			errMsg: "cbor: cannot unmarshal positive integer into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "int type",
+			data:   hexDecode("20"),
+			errMsg: "cbor: cannot unmarshal negative integer into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "string type",
+			data:   hexDecode("60"),
+			errMsg: "cbor: cannot unmarshal UTF-8 text string into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "array type",
+			data:   hexDecode("80"),
+			errMsg: "cbor: cannot unmarshal array into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "map type",
+			data:   hexDecode("a0"),
+			errMsg: "cbor: cannot unmarshal map into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "tag type",
+			data:   hexDecode("c074323031332d30332d32315432303a30343a30305a"),
+			errMsg: "cbor: cannot unmarshal tag into Go value of type cbor.ByteString",
+		},
+		{
+			name:   "float type",
+			data:   hexDecode("f90000"),
+			errMsg: "cbor: cannot unmarshal primitives into Go value of type cbor.ByteString",
+		},
+
+		// Truncated CBOR data
+		{
+			name:   "truncated head",
+			data:   hexDecode("18"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Truncated CBOR byte string
+		{
+			name:   "truncated byte string",
+			data:   hexDecode("44010203"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Extraneous CBOR data
+		{
+			name:   "extraneous data",
+			data:   hexDecode("c074323031332d30332d32315432303a30343a30305a00"),
+			errMsg: "cbor: 1 bytes of extraneous data starting at index 22",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Test ByteString.UnmarshalCBOR(data)
+			{
+				var v ByteString
+
+				err := v.UnmarshalCBOR(tc.data)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+			// Test Unmarshal(data, *ByteString), which calls ByteString.UnmarshalCBOR() under the hood
+			{
+				var v ByteString
+
+				err := Unmarshal(tc.data, &v)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+		})
+	}
+}
diff --git a/simplevalue_test.go b/simplevalue_test.go
index 67b1166..e18876c 100644
--- a/simplevalue_test.go
+++ b/simplevalue_test.go
@@ -5,7 +5,9 @@ package cbor
 
 import (
 	"bytes"
+	"io"
 	"reflect"
+	"strings"
 	"testing"
 )
 
@@ -51,6 +53,125 @@ func TestUnmarshalSimpleValue(t *testing.T) {
 	})
 }
 
+func TestUnmarshalSimpleValueOnBadData(t *testing.T) {
+	testCases := []struct {
+		name   string
+		data   []byte
+		errMsg string
+	}{
+		// Empty data
+		{
+			name:   "nil data",
+			data:   nil,
+			errMsg: io.EOF.Error(),
+		},
+		{
+			name:   "empty data",
+			data:   []byte{},
+			errMsg: io.EOF.Error(),
+		},
+
+		// Wrong CBOR types
+		{
+			name:   "uint type",
+			data:   hexDecode("01"),
+			errMsg: "cbor: cannot unmarshal positive integer into Go value of type SimpleValue",
+		},
+		{
+			name:   "int type",
+			data:   hexDecode("20"),
+			errMsg: "cbor: cannot unmarshal negative integer into Go value of type SimpleValue",
+		},
+		{
+			name:   "byte string type",
+			data:   hexDecode("40"),
+			errMsg: "cbor: cannot unmarshal byte string into Go value of type SimpleValue",
+		},
+		{
+			name:   "string type",
+			data:   hexDecode("60"),
+			errMsg: "cbor: cannot unmarshal UTF-8 text string into Go value of type SimpleValue",
+		},
+		{
+			name:   "array type",
+			data:   hexDecode("80"),
+			errMsg: "cbor: cannot unmarshal array into Go value of type SimpleValue",
+		},
+		{
+			name:   "map type",
+			data:   hexDecode("a0"),
+			errMsg: "cbor: cannot unmarshal map into Go value of type SimpleValue",
+		},
+		{
+			name:   "tag type",
+			data:   hexDecode("c074323031332d30332d32315432303a30343a30305a"),
+			errMsg: "cbor: cannot unmarshal tag into Go value of type SimpleValue",
+		},
+		{
+			name:   "float type",
+			data:   hexDecode("f90000"),
+			errMsg: "cbor: cannot unmarshal primitives into Go value of type SimpleValue",
+		},
+
+		// Truncated CBOR data
+		{
+			name:   "truncated head",
+			data:   hexDecode("18"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Truncated CBOR simple value
+		{
+			name:   "truncated simple value",
+			data:   hexDecode("f8"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Invalid simple value
+		{
+			name:   "invalid simple value",
+			data:   hexDecode("f800"),
+			errMsg: "cbor: invalid simple value 0 for type primitives",
+		},
+
+		// Extraneous CBOR data
+		{
+			name:   "extraneous data",
+			data:   hexDecode("f4f5"),
+			errMsg: "cbor: 1 bytes of extraneous data starting at index 1",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Test SimpleValue.UnmarshalCBOR(data)
+			{
+				var v SimpleValue
+
+				err := v.UnmarshalCBOR(tc.data)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+			// Test Unmarshal(data, *SimpleValue), which calls SimpleValue.UnmarshalCBOR() under the hood
+			{
+				var v SimpleValue
+
+				err := Unmarshal(tc.data, &v)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+		})
+	}
+}
+
 func testUnmarshalInvalidSimpleValueToEmptyInterface(t *testing.T, data []byte) {
 	var v interface{}
 	if err := Unmarshal(data, v); err == nil {
diff --git a/tag_test.go b/tag_test.go
index 2d4aa6d..1740a6e 100644
--- a/tag_test.go
+++ b/tag_test.go
@@ -1536,3 +1536,125 @@ func TestEncodeBuiltinTag(t *testing.T) {
 		})
 	}
 }
+
+func TestUnmarshalRawTagOnBadData(t *testing.T) {
+	testCases := []struct {
+		name   string
+		data   []byte
+		errMsg string
+	}{
+		// Empty data
+		{
+			name:   "nil data",
+			data:   nil,
+			errMsg: io.EOF.Error(),
+		},
+		{
+			name:   "empty data",
+			data:   []byte{},
+			errMsg: io.EOF.Error(),
+		},
+
+		// Wrong CBOR types
+		{
+			name:   "uint type",
+			data:   hexDecode("01"),
+			errMsg: "cbor: cannot unmarshal positive integer into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "int type",
+			data:   hexDecode("20"),
+			errMsg: "cbor: cannot unmarshal negative integer into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "byte string type",
+			data:   hexDecode("40"),
+			errMsg: "cbor: cannot unmarshal byte string into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "string type",
+			data:   hexDecode("60"),
+			errMsg: "cbor: cannot unmarshal UTF-8 text string into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "array type",
+			data:   hexDecode("80"),
+			errMsg: "cbor: cannot unmarshal array into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "map type",
+			data:   hexDecode("a0"),
+			errMsg: "cbor: cannot unmarshal map into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "primitive type",
+			data:   hexDecode("f4"),
+			errMsg: "cbor: cannot unmarshal primitives into Go value of type cbor.RawTag",
+		},
+		{
+			name:   "float type",
+			data:   hexDecode("f90000"),
+			errMsg: "cbor: cannot unmarshal primitives into Go value of type cbor.RawTag",
+		},
+
+		// Truncated CBOR data
+		{
+			name:   "truncated head",
+			data:   hexDecode("18"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Truncated CBOR tag data
+		{
+			name:   "truncated tag number",
+			data:   hexDecode("d8"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+		{
+			name:   "tag number not followed by tag content",
+			data:   hexDecode("da"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+		{
+			name:   "truncated tag content",
+			data:   hexDecode("c074323031332d30332d32315432303a30343a3030"),
+			errMsg: io.ErrUnexpectedEOF.Error(),
+		},
+
+		// Extraneous CBOR data
+		{
+			name:   "extraneous data",
+			data:   hexDecode("c074323031332d30332d32315432303a30343a30305a00"),
+			errMsg: "cbor: 1 bytes of extraneous data starting at index 22",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Test RawTag.UnmarshalCBOR(data)
+			{
+				var v RawTag
+
+				err := v.UnmarshalCBOR(tc.data)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+			// Test Unmarshal(data, *RawTag), which calls RawTag.UnmarshalCBOR() under the hood
+			{
+				var v RawTag
+
+				err := Unmarshal(tc.data, &v)
+				if err == nil {
+					t.Errorf("UnmarshalCBOR(%x) didn't return error", tc.data)
+				}
+				if !strings.HasPrefix(err.Error(), tc.errMsg) {
+					t.Errorf("UnmarshalCBOR(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
+				}
+			}
+		})
+	}
+}