Skip to content

reflect: regression in Value.Addr().Elem() roundtrip #24153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dsnet opened this issue Feb 27, 2018 · 9 comments
Closed

reflect: regression in Value.Addr().Elem() roundtrip #24153

dsnet opened this issue Feb 27, 2018 · 9 comments
Labels
FrozenDueToAge NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. release-blocker
Milestone

Comments

@dsnet
Copy link
Member

dsnet commented Feb 27, 2018

Consider the following:

type child struct{ Field string }
type parent struct{ child }

v := reflect.ValueOf(&parent{}).Elem().Field(0) // v is a child value
fmt.Println(v.Field(0).CanSet())                // Is child.Field settable? Yes.
v = v.Addr().Elem()                             // Perform roundtrip: v = *(&v)
fmt.Println(v.Field(0).CanSet())                // Is child.Field still settable? Maybe?

On go1.9, this printed:

true
true

On go1.10, this printed:

true
false

I find it very surprising that a round-trip reference/derefence would cause the exported Field to no longer be settable. It's not clear to me whether this is intended behavior or not.

\cc @mdempsky @ianlancetaylor

@dsnet dsnet added NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. release-blocker labels Feb 27, 2018
@dsnet dsnet added this to the Go1.10.1 milestone Feb 27, 2018
@dsnet dsnet changed the title reflect: regression in Addr().Elem() roundtrip reflect: regression in Value.Addr().Elem() roundtrip Feb 27, 2018
@dsnet
Copy link
Member Author

dsnet commented Feb 27, 2018

Git bisect blames https://golang.org/cl/66331.

The CL description seems to indicate that this is intended behavior:

However, the way this was implemented allowed other operations to be
interspersed between the Field calls, which could grant inappropriate
access.

If this is intended, then for #24152 we'll need to fix the json package to not assume that a roundtrip Vaue.Addr().Elem() is side-effect free (which the json.decodeState.indirect method currently assumes).

@mdempsky
Copy link
Contributor

mdempsky commented Feb 27, 2018

I would argue that reflect is working correctly in Go 1.10, and that json will need to fix its Addr/Elem roundtrip.

Rationale: Given

p := &parent{}
v := reflect.ValueOf(p)

it's legal to write

(*p).Field

which maps to the reflect operations v.Elem().Field(0).Field(0) in your example.

However, it's not legal to write

(*&(*p).child).Field

because child is not exported. Thus reflect should not allow v.Elem().Field(0).Addr().Elem().Field(0).

@dsnet
Copy link
Member Author

dsnet commented Feb 27, 2018

I argue that:

// case 1
(*p).Field = "hello"
v.Elem().FieldByName("Field").SetString("hello")

// case 2
(*p).child.Field = "hello"
v.Elem().FieldByName("child").FieldByName("Field").SetString("hello")

// case 3
(*&(*p).child).Field = "hello"
v.Elem().FieldByName("child").Addr().Elem().FieldByName("Field").SetString("hello")

Case 1 is clearly fine. Case 2 is permitted (but violates normal visibility rules). If case 2 is allowed, I don't see why case 3 is forbidden. You could say that we should forbid case 2, but I surmise that would break a lot of existing Go code.

@mdempsky
Copy link
Contributor

I think case 1 is clearly okay.

Case 2 is tricky though, and shouldn't be used as a precedent to justify case 3.

Earlier, we were talking about Value.Field, not Value.FieldByName. Field doesn't support directly accessing promoted fields. This means v.Elem().Field(0).Field(0) needs to be allowed because it's the only way to represent p.Field (which is valid), even though it also represents p.child.Field (which should be invalid).

It seems FieldByName is acting consistent with Field, even though it doesn't need to: since we can directly allow v.FieldByName("Field"), there's not a strong reason to need to allow v.FieldByName("child").FieldByName("Field"), IMO.

Maybe we need to continue allowing it for backwards compatibility, or so that FieldByName parallels Field. In either case, I don't think it's a good argument for allowing case 3.

@dsnet
Copy link
Member Author

dsnet commented Feb 27, 2018

Bleh, the whole situation is funky.

