Compare commits

...

6 Commits

Author SHA1 Message Date
Faye Amacker
948c054444
Merge pull request #648 from fxamacker/fxamacker/port-pr-647-to-2.7-branch
Some checks failed
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.17, macos-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.17, ubuntu-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.17, windows-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.19, macos-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.19, ubuntu-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.19, windows-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.20, macos-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.20, ubuntu-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.20, windows-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.21, macos-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.21, ubuntu-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.21, windows-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.22, macos-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.22, ubuntu-latest) (push) Has been cancelled
ci / test ${{matrix.os}} go-${{ matrix.go-version }} (1.22, windows-latest) (push) Has been cancelled
Port PR 647 to release-2.7 branch to optimize internal calls to UnmarshalCBOR()
2025-03-29 20:05:46 -05:00
Faye Amacker
608e40e88e Optimize calls to UnmarshalCBOR() for RawTag, etc.
Currently, unreleased changes in PR #636 and #645 cause the
input data to be checked twice when UnmarshalCBOR() is
called internally by Unmarshal() for:
- ByteString
- RawTag
- SimpleValue

UnmarshalCBOR() checks input data because it can be called by
user apps providing bad data. However, the codec already checks
input data before internally calling UnmarshalCBOR() so the
2nd check is redundant.

This commit avoids redundant check on the input data by having
Unmarshal() call the private unmarshalCBOR() if implemented
by ByteString, RawTag, SimpleValue, etc.:
- Internally, the codec calls the private unmarshalCBOR() to
  avoid the redundant check on input data.
- Externally, UnmarshalCBOR() is available as a wrapper that
  checks input data before calling the private unmarshalCBOR().

UnmarshalCBOR() for ByteString, RawTag, and SimpleValue are marked
as deprecated and Unmarshal() should be used instead.
2025-03-28 11:04:15 -05:00
Faye Amacker
efbaf6c3a0
Merge pull request #636 from fxamacker/fxamacker/check-wellformedness-in-UnmarshalCBOR
Update error handling in RawTag.UnmarshalCBOR(), etc. to match cbor.Unmarshal()
2025-03-17 23:01:56 -05:00
Faye Amacker
c3ffc7a1de Check CBOR well-formedness in ByteString.UnmarshalCBOR
When ByteString.UnmarshalCBOR() is called by codec
(normal case), the codec will first check if data is well-formed
before calling ByteString.UnmarshalCBOR(data).

However, it can also be called by user app (not intended use)
and user apps might not check if data is well-formed.  In
such cases, this function can panic if given malformed data.

This commit updates ByteString.UnmarshalCBOR() to check for
well-formedness inside the function, so it behaves the same
whether it is called by codec internally or by user app.

Unfortunately, this approach means the same data is checked twice
for the normal case of the codec using
Unmarshal(data, *ByteString).

This can be revisited and maybe optimized in the future.
2025-03-16 20:23:25 -05:00
Faye Amacker
8a96f6d88c Check CBOR well-formedness in SimpleValue.UnmarshalCBOR
When SimpleValue.UnmarshalCBOR() is called by codec
(normal case), the codec will first check if data is well-formed
before calling SimpleValue.UnmarshalCBOR(data).

However, it can also be called by user app (not intended use)
and user apps might not check if data is well-formed.  In
such cases, this function can panic if given malformed data.

This commit updates SimpleValue.UnmarshalCBOR() to check for
well-formedness inside the function, so it behaves the same
whether it is called by codec internally or by user app.

Unfortunately, this approach means the same data is checked twice
for the normal case of the codec using
Unmarshal(data, *SimpleValue).

This can be revisited and maybe optimized in the future.
2025-03-16 19:55:18 -05:00
Faye Amacker
19b7fb6109 Check CBOR well-formedness in RawTag.UnmarshalCBOR
When RawTag.UnmarshalCBOR() is called by codec (normal case),
the codec will first check if data is well-formed before
calling RawTag.UnmarshalCBOR(data).

However, it can also be called by user app (not intended use)
and user apps might not check if data is well-formed.  In
such cases, this function can panic if given malformed data.

This commit updates RawTag.UnmarshalCBOR() to check for
well-formedness inside the function, so it behaves the same
whether it is called by codec internally or by user app.

