fix: ensure Rand overwrites buffer, fix fallback CSPRNG race condition, and add tests for overwrite and concurrency.

lemon-mint committed Nov 19, 2025 at 10:59 UTC cbfb2e8286fa4d6d5d478183d9830d0667e21122
2 files changed +72 -12
portal/utils/randpool/randpool.go
+21 -12
@@ -9,18 +9,21 @@ import (
9 "golang.org/x/crypto/chacha20"
10 )
11
12 -var _csprng_fallback = func() *chacha20.Cipher {
13 - var initdata [12 + 32]byte // 12 byte nonce, 32 byte key
14 - _, err := io.ReadFull(rand.Reader, initdata[:])
15 - if err != nil {
16 - panic(err)
17 - }
18 - c, err := chacha20.NewUnauthenticatedCipher(initdata[12:], initdata[:12])
19 - if err != nil {
20 - panic(err)
21 - }
22 - return c
23 -}()
12 +var (
13 + _csprng_fallback_mu sync.Mutex
14 + _csprng_fallback = func() *chacha20.Cipher {
15 + var initdata [12 + 32]byte // 12 byte nonce, 32 byte key
16 + _, err := io.ReadFull(rand.Reader, initdata[:])
17 + if err != nil {
18 + panic(err)
19 + }
20 + c, err := chacha20.NewUnauthenticatedCipher(initdata[12:], initdata[:12])
21 + if err != nil {
22 + panic(err)
23 + }
24 + return c
25 + }()
26 +)
27
28 type chacha20rng struct {
29 c *chacha20.Cipher
@@ -34,7 +37,9 @@ var _chacha20rngPool = sync.Pool{
37 if err != nil {
38 // if system rand fails, use fallback and print log
39 log.Println("randpool: chacha20rng init failed to read from system rand, using fallback")
40 + _csprng_fallback_mu.Lock()
41 _csprng_fallback.XORKeyStream(initdata[:], initdata[:])
42 + _csprng_fallback_mu.Unlock()
43 }
44 c, err := chacha20.NewUnauthenticatedCipher(initdata[12:], initdata[:12])
45 if err != nil {
@@ -53,6 +58,10 @@ func _chacha20rng() *chacha20rng {
58 func chacha20rand(dst []byte) {
59 c := _chacha20rng()
60 c.used += uint64(len(dst))
61 + // Zero out the destination buffer to ensure we overwrite instead of XOR
62 + for i := range dst {
63 + dst[i] = 0
64 + }
65 c.c.XORKeyStream(dst, dst)
66 if c.used < 50*1<<30 {
67 // Return to pool only if we haven't used more than 50GiB
portal/utils/randpool/randpool_test.go new
+51
@@ -0,0 +1,51 @@
1 +package randpool
2 +
3 +import (
4 + "bytes"
5 + "testing"
6 +)
7 +
8 +func TestRandOverwrite(t *testing.T) {
9 + // Create a buffer with known data
10 + buf := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
11 + original := make([]byte, len(buf))
12 + copy(original, buf)
13 +
14 + // Call Rand
15 + Rand(buf)
16 +
17 + // Verify that the buffer has changed
18 + if bytes.Equal(buf, original) {
19 + t.Error("Buffer should have changed after Rand")
20 + }
21 +
22 + // Verify that it's not just XORed (though hard to prove deterministically without mocking,
23 + // the fact that we zeroed it in code gives us confidence.
24 + // If it was XORed with 0xFF, the result would be (stream ^ 0xFF).
25 + // Since we zeroed it, the result is (stream ^ 0x00) = stream.
26 + // We can't easily distinguish stream vs stream^0xFF without knowing stream.
27 + // But we can check that running it twice produces different results.
28 +
29 + buf2 := make([]byte, 5) // Zeros
30 + Rand(buf2)
31 +
32 + if bytes.Equal(buf, buf2) {
33 + t.Error("Two random calls produced same output")
34 + }
35 +}
36 +
37 +func TestRandConcurrency(t *testing.T) {
38 + // Just run a bunch of goroutines to trigger the pool and potential race conditions
39 + // (though the fallback race is hard to trigger without fault injection)
40 + done := make(chan bool)
41 + for i := 0; i < 100; i++ {
42 + go func() {
43 + buf := make([]byte, 32)
44 + Rand(buf)
45 + done <- true
46 + }()
47 + }
48 + for i := 0; i < 100; i++ {
49 + <-done
50 + }
51 +}