Back-to-back use of Field also allows other things not normally allowed:

type child1 struct{ Conflict string }
type child2 struct{ Conflict string }
type parent struct {
	child1
	child2
}

p := new(parent)
reflect.ValueOf(p).Elem().Field(0).Field(0).SetString("hello")
reflect.ValueOf(p).Elem().Field(1).Field(0).SetString("goodbye")
fmt.Println(p) // &{{hello} {goodbye}}

Here, the existence of a conflict on Conflict makes it such that it would normally be impossible to access this field. However, it is possible with the back-to-back Field calls.

@mdempsky
Copy link
Contributor

Yeah, it's kind of a mess. :( I'd argue that should be disallowed too, but I'm not sure how to do that efficiently.

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/97796 mentions this issue: encoding/json: avoid assuming side-effect free reflect.Value.Addr().Elem()

gopherbot pushed a commit that referenced this issue Mar 1, 2018
…lem()

Consider the following:
	type child struct{ Field string }
	type parent struct{ child }

	p := new(parent)
	v := reflect.ValueOf(p).Elem().Field(0)
	v.Field(0).SetString("hello")           // v.Field = "hello"
	v = v.Addr().Elem()                     // v = *(&v)
	v.Field(0).SetString("goodbye")         // v.Field = "goodbye"

It would appear that v.Addr().Elem() should have the same value, and
that it would be safe to set "goodbye".
However, after CL 66331, any interspersed calls between Field calls
causes the RO flag to be set.
Thus, setting to "goodbye" actually causes a panic.

That CL affects decodeState.indirect which assumes that back-to-back
Value.Addr().Elem() is side-effect free. We fix that logic to keep
track of the Addr() and Elem() calls and set v back to the original
after a full round-trip has occured.

Fixes #24152
Updates #24153

Change-Id: Ie50f8fe963f00cef8515d89d1d5cbc43b76d9f9c
Reviewed-on: https://go-review.googlesource.com/97796
Reviewed-by: Matthew Dempsky <[email protected]>
Run-TryBot: Matthew Dempsky <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
@ianlancetaylor
Copy link
Contributor

The problem described by this issue is working as intended.

@andybons andybons removed this from the Go1.10.1 milestone Mar 26, 2018
@gopherbot
Copy link
Contributor

Change https://golang.org/cl/102784 mentions this issue: [release-branch.go1.10] encoding/json: avoid assuming side-effect free reflect.Value.Addr().Elem()

gopherbot pushed a commit that referenced this issue Mar 29, 2018
…e reflect.Value.Addr().Elem()

Consider the following:
	type child struct{ Field string }
	type parent struct{ child }

	p := new(parent)
	v := reflect.ValueOf(p).Elem().Field(0)
	v.Field(0).SetString("hello")           // v.Field = "hello"
	v = v.Addr().Elem()                     // v = *(&v)
	v.Field(0).SetString("goodbye")         // v.Field = "goodbye"

It would appear that v.Addr().Elem() should have the same value, and
that it would be safe to set "goodbye".
However, after CL 66331, any interspersed calls between Field calls
causes the RO flag to be set.
Thus, setting to "goodbye" actually causes a panic.

That CL affects decodeState.indirect which assumes that back-to-back
Value.Addr().Elem() is side-effect free. We fix that logic to keep
track of the Addr() and Elem() calls and set v back to the original
after a full round-trip has occured.

Fixes #24152
Updates #24153

Change-Id: Ie50f8fe963f00cef8515d89d1d5cbc43b76d9f9c
Reviewed-on: https://go-review.googlesource.com/97796
Reviewed-by: Matthew Dempsky <[email protected]>
Run-TryBot: Matthew Dempsky <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-on: https://go-review.googlesource.com/102784
Run-TryBot: Andrew Bonventre <[email protected]>
Reviewed-by: Joe Tsai <[email protected]>
Reviewed-by: Ian Lance Taylor <[email protected]>
@golang golang locked and limited conversation to collaborators Mar 27, 2019
@dmitshur dmitshur added this to the Go1.11 milestone Sep 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. release-blocker
Projects
None yet
Development

No branches or pull requests

6 participants