Skip to content
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

reflect: Value.Addr().Elem() loses flagEmbedRO in origin value #32772

Closed
kezhuw opened this issue Jun 25, 2019 · 8 comments
Closed

reflect: Value.Addr().Elem() loses flagEmbedRO in origin value #32772

kezhuw opened this issue Jun 25, 2019 · 8 comments
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@kezhuw
Copy link
Contributor

kezhuw commented Jun 25, 2019

This makes exported fields in unexported anonymous field not settable

What version of Go are you using (go version)?

$ go version
go version go1.12.6 darwin/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/Cellar/go/1.12.6/libexec"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.12.6/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/b6/81wsy1517sxdpkd09pwj98_w0000gn/T/go-build799570696=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

https://play.golang.org/p/mKRa1-3j2He

package main

import (
	"fmt"
	"reflect"
)

type embed struct{ X int }

type S1 struct {
	embed
}

func printCanSet(value reflect.Value, index []int) {
	current := value
	for _, i := range index {
		if i == -1 {
		        // Addr() decays flagRO, which is a combination of flagEmbedRO and flagStickyRO, to flagStickyRO which is propagated to
			// all nested fields, thus making exported fields in unexported anonymous field not settable.
			//
			// See for implementation details:
			// * https://github.com/golang/go/blob/master/src/reflect/value.go#L83
			// * https://github.com/golang/go/blob/master/src/reflect/value.go#L256
			// * https://github.com/golang/go/blob/master/src/reflect/value.go#L829
			current = current.Addr().Elem()
		} else {
			current = current.Field(i)
		}
	}
	fmt.Printf("Value %+v lookup index %+v: CanSet ? %t\n", value, index, current.CanSet())
}

func main() {
	value := reflect.ValueOf(&S1{}).Elem()
	printCanSet(value, []int{0, 0})
	printCanSet(value, []int{0, -1, 0})
}

What did you expect to see?

The second should prints true for CanSet.

What did you see instead?

The second prints false for CanSet.

See also

@ianlancetaylor
Copy link
Contributor

I don't really see how to fix this safely: ensuring that the Addr can not be used in some way to set the unexported fields. If you think that your changes are safe, do you want to send a pull request? Thanks.

@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jun 25, 2019
@ianlancetaylor ianlancetaylor added this to the Go1.14 milestone Jun 25, 2019
@kezhuw
Copy link
Contributor Author

kezhuw commented Jun 26, 2019

@ianlancetaylor I thought after your response, I think it is safe for reasons:

  • Result of Value.Addr() is not CanAddr and hence not CanSet, so the field itself can't be modified later, this does not interfere with flagRO.
  • Addr().Elem() get flagRO same as origin value, so it should behave same as before in CanSet.
  • Value.Field() inherits flagStickyRO but not flagEmbedRO from wrapped value, so
    • if the value itself is flagEmbedRO, Value.Field()'s flagRO depends only on that field but not its wrapped value;
    • if the value itself is flagStickyRO, all its fields are unexported.

I have update commits with more test cases in trial fix branch to (kezhuw/go@306e9c9 kezhuw/go@a16093d), I plan to send a pull request later after job (at least 10 hours late) if above sounds good to you.

See Value.Field().

kezhuw added a commit to kezhuw/go that referenced this issue Jun 26, 2019
Currently, `Value.Addr()` collapses `flagRO`, which is a combination of
`flagEmbedRO` and `flagStickyRO`, to `flagStickyRO`. This causes exported
fields of unexported anonymous field from `Value.Addr().Elem()` read only.

This commit fix this by inheriting all bits of `flagRO` from origin
value in `Value.Addr()`. This should be safe due to following reasons:
* Result of `Value.Addr()` is not `CanSet()` because of it is not `CanAddr()`,
  but not `flagRO`.
* `Addr().Elem()` get same `flagRO` as origin, so it should behave same as
  origin in `CanSet()`.

Fix golang#32772.
kezhuw added a commit to kezhuw/go that referenced this issue Jun 26, 2019
Currently, Value.Addr() collapses flagRO, which is a combination of
flagEmbedRO and flagStickyRO, to flagStickyRO. This causes exported
fields of unexported anonymous field from Value.Addr().Elem() read only.

