]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: fix extra data regression under goexperiment.jsonv2
authorJoe Tsai <joetsai@digital-static.net>
Thu, 24 Jul 2025 19:16:35 +0000 (12:16 -0700)
committerGopher Robot <gobot@golang.org>
Fri, 25 Jul 2025 03:30:28 +0000 (20:30 -0700)
When operating under v1 semantics in the v2 implementation,
a extra data error should take precedence over any semantic error
that could theoretically occur within the value itself.

This change only affects code compiled under goexperiment.jsonv2.

Fixes #74614

Change-Id: I055a606b053fa66b0c766ae205487b8290109285
Reviewed-on: https://go-review.googlesource.com/c/go/+/689919
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

src/encoding/json/jsontext/decode.go
src/encoding/json/v2/arshal.go

index 44b436686fb06566c1889b14949eda58d8f4ca52..b9f247bff4566c827dd22892502db24d15b8e41a 100644 (file)
@@ -776,7 +776,8 @@ func (d *decoderState) ReadValue(flags *jsonwire.ValueFlags) (Value, error) {
 
 // CheckNextValue checks whether the next value is syntactically valid,
 // but does not advance the read offset.
-func (d *decoderState) CheckNextValue() error {
+// If last, it verifies that the stream cleanly terminates with [io.EOF].
+func (d *decoderState) CheckNextValue(last bool) error {
        d.PeekKind() // populates d.peekPos and d.peekErr
        pos, err := d.peekPos, d.peekErr
        d.peekPos, d.peekErr = 0, nil
@@ -787,13 +788,18 @@ func (d *decoderState) CheckNextValue() error {
        var flags jsonwire.ValueFlags
        if pos, err := d.consumeValue(&flags, pos, d.Tokens.Depth()); err != nil {
                return wrapSyntacticError(d, err, pos, +1)
+       } else if last {
+               return d.checkEOF(pos)
        }
        return nil
 }
 
 // CheckEOF verifies that the input has no more data.
 func (d *decoderState) CheckEOF() error {
-       switch pos, err := d.consumeWhitespace(d.prevEnd); err {
+       return d.checkEOF(d.prevEnd)
+}
+func (d *decoderState) checkEOF(pos int) error {
+       switch pos, err := d.consumeWhitespace(pos); err {
        case nil:
                err := jsonwire.NewInvalidCharacterError(d.buf[pos:], "after top-level value")
                return wrapSyntacticError(d, err, pos, 0)
index 5cd2106be938a79d3d47e093de043443a2bd710d..e2ce778d5ad96c4a641a4765f7072d6942694efa 100644 (file)
@@ -409,7 +409,7 @@ func Unmarshal(in []byte, out any, opts ...Options) (err error) {
        dec := export.GetBufferedDecoder(in, opts...)
        defer export.PutBufferedDecoder(dec)
        xd := export.Decoder(dec)
-       err = unmarshalFull(dec, out, &xd.Struct)
+       err = unmarshalDecode(dec, out, &xd.Struct, true)
        if err != nil && xd.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
                return internal.TransformUnmarshalError(out, err)
        }
@@ -426,25 +426,13 @@ func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) {
        dec := export.GetStreamingDecoder(in, opts...)
        defer export.PutStreamingDecoder(dec)
        xd := export.Decoder(dec)
-       err = unmarshalFull(dec, out, &xd.Struct)
+       err = unmarshalDecode(dec, out, &xd.Struct, true)
        if err != nil && xd.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
                return internal.TransformUnmarshalError(out, err)
        }
        return err
 }
 
-func unmarshalFull(in *jsontext.Decoder, out any, uo *jsonopts.Struct) error {
-       switch err := unmarshalDecode(in, out, uo); err {
-       case nil:
-               return export.Decoder(in).CheckEOF()
-       case io.EOF:
-               offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
-               return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
-       default:
-               return err
-       }
-}
-
 // UnmarshalDecode deserializes a Go value from a [jsontext.Decoder] according to
 // the provided unmarshal options (while ignoring marshal, encode, or decode options).
 // Any unmarshal options already specified on the [jsontext.Decoder]
@@ -463,14 +451,14 @@ func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error)
                defer func() { xd.Struct = optsOriginal }()
                xd.Struct.JoinWithoutCoderOptions(opts...)
        }
-       err = unmarshalDecode(in, out, &xd.Struct)
+       err = unmarshalDecode(in, out, &xd.Struct, false)
        if err != nil && xd.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
                return internal.TransformUnmarshalError(out, err)
        }
        return err
 }
 
-func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct) (err error) {
+func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct, last bool) (err error) {
        v := reflect.ValueOf(out)
        if v.Kind() != reflect.Pointer || v.IsNil() {
                return &SemanticError{action: "unmarshal", GoType: reflect.TypeOf(out), Err: internal.ErrNonNilReference}
@@ -481,7 +469,11 @@ func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct) (err er
        // In legacy semantics, the entirety of the next JSON value
        // was validated before attempting to unmarshal it.
        if uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
-               if err := export.Decoder(in).CheckNextValue(); err != nil {
+               if err := export.Decoder(in).CheckNextValue(last); err != nil {
+                       if err == io.EOF {
+                               offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
+                               return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
+                       }
                        return err
                }
        }
@@ -495,8 +487,15 @@ func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct) (err er
                if !uo.Flags.Get(jsonflags.AllowDuplicateNames) {
                        export.Decoder(in).Tokens.InvalidateDisabledNamespaces()
                }
+               if err == io.EOF {
+                       offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
+                       return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
+               }
                return err
        }
+       if last {
+               return export.Decoder(in).CheckEOF()
+       }
        return nil
 }