| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
TryBots failure for `gotip-wasip1-wasm_wazero` looks real?
return d.unmarshal(val.Elem(), start, d.unmarshalDepth)Sorry, I'm not familiar with this package at all. But, I think this might be an incomplete fix?
It seems that `Decoder.unmarshalDepth` only keeps track of the `UnmarshalXML` method depth. So, a recursive XML structure where not all `structs` implement `UnmarshalXML` method can still bypass this limit:
```
type Section struct {
Sub *Section `xml:"section"`
Custom *Extension `xml:"extension"`
}
type Extension struct {
Body Section
}func (e *Extension) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var body Section
if err := d.DecodeElement(&body, &start); err != nil {
return err
}
e.Body = body
return nil
}func TestDecodeElementDepthBypass(t *testing.T) {
// Construct a document with 3 blocks of 5,000 nested <section> tags,
// separated by <extension> tags.
// Total XML nesting depth = 15,003 tags deep (maxUnmarshalDepth is 10,000).
openSections := strings.Repeat("<section>", 5000)
closeSections := strings.Repeat("</section>", 5000)var buf strings.Builder
for i := 0; i < 3; i++ {
buf.WriteString(openSections)
buf.WriteString("<extension>")
}
for i := 0; i < 3; i++ {
buf.WriteString("</extension>")
buf.WriteString(closeSections)
}
var sec Section
err := xml.Unmarshal([]byte(buf.String()), &sec)
if err == nil {
t.Fatalf("FAILED: Unmarshaled 15,003 levels deep without error (depth limit bypassed)")
}
```In this case, after reaching a `depth` of 5000 inside the nested `<section>` tags, the `depth` gets reset back to `d.unmarshalDepth` (1, 2, then 3) whenever `<extension>` is encountered.
if runtime.GOARCH == "wasm" {
tests = []struct {
name string
depth int
wantErr error
}{
{
name: "wasm below limit",
depth: 4998,
wantErr: nil,
},
{
name: "wasm above limit",
depth: 4999,
wantErr: errUnmarshalDepth,
},
}
}
Probably cleaner to just define a `limit` variable that has different value according to `runtime.GOARCH`. Then, the test cases can define `depth` as `limit-1` and so on.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
return d.unmarshal(val.Elem(), start, d.unmarshalDepth)Done
if runtime.GOARCH == "wasm" {
tests = []struct {
name string
depth int
wantErr error
}{
{
name: "wasm below limit",
depth: 4998,
wantErr: nil,
},
{
name: "wasm above limit",
depth: 4999,
wantErr: errUnmarshalDepth,
},
}
}
Probably cleaner to just define a `limit` variable that has different value according to `runtime.GOARCH`. Then, the test cases can define `depth` as `limit-1` and so on.
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
TryBots failure for `gotip-wasip1-wasm_wazero` looks real?
Had to skip the test on wazero. LMK if you don't like the checking of GO_BUILDER_NAME.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
in the unmarshal method. When a custom UnmarshalXML method callednit: extra space, probably from automatic line wrapping?
Same before "This allowed documents".
d.unmarshalDepth++Should these be removed?
I think this will cause the following to fail:
```
type manualNode struct {
Child *manualNode
}
func (m *manualNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
var child manualNode
if err := d.DecodeElement(&child, &t); err != nil {
return err
}
m.Child = &child
case xml.EndElement:
return nil
}
}
}func TestManual(t *testing.T) {
depth := 10001
payload := bytes.Join([][]byte{
bytes.Repeat([]byte("<a>"), depth),
bytes.Repeat([]byte("</a>"), depth),
}, nil)var node manualNode
err := xml.Unmarshal(payload, &node)
if err == nil {
t.Fatalf("Unmarshal depth limit bypassed: unmarshaled 10,001 levels deep without error")
}
}
```
If we do not increment `d.unmarshalDepth` here, when someone uses `Token` manually before calling `DecodeElement`, we would fail to account for the depth increase.
Although, just adding back `d.unmarshalDepth++` and `d.unmarshalDepth--` increase here would mean that when someones calls `Decode` without `Token`, the depth would increase by 2 instead of 1 I think. Maybe that's fine since our depth limit is pretty high? It would probably be confusing if a user ever hits the limit though.
type Section struct {
Sub *Section `xml:"section"`
Custom *Extension `xml:"extension"`
}
type Extension struct {
Body Section
}nit: no need to export `Section` and `Extension`. Doesn't matter that much since it's a test, but might as well be consistent. Renaming `Section` and `Extension` to be more descriptive for the test might be nice too.
(I guess this was copied from my comment, my bad.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
in the unmarshal method. When a custom UnmarshalXML method callednit: extra space, probably from automatic line wrapping?
Same before "This allowed documents".
This is a byproduct of typewriter training in my youth, in which double-spaces after terminal punctuation were taught. With the advent of proportional fonts in computing, it seems to be now viewed as outdated (but not necessarily "incorrect").
type Section struct {
Sub *Section `xml:"section"`
Custom *Extension `xml:"extension"`
}
type Extension struct {
Body Section
}nit: no need to export `Section` and `Extension`. Doesn't matter that much since it's a test, but might as well be consistent. Renaming `Section` and `Extension` to be more descriptive for the test might be nice too.
(I guess this was copied from my comment, my bad.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Yeah I think we need to retain this, its interaction with the `unmarshal` `depth` argument is making everything more complicated though.
oldDepth := d.unmarshalDepth
d.unmarshalDepth = depth
defer func() { d.unmarshalDepth = oldDepth }()I think we should probably just do away with the `depth` argument and move to always using `unmarshalDepth`, since it makes following this all a bit confusing.
This way we'd remove the depth arg from unmarshal, unmarshalInterface,and unmarshalPath, and add
unmarshalDepth++
defer unmarshalDepth--
to the preamble of each method.
depth: maxDepth,If we switch to using only unmarshalDepth I believe this would need to be `(maxDepth/4)` and the below would be `(maxDepth/4)+1`, since the DecodeElement loop recurses in unmarshal ~4 times.
t.Fatalf("FAILED: Unmarshaled 15,003 levels deep without error (depth limit bypassed)")`Fatal`, since there are no formatting args.
t.Fatalf("FAILED: Unmarshaled 15,003 levels deep without error (depth limit bypassed)")You can remove this prefix.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
t.Fatalf("FAILED: Unmarshaled 15,003 levels deep without error (depth limit bypassed)")`Fatal`, since there are no formatting args.
Done
t.Fatalf("FAILED: Unmarshaled 15,003 levels deep without error (depth limit bypassed)")You can remove this prefix.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
d.unmarshalDepth++I think I have a better solution for tracking depth by updating it within the push and pop methods on the decoder.
oldDepth := d.unmarshalDepth
d.unmarshalDepth = depth
defer func() { d.unmarshalDepth = oldDepth }()I think we should probably just do away with the `depth` argument and move to always using `unmarshalDepth`, since it makes following this all a bit confusing.
This way we'd remove the depth arg from unmarshal, unmarshalInterface,and unmarshalPath, and add
unmarshalDepth++
defer unmarshalDepth--to the preamble of each method.
I think I have a better solution for tracking depth by updating it within the push and pop methods on the decoder.
depth: maxDepth,If we switch to using only unmarshalDepth I believe this would need to be `(maxDepth/4)` and the below would be `(maxDepth/4)+1`, since the DecodeElement loop recurses in unmarshal ~4 times.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Fixes #80481Reminder to cherrypick to 1.27 release branch too.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
From my reading this maintains our old behavior while significantly improving the coverage. Since we now essentially increment the depth every time we parse a start element, I think this should be a concrete fix. Thanks!
interleaving elements with and without custom unmarshalers.Can you include a note about the new behavior, and why we decided on it vs. the old implementation.
// unmarshalPath can call unmarshal, so we need to pass the depth through so thatStale comment.
// the recursion depth of unmarshalPath is limited to the path length specifiedStale comment.
builder := os.Getenv("GO_BUILDER_NAME")We should use the same wasm check/bypass in all of the depth tests (new and old) consistently.
if err == nil {Check we are returning errUnmarshalDepth here and throughout, so we know we are returning the correct error.
t.Fatal("Unmarshaled 15,003 levels deep without error (depth limit bypassed)")Use the constant, so if it changes the error message doesn't become confusing.
func TestManual(t *testing.T) {TestRecursiveUnmarshalInterfaceDepth
if err == nil {Same here, check for errUnmarshalDepth.
if s := d.stk; s != nil && s.kind == stkStart {I think this breaks our previous behavior. The intention here (if I'm remembering correctly) is to prevent using RawToken from inside of custom UnmarshalXML methods. This change would break streaming Decoders, I believe. Probably we should just add a bool to Decoder that indicates if we're inside of UnmarshalXML and set/unset it appropriately, then check that here.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
interleaving elements with and without custom unmarshalers.Can you include a note about the new behavior, and why we decided on it vs. the old implementation.
Done
// unmarshalPath can call unmarshal, so we need to pass the depth through so thatIan AlexanderStale comment.
Done
// the recursion depth of unmarshalPath is limited to the path length specifiedIan AlexanderStale comment.
Done
builder := os.Getenv("GO_BUILDER_NAME")We should use the same wasm check/bypass in all of the depth tests (new and old) consistently.
The wazero builder cannot create the executable because of its limited stack size. I'd rather have the test run on the remaining wasm builders. I added a comment explaining. WDYT?
Check we are returning errUnmarshalDepth here and throughout, so we know we are returning the correct error.
Done
t.Fatal("Unmarshaled 15,003 levels deep without error (depth limit bypassed)")Use the constant, so if it changes the error message doesn't become confusing.
Done
func TestManual(t *testing.T) {Ian AlexanderTestRecursiveUnmarshalInterfaceDepth
Done
depth := 10001Ian AlexandermaxUnmarshalDepth+1
Done
Same here, check for errUnmarshalDepth.
Done
I think this breaks our previous behavior. The intention here (if I'm remembering correctly) is to prevent using RawToken from inside of custom UnmarshalXML methods. This change would break streaming Decoders, I believe. Probably we should just add a bool to Decoder that indicates if we're inside of UnmarshalXML and set/unset it appropriately, then check that here.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
builder := os.Getenv("GO_BUILDER_NAME")Ian AlexanderWe should use the same wasm check/bypass in all of the depth tests (new and old) consistently.
The wazero builder cannot create the executable because of its limited stack size. I'd rather have the test run on the remaining wasm builders. I added a comment explaining. WDYT?
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Reminder to cherrypick to 1.27 release branch too.
Acknowledged
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
10 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: src/encoding/xml/read.go
Insertions: 4, Deletions: 4.
@@ -207,6 +207,10 @@
// Record that decoder must stop at end tag corresponding to start.
d.pushEOF()
+ savedInUnmarshalXML := d.inUnmarshalXML
+ d.inUnmarshalXML = true
+ defer func() { d.inUnmarshalXML = savedInUnmarshalXML }()
+
err := val.UnmarshalXML(d, *start)
if err != nil {
d.popEOF()
@@ -547,8 +551,6 @@
case StartElement:
consumed := false
if sv.IsValid() {
- // unmarshalPath can call unmarshal, so we need to pass the depth through so that
- // we can continue to enforce the maximum recursion limit.
consumed, err = d.unmarshalPath(tinfo, sv, nil, &t)
if err != nil {
return err
@@ -748,8 +750,6 @@
}
switch t := tok.(type) {
case StartElement:
- // the recursion depth of unmarshalPath is limited to the path length specified
- // by the struct field tag, so we don't increment the depth here.
consumed2, err := d.unmarshalPath(tinfo, sv, parents, &t)
if err != nil {
return true, err
```
```
The name of the file: src/encoding/xml/xml.go
Insertions: 18, Deletions: 17.
@@ -197,22 +197,23 @@
// the attribute xmlns="DefaultSpace".
DefaultSpace string
- r io.ByteReader
- t TokenReader
- buf bytes.Buffer
- saved *bytes.Buffer
- stk *stack
- stkDepth int
- free *stack
- needClose bool
- toClose Name
- nextToken Token
- nextByte int
- ns map[string]string
- err error
- line int
- linestart int64
- offset int64
+ r io.ByteReader
+ t TokenReader
+ buf bytes.Buffer
+ saved *bytes.Buffer
+ stk *stack
+ stkDepth int
+ free *stack
+ needClose bool
+ toClose Name
+ nextToken Token
+ nextByte int
+ ns map[string]string
+ err error
+ line int
+ linestart int64
+ offset int64
+ inUnmarshalXML bool
}
// NewDecoder creates a new XML parser reading from r.
@@ -548,7 +549,7 @@
// start and end elements match and does not translate
// name space prefixes to their corresponding URLs.
func (d *Decoder) RawToken() (Token, error) {
- if s := d.stk; s != nil && s.kind == stkStart {
+ if d.inUnmarshalXML {
return nil, errRawToken
}
return d.rawToken()
```
```
The name of the file: src/encoding/xml/read_test.go
Insertions: 23, Deletions: 6.
@@ -1144,6 +1144,8 @@
}
func TestDecodeElementRecursion(t *testing.T) {
+ // The wazero builder is unable to build the test binary due to its small
+ // stack size.
builder := os.Getenv("GO_BUILDER_NAME")
if testing.Short() || strings.Contains(builder, "wazero") {
t.Skip("test requires significant memory")
@@ -1222,8 +1224,8 @@
var node standardNode
err := Unmarshal(buf.Bytes(), &node)
- if err == nil {
- t.Fatal("Unmarshaled 15,003 levels deep without error (depth limit bypassed)")
+ if err != errUnmarshalDepth {
+ t.Fatalf("Unexpected error: got %q want %q", err, errUnmarshalDepth)
}
}
@@ -1250,8 +1252,8 @@
}
}
-func TestManual(t *testing.T) {
- depth := 10001
+func TestRecursiveUnmarshalInterfaceDepth(t *testing.T) {
+ depth := maxUnmarshalDepth + 1
payload := bytes.Join([][]byte{
bytes.Repeat([]byte("<a>"), depth),
bytes.Repeat([]byte("</a>"), depth),
@@ -1259,7 +1261,22 @@
var node manualNode
err := Unmarshal(payload, &node)
- if err == nil {
- t.Fatalf("Unmarshal depth limit bypassed: unmarshaled %d levels deep without error", depth)
+ if err != errUnmarshalDepth {
+ t.Fatalf("Unexpected error: got %q want %q", err, errUnmarshalDepth)
+ }
+}
+
+type rawTokenNode struct{}
+
+func (r *rawTokenNode) UnmarshalXML(d *Decoder, start StartElement) error {
+ _, err := d.RawToken()
+ return err
+}
+
+func TestUnmarshalXMLRawToken(t *testing.T) {
+ var node rawTokenNode
+ err := Unmarshal([]byte("<a></a>"), &node)
+ if err != errRawToken {
+ t.Fatalf("UnmarshalXML calling RawToken: got error %v, want %v", err, errRawToken)
}
}
```
encoding/xml: fix depth processing in (*Decoder).unmarshal
(*Decoder).DecodeElement bypassed recursion depth guard by unilaterally
passing the constant 0 to (*Decoder).unmarshal. Previously, unmarshal
depth was tracked via a depth parameter passed down the call stack,
which manual loops inside custom UnmarshalXML methods could bypass.
This change simplifies depth tracking by maintaining a stack depth value
that is adjusted as start elements are pushed / popped. This eliminates
the need to reason about and synchronize two different values storing
the unmarshal depth.
Additionally, guarding (*Decoder).RawToken using parser stack state
broke streaming decoders reading tokens within open XML elements. This
change simplifies the guard by adding an explicit inUnmarshalXML flag.
Thanks to Moran Omer (GitHub: moraneus) for reporting this issue.
Fixes #80481
Fixes CVE-2026-56859
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |