Chris Seaton
Chris Seaton

Reputation: 290

When is it safe in Go to reference an object only through a `uintptr`?

The Go Programming Language says in Section 13.2 that this is code is safe and x will always be visible to the garbage collector:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

And that this code is unsafe, because x is temporarily not visible to the garbage collector, which could move it, making pb a dangling pointer:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

But I can't see the difference between these two examples.

In the case described as safe, after uintptr has been called, the only reference to the x is the uintptr value, isn't it? There's a Pointer to it on the same line, but it was an argument to uintptr, which has run, so nothing is referencing the arguments, and so the Pointer is not live and the uintptr is the only reference to the object.

I can't see how storing the uintptr in a local variable instead of as an expression intermediate value makes it any more safe. Aren't local variables like tmp removed in compiler phases anyway, becoming anonymous dataflow edges, so that the generated code should be semantically equivalent? Or does Go have some rules for when garbage collection can run? Such as having safepoints only between statements? But the code in the first example has method calls so I would presume they would always be safepoints?

Upvotes: 3

Views: 1052

Answers (1)

Elias Van Ootegem
Elias Van Ootegem

Reputation: 76423

Found the reference I hinted at in my comments here

A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.

What this means is that this expression:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

Is safe because you're creating a uintptr, which is seen as an integer, not a reference, but it's immediately assigned (unless there's a race condition somewhere else, the object that x references cannot be GC'ed) until after the assignment). The uintptr (again: integer type) is also immediately cast to a pointer, turning it into a reference so the GC will manage pb. This means that:

  • uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)): all safe, because x clearly is an existing reference to an object
  • pb is assigned an integer that is (through the cast) marked as a reference to an int16 object

However, when you write this:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))

There is a chance that, between assigning tmp (remember integer, not reference), the actual object in memory is moved. As it says in the docs: tmp will not be updated. Thus, when you assign pb, you could end up with an invalid pointer.
think of tmp in this case as x in the first case. Rather than being a reference to an object, it's as if you wrote

tmp := 123456 // a random integer
pb := (*int16) (unsafe.Pointer(tmp)) // not safe, obviously

For example:

var pb *int16
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
go func() {
    time.Sleep(1 * time.Second)
    pb = (*int16)(unsafe.Pointer(tmp))
}()
// original value of x could be GC'ed here, before the goroutine starts, or the time.Sleep call returns
x = TypeOfX{
    b: 123,
}

Upvotes: 2

Related Questions