This commit fix this by inheriting all bits of flagRO from origin
value in Value.Addr(). This should be safe due to following reasons:
* Result of Value.Addr() is not CanSet() because of it is not CanAddr()
  but not flagRO.
* Addr().Elem() get same flagRO as origin, so it should behave same as
  origin in CanSet().

Fixes golang#32772.
@gopherbot
Copy link

Change https://golang.org/cl/183937 mentions this issue: Fix lost flagEmbedRO in reflect.Value.Addr().Elem()

@rsc rsc modified the milestones: Go1.14, Backlog Oct 9, 2019
@smasher164 smasher164 modified the milestones: Backlog, Go1.14 Oct 11, 2019
@ianlancetaylor ianlancetaylor modified the milestones: Go1.14, Go1.15 Dec 5, 2019
@mdempsky
Copy link
Member

I'm sympathetic to the rationale that if v.CanAddr(), then v.Addr().Elem() should be the same as just v alone. After all, if x is an addressable Go expression, then *&x is the same as x.

In general, package reflect exposes the same API as you could use through a package's exported interface. An issue arises when fields are promoted from a non-exported embedded field. For example:

type t struct { X int }
type U struct { t }
var V U

An external package can access V.X, which is short-hand for V.t.X (though the latter isn't actually in external packages).

This is problematic for package reflect because reflect doesn't provide a way to directly access promoted fields. They have to be accessed field-by-field, and the behavior differed between cmd/compile (née gc) and gccgo. It was decided in #12367 (included in Go 1.6) to reconcile this by special casing accesses like v.Field(0).Field(0) if the result is an exported field. That is, to special case V.t.X, since package reflect doesn't support V.X directly. We also special case dereferences, because of pointer embedding. (This was further fixed in #22031, released in Go 1.10.)

This is where the intuition about *& falls down in my opinion: is that intuition important enough to further special case expressions (*&V.t).X? I don't think it's unreasonable, but I'm also not convinced.

@kezhuw You mentioned finding this when upgrading from Go 1.5 to Go 1.11. Do you mean this broke some code you had written? Can you share the code?

@kezhuw
Copy link
Contributor Author

kezhuw commented Apr 19, 2020

@mdempsky You can see the last two commits in https://github.com/kezhuw/toml/commits/9d1a8af4e1bcb6c1efa7523c915d9570b4a40e48. I have push another branch https://github.com/kezhuw/toml/commits/go-issues-32772 to revert the fix and preserve only one test case, you can go test . to see the panic stack.

@kezhuw
Copy link
Contributor Author

kezhuw commented Apr 19, 2020

@mdempsky Here is build matrix https://travis-ci.org/github/kezhuw/toml/builds/676804186 after reverting the fix.

@mdempsky
Copy link
Member

@kezhuw Thanks. Knowing this broke real world code rather than just a theoretical concern is useful information.

@ianlancetaylor What do you think? Package reflect today special cases V.t.X as the way to access promoted field V.X. Should it also special case (*&V.t).X (when V.t is addressable)?

I can't think of any negative consequences to this aside from theoretical purity.

@ianlancetaylor
Copy link
Contributor

Thanks for the analysis. I'm OK with the change if it doesn't introduce any holes in the type system, which it sounds like it doesn't.

xujianhai666 pushed a commit to xujianhai666/go-1 that referenced this issue May 21, 2020
Currently, Value.Addr collapses flagRO, which is a combination of
flagEmbedRO and flagStickyRO, to flagStickyRO. This causes exported
fields of unexported anonymous field from Value.Addr.Elem read only.

This commit fix this by keeping all bits of flagRO from origin
value in Value.Addr. This should be safe due to following reasons:
* Result of Value.Addr is not CanSet because of it is not CanAddr
   but not flagRO.
* Addr.Elem get same flagRO as origin, so it should behave same as
   origin in CanSet.

Fixes golang#32772.

Change-Id: I79e086628c0fb6569a50ce63f3b95916f997eda1
GitHub-Last-Rev: 78e280e
GitHub-Pull-Request: golang#32787
Reviewed-on: https://go-review.googlesource.com/c/go/+/183937
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
@golang golang locked and limited conversation to collaborators May 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants