Skip to content

Commit 9450c82

Browse files
authored
Merge pull request #332 from mdlayher/mdl-advent2019
advent-2019: Safe use of unsafe.Pointer blog
2 parents 75eff32 + 1f1daaa commit 9450c82

File tree

1 file changed

+334
-0
lines changed

1 file changed

+334
-0
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
+++
2+
date = "2019-12-05T00:00:00+00:00"
3+
title = "Safe use of unsafe.Pointer"
4+
subtitle = "Using Go tools to safely write unsafe code."
5+
+++
6+
7+
Package [`unsafe`](https://golang.org/pkg/unsafe/) provides an escape hatch from
8+
Go's type system, enabling interactions with low-level and system call APIs, in
9+
a manner similar to C programs. However, `unsafe` has several rules which must
10+
be followed in order to perform these interactions in a sane way. It's easy to
11+
make [subtle mistakes when writing `unsafe` code](https://github.com/golang/sys/commit/b69606af412f43a225c1cf2044c90e317f41ae09),
12+
but these mistakes can often be avoided.
13+
14+
This blog post will introduce some of the current and upcoming Go tooling that can
15+
verify safe usage of the `unsafe.Pointer` type in your Go programs. If you have
16+
not worked with `unsafe` before, I recommend reading my
17+
[previous Gopher Academy Advent series blog](https://blog.gopheracademy.com/advent-2017/unsafe-pointer-and-system-calls/)
18+
on the topic.
19+
20+
Extra care and caution must be taken whenever introducing `unsafe` to a code
21+
base, but these diagnostic tools can help you solve problems before they lead to
22+
a major bug or a possible security flaw in your application.
23+
24+
## Compile-time verification with `go vet`
25+
26+
For several years now, the `go vet` tool has had the ability to check for
27+
invalid conversions between the `unsafe.Pointer` and `uintptr` types.
28+
29+
Let's take a look at an example program. Suppose we would like to use pointer
30+
arithmetic to iterate over and print each element of an array:
31+
32+
```go
33+
package main
34+
35+
import (
36+
"fmt"
37+
"unsafe"
38+
)
39+
40+
func main() {
41+
// An array of contiguous uint32 values stored in memory.
42+
arr := []uint32{1, 2, 3}
43+
44+
// The number of bytes each uint32 occupies: 4.
45+
const size = unsafe.Sizeof(uint32(0))
46+
47+
// Take the initial memory address of the array and begin iteration.
48+
p := uintptr(unsafe.Pointer(&arr[0]))
49+
for i := 0; i < len(arr); i++ {
50+
// Print the integer that resides at the current address and then
51+
// increment the pointer to the next value in the array.
52+
fmt.Printf("%d ", (*(*uint32)(unsafe.Pointer(p))))
53+
p += size
54+
}
55+
}
56+
```
57+
58+
At first glance, this program appears to work as expected, and we can see each
59+
of the array's elements printed to our terminal:
60+
61+
```
62+
$ go run main.go
63+
1 2 3
64+
```
65+
66+
However, there is a subtle flaw in this program. What does `go vet` have to say?
67+
68+
```
69+
$ go vet .
70+
# github.com/mdlayher/example
71+
./main.go:20:33: possible misuse of unsafe.Pointer
72+
```
73+
74+
In order to understand this error, we must consult [the rules](https://golang.org/pkg/unsafe/#Pointer)
75+
of the `unsafe.Pointer` type:
76+
77+
> Converting a Pointer to a uintptr produces the memory address of the value
78+
> pointed at, as an integer. The usual use for such a uintptr is to print it.
79+
>
80+
> Conversion of a uintptr back to Pointer is not valid in general.
81+
>
82+
> A uintptr is an integer, not a reference. Converting a Pointer to a uintptr
83+
> creates an integer value with no pointer semantics. Even if a uintptr holds
84+
> the address of some object, the garbage collector will not update that
85+
> uintptr's value if the object moves, nor will that uintptr keep the object
86+
> from being reclaimed.
87+
88+
We can isolate the offending portion of our program as follows:
89+
90+
```go
91+
p := uintptr(unsafe.Pointer(&arr[0]))
92+
93+
// What happens if there's a garbage collection here?
94+
fmt.Printf("%d ", (*(*uint32)(unsafe.Pointer(p))))
95+
```
96+
97+
Because we store the `uintptr` value in `p` but do not immediately make use of
98+
that value, it's possible that when a garbage collection occurs, the address
99+
(now stored in `p` as a uintptr integer) will no longer be valid!
100+
101+
Let's assume this scenario has occurred and that `p` no longer points at a
102+
`uint32`. Perhaps when we reinterpret `p` as a pointer, the memory pointed at is
103+
now being used to store a user's authentication credentials or a TLS private key.
104+
We've introduced a potential security flaw in our application and could easily
105+
leak sensitive material to an attacker through a normal channel such as `stdout`
106+
or an HTTP API's response body.
107+
108+
In effect, once we've converted an `unsafe.Pointer` to `uintptr`, we cannot
109+
safely convert it back to `unsafe.Pointer`, with the exception of one special
110+
case:
111+
112+
> If p points into an allocated object, it can be advanced through the object by
113+
> conversion to uintptr, addition of an offset, and conversion back to Pointer.
114+
115+
In order to perform this pointer arithmetic iteration logic safely, we must
116+
perform the type conversions and pointer arithmetic all at once:
117+
118+
```go
119+
package main
120+
121+
import (
122+
"fmt"
123+
"unsafe"
124+
)
125+
126+
func main() {
127+
// An array of contiguous uint32 values stored in memory.
128+
arr := []uint32{1, 2, 3}
129+
130+
// The number of bytes each uint32 occupies: 4.
131+
const size = unsafe.Sizeof(uint32(0))
132+
133+
for i := 0; i < len(arr); i++ {
134+
// Print an integer to the screen by:
135+
// - taking the address of the first element of the array
136+
// - applying an offset of (i * 4) bytes to advance into the array
137+
// - converting the uintptr back to *uint32 and dereferencing it to
138+
// print the value
139+
fmt.Printf("%d ", *(*uint32)(unsafe.Pointer(
140+
uintptr(unsafe.Pointer(&arr[0])) + (uintptr(i) * size),
141+
)))
142+
}
143+
}
144+
```
145+
146+
This program produces the same result as before, but is now considered valid by
147+
`go vet` as well!
148+
149+
```
150+
$ go run main.go
151+
1 2 3
152+
$ go vet .
153+
```
154+
155+
I don't recommend using pointer arithmetic for iteration logic in this way, but
156+
it's excellent that Go provides this escape hatch (and tooling for using it
157+
safely!) when it is truly needed.
158+
159+
## Run-time verification with the Go compiler's `checkptr` debugging flag
160+
161+
The Go compiler recently gained support for a [new debugging flag](https://go-review.googlesource.com/c/go/+/162237)
162+
which can instrument uses of `unsafe.Pointer` to detect invalid usage patterns
163+
at runtime. As of Go 1.13, this feature is unreleased, but can be used by
164+
installing Go from tip:
165+
166+
```
167+
$ go get golang.org/dl/gotip
168+
go: finding golang.org/dl latest
169+
...
170+
$ gotip download
171+
Updating the go development tree...
172+
...
173+
Success. You may now run 'gotip'!
174+
$ gotip version
175+
go version devel +8054b13 Thu Nov 28 15:16:27 2019 +0000 linux/amd64
176+
```
177+
178+
Let's review another example. Suppose we are passing a Go structure to a Linux
179+
kernel API which would typically accept a C union. One pattern for doing this is
180+
to have an overarching Go structure which contains a raw byte array (mimicking a
181+
C union), and then to create typed variants for possible argument combinations.
182+
183+
```go
184+
package main
185+
186+
import (
187+
"fmt"
188+
"unsafe"
189+
)
190+
191+
// one is a typed Go structure containing structured data to pass to the kernel.
192+
type one struct{ v uint64 }
193+
194+
// two mimics a C union type which passes a blob of data to the kernel.
195+
type two struct{ b [32]byte }
196+
197+
func main() {
198+
// Suppose we want to send the contents of a to the kernel as raw bytes.
199+
in := one{v: 0xff}
200+
out := (*two)(unsafe.Pointer(&in))
201+
202+
// Assume the kernel will only access the first 8 bytes. But what is stored
203+
// in the remaining 24 bytes?
204+
fmt.Printf("%#v\n", out.b[0:8])
205+
}
206+
```
207+
208+
When we run this program on a stable version of Go (as of Go 1.13.4), we can see
209+
that the first 8 bytes of the array contain our `uint64` data in its native
210+
endian format (little endian on my machine):
211+
212+
```
213+
$ go run main.go
214+
[]byte{0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
215+
```
216+
217+
However, it turns out there is an issue with this program as well. If we attempt
218+
to run the program on Go tip with the `checkptr` debug flag, we will see:
219+
220+
```
221+
$ gotip run -gcflags=all=-d=checkptr main.go
222+
panic: runtime error: unsafe pointer conversion
223+
224+
goroutine 1 [running]:
225+
main.main()
226+
/home/matt/src/github.com/mdlayher/example/main.go:17 +0x60
227+
exit status 2
228+
```
229+
230+
This check is still quite new and as such does not provide much information
231+
beyond the "unsafe pointer conversion" panic message and a stack trace. But the
232+
stack trace does at least provide a hint that line 17 is suspect.
233+
234+
By casting a smaller structure into a larger one, we enable reading arbitrary
235+
memory beyond the end of the smaller structure's data! This is another way that
236+
careless use of `unsafe` could result in a security vulnerability in your
237+
application.
238+
239+
In order to perform this operation safely, we have to make sure that we
240+
initialize the "union" structure first before copying data into it, so we can
241+
ensure that arbitrary memory is not accessed:
242+
243+
```go
244+
package main
245+
246+
import (
247+
"fmt"
248+
"unsafe"
249+
)
250+
251+
// one is a typed Go structure containing structured data to pass to the kernel.
252+
type one struct{ v uint64 }
253+
254+
// two mimics a C union type which passes a blob of data to the kernel.
255+
type two struct{ b [32]byte }
256+
257+
// newTwo safely produces a two structure from an input one.
258+
func newTwo(in one) *two {
259+
// Initialize out and its array.
260+
var out two
261+
262+
// Explicitly copy the contents of in into out by casting both into byte
263+
// arrays and then slicing the arrays. This will produce the correct packed
264+
// union structure, without relying on unsafe casting to a smaller type of a
265+
// larger type.
266+
copy(
267+
(*(*[unsafe.Sizeof(two{})]byte)(unsafe.Pointer(&out)))[:],
268+
(*(*[unsafe.Sizeof(one{})]byte)(unsafe.Pointer(&in)))[:],
269+
)
270+
271+
return &out
272+
}
273+
274+
func main() {
275+
// All is well! The two structure is appropriately initialized.
276+
out := newTwo(one{v: 0xff})
277+
278+
fmt.Printf("%#v\n", out.b[:8])
279+
}
280+
```
281+
282+
We can now run our updated program with the same flags as before, and we will
283+
see that the issue has been resolved:
284+
285+
```
286+
$ gotip run -gcflags=all=-d=checkptr main.go
287+
[]byte{0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
288+
```
289+
290+
By removing the slicing operation from the `fmt.Printf` call, we can verify that
291+
the remainder of the byte array has been initialized to `0` bytes:
292+
293+
```
294+
[32]uint8{
295+
0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
296+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
297+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
298+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
299+
}
300+
```
301+
302+
This is a very easy mistake to make, and in fact, I recently had to fix this
303+
exact issue in code I wrote for [the tests in `x/sys/unix`](https://github.com/golang/sys/commit/b69606af412f43a225c1cf2044c90e317f41ae09)!
304+
I've written a fair amount of `unsafe` code in Go, but even veteran programmers
305+
can easily make mistakes. This is why these types of diagnostic tools are so
306+
important!
307+
308+
## Conclusion
309+
310+
Package `unsafe` is a very powerful tool with a razor-sharp edge, but it should
311+
not be feared. When interacting with Linux kernel system call APIs, it is often
312+
necessary to resort to `unsafe` code. Making effective use of tools such as
313+
`go vet` and the `checkptr` compiler debugging flag is crucial in order to
314+
ensure safety in your applications.
315+
316+
If you work with `unsafe` code on a regular basis, I highly recommend joining
317+
the [`#darkarts` channel on Gophers Slack](https://invite.slack.golangbridge.org/).
318+
There are a lot of veterans in that channel who have helped me learn how to make
319+
effective use of `unsafe` in my own applications.
320+
321+
If you have any questions, feel free to contact me! I'm mdlayher on
322+
[Gophers Slack](https://gophers.slack.com/), [GitHub](https://github.com/mdlayher)
323+
and [Twitter](https://twitter.com/mdlayher).
324+
325+
Special thanks to:
326+
327+
- [Cuong Manh Le (@cuonglm)](https://github.com/cuonglm) for his insight regarding the [`=all` modifier for the `checkptr` debugging flag](https://github.com/gopheracademy/gopheracademy-web/pull/332#discussion_r351896035)
328+
- [Miki Tebeka (@tebeka)](https://github.com/tebeka) for review of this post
329+
330+
## Links
331+
332+
* [Package `unsafe`](https://golang.org/pkg/unsafe/)
333+
* [Gopher Academy: unsafe.Pointer and system calls](https://blog.gopheracademy.com/advent-2017/unsafe-pointer-and-system-calls/)
334+
* [cmd/compile: add -d=checkptr to validate unsafe.Pointer rules](https://go-review.googlesource.com/c/go/+/162237)

0 commit comments

Comments
 (0)