Skip to content

proposal: add built-in function "formallyRead" for preventing elimination of "dead" stores #33325

Closed
@nsajko

Description

@nsajko

Dead stores are writes to variables that are not followed by corresponding reads. While dead store elimination is mostly a desirable optimization, some of the time it is essential for program correctness that dead store elimination (or the unreachable code elimination that it sometimes implies) does not happen.

The two examples on my mind are:

  • Secure zeroization of sensitive data (also known as cleansing, wiping, etc.), where the store (to computer memory) itself is essential, and it should not legitimately be followed by a read.

  • Benchmarking, where the problem is the elimination of code that is meant to be benchmarked

In C these problems might be solved by using memset_s, volatile types or inline assembly tricks.

In current Go user code, convoluted efforts to prevent dead store elimination for wiping memory and to prevent dead code elimination for benchmarking ultimately rely only on the lack of optimizations in current Go implementations, instead of on formal correctness, which this proposal aims to enable in addition to simplification of user code.

The new built-in "formallyRead" is meant to return nothing and either

  • takes a pointer or an arbitrary number of pointers

  • takes a slice or an arbitrary number of slices

  • if package "unsafe" is imported it additionally can take an unsafe.Pointer and a uintptr (to indicate base and size of a memory object)

The pointer arguments to formallyRead indicate objects that must be considered as having formally been read at the point of the formallyRead invocation. Slice arguments indicate the same for the slice's underlying array in the range up to len(slice).

The following minimal example is meant to show the effects of formallyRead :

package main

func main() {
                ...

                var key [32]byte

                // Do cryptographic operations with the key
                ...

                for i := range key {
                                key[i] = 0
                }
                // Pretend to read the key so the compiler would not optimize out the preceding loop.
                formallyRead(&key) // or formallyRead(key[:])

                ...
}

The following example showcases the cutting down of benchmark code that formallyRead would enable, because use of sink package level variables is no longer necessary:

var GlobalF float64

func BenchmarkAcos(b *testing.B) {
	x := 0.0
	for i := 0; i < b.N; i++ {
		x = Acos(.5)
	}
	GlobalF = x
}

turns into:

func BenchmarkAcos(b *testing.B) {
	for i := 0; i < b.N; i++ {
		x := Acos(.5)
		formallyRead(&x)
	}
}

See also:

Regarding benchmarking:

// Global exported variables are used to store the

https://groups.google.com/forum/#!topic/golang-dev/eCPwwvqs2Cg

A more ambitious and ambiguous proposal regarding the zeroization use case:

#21374

Discusses some ugly hacks that are done in the name of zeroization:

#21865

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions