diff --git a/pkg/rand/BUILD b/pkg/rand/BUILD index 46ebdaf4c..0e3bb37b4 100644 --- a/pkg/rand/BUILD +++ b/pkg/rand/BUILD @@ -1,4 +1,4 @@ -load("//tools:defs.bzl", "go_library") +load("//tools:defs.bzl", "go_library", "go_test") package( default_applicable_licenses = ["//:license"], @@ -18,3 +18,9 @@ go_library( "@org_golang_x_sys//unix:go_default_library", ], ) + +go_test( + name = "rand_test", + srcs = ["rng_test.go"], + library = ":rand", +) diff --git a/pkg/rand/rng.go b/pkg/rand/rng.go index f35944662..d11d9f3fc 100644 --- a/pkg/rand/rng.go +++ b/pkg/rand/rng.go @@ -39,6 +39,84 @@ func (rg *RNG) Uint32() uint32 { if _, err := rg.Reader.Read(data[:]); err != nil { panic(fmt.Sprintf("Read() failed: %v", err)) } - // The endianness doesn't matter here as it's random bytes either way. - return binary.LittleEndian.Uint32(data[:]) + return binary.NativeEndian.Uint32(data[:]) +} + +// Int63n is analogous to the standard library's math/rand.Int63n. +func (rg *RNG) Int63n(n int64) int64 { + // Based on Go's rand package implementation, but using + // cryptographically secure random numbers. + if n <= 0 { + panic(fmt.Sprintf("n must be positive, but got %d", n)) + } + + // This can be done quickly when n is a power of 2. + if n&(n-1) == 0 { + return int64(rg.Uint64()) & (n - 1) + } + + // The naive approach would be to return rg.Int63()%n, but we need the + // random number to be fair. It shouldn't be biased towards certain + // results, but simple modular math can be very biased. For example, if + // n is 40% of the maximum int64, then the output values of rg.Int63 + // map to return values as follows: + // + // - The first 40% of values map to themselves. + // - The second 40% map to themselves - maximum int64. + // - The remaining 20% map to the themselves - 2 * (maximum int64), + // i.e. the first half of possible output values. + // + // And thus 60% of results map the the first half of possible output + // values, and 40% map the second half. Oops! + // + // We use the same trick as Go to deal with this: shave off the last + // segment (the 20% in our example) to make the RNG more fair. + // + // In the worst case, n is just over half of maximum int64, meaning + // that the upper half of rg.Int63 return values are bad. So each call + // to rg.Int63 has, at worst, a 50% chance of needing a retry. + maximum := int64((1 << 63) - 1 - (1<<63)%uint64(n)) + ret := rg.Int63() + for ret > maximum { + ret = rg.Int63() + } + return ret % n +} + +// Int63 is analogous to the standard library's math/rand.Int63. +func (rg *RNG) Int63() int64 { + return ((1 << 63) - 1) & int64(rg.Uint64()) +} + +// Uint64 is analogous to the standard library's math/rand.Uint64. +func (rg *RNG) Uint64() uint64 { + var data [8]byte + if _, err := rg.Reader.Read(data[:]); err != nil { + panic(fmt.Sprintf("Read() failed: %v", err)) + } + return binary.NativeEndian.Uint64(data[:]) +} + +// Uint32 is analogous to the standard library's math/rand.Uint32. +func Uint32() uint32 { + rng := RNG{Reader: Reader} + return rng.Uint32() +} + +// Int63n is analogous to the standard library's math/rand.Int63n. +func Int63n(n int64) int64 { + rng := RNG{Reader: Reader} + return rng.Int63n(n) +} + +// Int63 is analogous to the standard library's math/rand.Int63. +func Int63() int64 { + rng := RNG{Reader: Reader} + return rng.Int63() +} + +// Uint64 is analogous to the standard library's math/rand.Uint64. +func Uint64() uint64 { + rng := RNG{Reader: Reader} + return rng.Uint64() } diff --git a/pkg/rand/rng_test.go b/pkg/rand/rng_test.go new file mode 100644 index 000000000..b738567ac --- /dev/null +++ b/pkg/rand/rng_test.go @@ -0,0 +1,42 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rand + +import ( + "math/rand" + "testing" +) + +const testIterations = 100_000 + +func TestInt63n(t *testing.T) { + for i := 0; i < testIterations; i++ { + maximum := rand.Int63() + for maximum <= 0 { + maximum = rand.Int63() + } + if n := Int63n(maximum); n >= maximum || n < 0 { + t.Errorf("Int63n(%d) returned bad value %d", maximum, n) + } + } +} + +func TestInt63(t *testing.T) { + for i := 0; i < testIterations; i++ { + if n := Int63(); n < 0 { + t.Errorf("Int63() returned bad value %d", n) + } + } +} diff --git a/pkg/sentry/arch/BUILD b/pkg/sentry/arch/BUILD index d98f4416a..7ae406c23 100644 --- a/pkg/sentry/arch/BUILD +++ b/pkg/sentry/arch/BUILD @@ -36,6 +36,7 @@ go_library( "//pkg/log", "//pkg/marshal", "//pkg/marshal/primitive", + "//pkg/rand", "//pkg/sentry/arch/fpu", "//pkg/sentry/limits", "//pkg/usermem", diff --git a/pkg/sentry/arch/arch_amd64.go b/pkg/sentry/arch/arch_amd64.go index fe106d647..6240b6803 100644 --- a/pkg/sentry/arch/arch_amd64.go +++ b/pkg/sentry/arch/arch_amd64.go @@ -20,12 +20,12 @@ package arch import ( "bytes" "fmt" - "math/rand" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/marshal" "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/rand" "gvisor.dev/gvisor/pkg/sentry/arch/fpu" "gvisor.dev/gvisor/pkg/sentry/limits" ) diff --git a/pkg/sentry/arch/arch_arm64.go b/pkg/sentry/arch/arch_arm64.go index 65849cd68..9e002de39 100644 --- a/pkg/sentry/arch/arch_arm64.go +++ b/pkg/sentry/arch/arch_arm64.go @@ -19,12 +19,12 @@ package arch import ( "fmt" - "math/rand" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/marshal" "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/rand" "gvisor.dev/gvisor/pkg/sentry/arch/fpu" "gvisor.dev/gvisor/pkg/sentry/limits" )