|
| 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