Unfortunately, this approach means the same data is checked twice
for the normal case of the codec using Unmarshal(data, *RawTag).
This can be revisited and maybe optimized in the future.
2025-03-16 19:50:38 -05:00
8 changed files with 472 additions and 8 deletions

View File

@ -38,11 +38,38 @@ func (bs ByteString) MarshalCBOR() ([]byte, error) {
// UnmarshalCBOR decodes CBOR byte string (major type 2) to ByteString.
// Decoding CBOR null and CBOR undefined sets ByteString to be empty.
//
// Deprecated: No longer used by this codec; kept for compatibility
// with user apps that directly call this function.
func (bs *ByteString) UnmarshalCBOR(data []byte) error {
if bs == nil {
return errors.New("cbor.ByteString: UnmarshalCBOR on nil pointer")
}
d := decoder{data: data, dm: defaultDecMode}
// Check well-formedness of CBOR data item.
// 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
}
return bs.unmarshalCBOR(data)
}
// unmarshalCBOR decodes CBOR byte string (major type 2) to ByteString.
// Decoding CBOR null and CBOR undefined sets ByteString to be empty.
// This function assumes data is well-formed, and does not perform bounds checking.
// This function is called by Unmarshal().
func (bs *ByteString) unmarshalCBOR(data []byte) error {
if bs == nil {
return errors.New("cbor.ByteString: UnmarshalCBOR on nil pointer")
}
// Decoding CBOR null and CBOR undefined to ByteString resets data.
// This behavior is similar to decoding CBOR null and CBOR undefined to []byte.
if len(data) == 1 && (data[0] == 0xf6 || data[0] == 0xf7) {

View File

@ -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("Unmarshal(%x) didn't return error", tc.data)
}
if !strings.HasPrefix(err.Error(), tc.errMsg) {
t.Errorf("Unmarshal(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
}
}
})
}
}

View File

