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