@ -31,6 +31,7 @@ type specialType int
const (
specialTypeNone specialType = iota
specialTypeUnmarshalerIface
specialTypeUnexportedUnmarshalerIface
specialTypeEmptyIface
specialTypeIface
specialTypeTag
@ -69,6 +70,8 @@ func newTypeInfo(t reflect.Type) *typeInfo {
tInfo.spclType = specialTypeTag
} else if t == typeTime {
tInfo.spclType = specialTypeTime
} else if reflect.PtrTo(t).Implements(typeUnexportedUnmarshaler) {
tInfo.spclType = specialTypeUnexportedUnmarshalerIface
} else if reflect.PtrTo(t).Implements(typeUnmarshaler) {
tInfo.spclType = specialTypeUnmarshalerIface
}

View File

@ -151,6 +151,10 @@ type Unmarshaler interface {
UnmarshalCBOR([]byte) error
}
type unmarshaler interface {
unmarshalCBOR([]byte) error
}
// InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
type InvalidUnmarshalError struct {
s string
@ -1460,6 +1464,9 @@ func (d *decoder) parseToValue(v reflect.Value, tInfo *typeInfo) error { //nolin
case specialTypeUnmarshalerIface:
return d.parseToUnmarshaler(v)
case specialTypeUnexportedUnmarshalerIface:
return d.parseToUnexportedUnmarshaler(v)
}
}
@ -1805,6 +1812,26 @@ func (d *decoder) parseToUnmarshaler(v reflect.Value) error {
return errors.New("cbor: failed to assert " + v.Type().String() + " as cbor.Unmarshaler")
}
// parseToUnexportedUnmarshaler parses CBOR data to value implementing unmarshaler interface.
// It assumes data is well-formed, and does not perform bounds checking.
func (d *decoder) parseToUnexportedUnmarshaler(v reflect.Value) error {
if d.nextCBORNil() && v.Kind() == reflect.Ptr && v.IsNil() {
d.skip()
return nil
}
if v.Kind() != reflect.Ptr && v.CanAddr() {
v = v.Addr()
}
if u, ok := v.Interface().(unmarshaler); ok {
start := d.off
d.skip()
return u.unmarshalCBOR(d.data[start:d.off])
}
d.skip()
return errors.New("cbor: failed to assert " + v.Type().String() + " as cbor.unmarshaler")
}
// parse parses CBOR data and returns value in default Go type.
// It assumes data is well-formed, and does not perform bounds checking.
func (d *decoder) parse(skipSelfDescribedTag bool) (interface{}, error) { //nolint:gocyclo
@ -2969,13 +2996,14 @@ func (d *decoder) nextCBORNil() bool {
}
var (
typeIntf = reflect.TypeOf([]interface{}(nil)).Elem()
typeTime = reflect.TypeOf(time.Time{})
typeBigInt = reflect.TypeOf(big.Int{})
typeUnmarshaler = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
typeBinaryUnmarshaler = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
typeString = reflect.TypeOf("")
typeByteSlice = reflect.TypeOf([]byte(nil))
typeIntf = reflect.TypeOf([]interface{}(nil)).Elem()
typeTime = reflect.TypeOf(time.Time{})
typeBigInt = reflect.TypeOf(big.Int{})
typeUnmarshaler = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
typeUnexportedUnmarshaler = reflect.TypeOf((*unmarshaler)(nil)).Elem()
typeBinaryUnmarshaler = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
typeString = reflect.TypeOf("")
typeByteSlice = reflect.TypeOf([]byte(nil))
)
func fillNil(_ cborType, v reflect.Value) error {

View File

@ -45,6 +45,9 @@ func (sv SimpleValue) MarshalCBOR() ([]byte, error) {
}
// UnmarshalCBOR decodes CBOR simple value (major type 7) to SimpleValue.
//
// Deprecated: No longer used by this codec; kept for compatibility
// with user apps that directly call this function.
func (sv *SimpleValue) UnmarshalCBOR(data []byte) error {
if sv == nil {
return errors.New("cbor.SimpleValue: UnmarshalCBOR on nil pointer")
@ -52,6 +55,29 @@ func (sv *SimpleValue) UnmarshalCBOR(data []byte) error {
d := decoder{data: data, dm: defaultDecMode}
// Check well-formedness of CBOR data item.
// 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
}
return sv.unmarshalCBOR(data)
}
// unmarshalCBOR decodes CBOR simple value (major type 7) to SimpleValue.
// This function assumes data is well-formed, and does not perform bounds checking.
// This function is called by Unmarshal().
func (sv *SimpleValue) unmarshalCBOR(data []byte) error {
if sv == nil {
return errors.New("cbor.SimpleValue: UnmarshalCBOR on nil pointer")
}
d := decoder{data: data, dm: defaultDecMode}
typ, ai, val := d.getHead()
if typ != cborTypePrimitives {

View File

@ -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("Unmarshal(%x) didn't return error", tc.data)
}
if !strings.HasPrefix(err.Error(), tc.errMsg) {
t.Errorf("Unmarshal(%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 {

26
tag.go
View File

@ -23,11 +23,37 @@ type RawTag struct {
}
// UnmarshalCBOR sets *t with tag number and raw tag content copied from data.
//
// Deprecated: No longer used by this codec; kept for compatibility
// with user apps that directly call this function.
func (t *RawTag) UnmarshalCBOR(data []byte) error {
if t == nil {
return errors.New("cbor.RawTag: UnmarshalCBOR on nil pointer")
}
d := decoder{data: data, dm: defaultDecMode}
// Check if data is a well-formed CBOR data item.
// 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
}
return t.unmarshalCBOR(data)
}
// unmarshalCBOR sets *t with tag number and raw tag content copied from data.
// This function assumes data is well-formed, and does not perform bounds checking.
// This function is called by Unmarshal().
func (t *RawTag) unmarshalCBOR(data []byte) error {
if t == nil {
return errors.New("cbor.RawTag: UnmarshalCBOR on nil pointer")
}
// Decoding CBOR null and undefined to cbor.RawTag is no-op.
if len(data) == 1 && (data[0] == 0xf6 || data[0] == 0xf7) {
return nil

View File

@ -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("Unmarshal(%x) didn't return error", tc.data)
}
if !strings.HasPrefix(err.Error(), tc.errMsg) {
t.Errorf("Unmarshal(%x) returned error %q, want %q", tc.data, err.Error(), tc.errMsg)
}
}
})
}
}