riscv-qemu: add VirtIO RNG device

This implements machine.GetRNG() using VirtIO. This gets the tests to
pass for crypto/md5 and crypto/sha1 that use crypto/rand in their tests.
This commit is contained in:
Ayke van Laethem
2025-03-10 11:34:29 +01:00
committed by Ron Evans
parent 7f970a45c2
commit cab3834bc9
6 changed files with 193 additions and 7 deletions
-3
View File
@@ -429,7 +429,6 @@ TEST_PACKAGES_NONWASM = \
#
# * No filesystem is available, so packages like compress/zlib can't be tested
# (just like wasm).
# * There is no RNG implemented (TODO, I think this is fixable).
# * picolibc math functions apparently are less precise, the math package
# fails on baremetal.
# * Some packages fail or hang for an unknown reason, this should be
@@ -438,8 +437,6 @@ TEST_PACKAGES_BAREMETAL = $(filter-out $(TEST_PACKAGES_NONBAREMETAL), $(TEST_PAC
TEST_PACKAGES_NONBAREMETAL = \
$(TEST_PACKAGES_NONWASM) \
crypto/elliptic \
crypto/md5 \
crypto/sha1 \
math \
reflect \
encoding/asn1 \
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey
//go:build nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt)
// If you update the above build constraint, you'll probably also need to update
// src/runtime/rand_hwrng.go.
+189
View File
@@ -0,0 +1,189 @@
//go:build tinygo.riscv32 && virt
// Machine implementation for VirtIO targets.
// At the moment only QEMU RISC-V is supported, but support for ARM for example
// should not be difficult to add with a change to virtioFindDevice.
package machine
import (
"errors"
"runtime/volatile"
"sync"
"unsafe"
)
const deviceName = "riscv-qemu"
func (p Pin) Set(high bool) {
// no pins defined
}
var rngLock sync.Mutex
var rngDevice *virtioDevice1
var rngBuf volatile.Register32
var errNoRNG = errors.New("machine: no entropy source found")
var errNoRNGData = errors.New("machine: entropy source didn't return enough data")
// GetRNG returns random numbers from a VirtIO entropy source.
// When running in QEMU, it requires adding the RNG device:
//
// -device virtio-rng-device
func GetRNG() (uint32, error) {
rngLock.Lock()
// Initialize the device on first use.
if rngDevice == nil {
// Search for an available RNG.
rngDevice = virtioFindDevice(virtioDeviceEntropySource)
if rngDevice == nil {
rngLock.Unlock()
return 0, errNoRNG
}
// Initialize the device.
rngDevice.status.Set(0) // reset device
rngDevice.status.Set(virtioDeviceStatusAcknowledge)
rngDevice.status.Set(virtioDeviceStatusAcknowledge | virtioDeviceStatusDriver)
rngDevice.hostFeaturesSel.Set(0)
rngDevice.status.Set(virtioDeviceStatusAcknowledge | virtioDeviceStatusDriver | virtioDeviceStatusDriverOk)
rngDevice.guestPageSize.Set(4096)
// Configure queue, according to section 4.2.4 "Legacy interface".
// Note: we're skipping checks for queuePFM and queueNumMax.
rngDevice.queueSel.Set(0) // use queue 0 (the only queue)
rngDevice.queueNum.Set(1) // use a single buffer in the queue
rngDevice.queueAlign.Set(4096) // default alignment appears to be 4096
rngDevice.queuePFN.Set(uint32(uintptr(unsafe.Pointer(&rngQueue))) / 4096)
// Configure the only buffer in the queue (but don't increment
// rngQueue.available yet).
rngQueue.buffers[0].address = uint64(uintptr(unsafe.Pointer(&rngBuf)))
rngQueue.buffers[0].length = uint32(unsafe.Sizeof(rngBuf))
rngQueue.buffers[0].flags = 2 // 2 means write-only buffer
}
// Increment the available ring buffer. This doesn't actually change the
// buffer index (it's a ring with a single entry), but the number needs to
// be incremented otherwise the device won't recognize a new buffer.
index := rngQueue.available.index
rngQueue.available.index = index + 1
rngDevice.queueNotify.Set(0) // notify the device of the 'new' (reused) buffer
for rngQueue.used.index.Get() != index+1 {
// Busy wait until the RNG buffer is filled.
// A better way would be to wait for an interrupt, but since this driver
// implementation is mostly used for testing it's good enough for now.
}
// Check that we indeed got 4 bytes back.
if rngQueue.used.ring[0].length != 4 {
rngLock.Unlock()
return 0, errNoRNGData
}
// Read the resulting random numbers.
result := rngBuf.Get()
rngLock.Unlock()
return result, nil
}
// Implement a driver for the VirtIO entropy device.
// https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html
// http://wiki.osdev.org/Virtio
// http://www.dumais.io/index.php?article=aca38a9a2b065b24dfa1dee728062a12
const (
virtioDeviceStatusAcknowledge = 1
virtioDeviceStatusDriver = 2
virtioDeviceStatusDriverOk = 4
virtioDeviceStatusFeaturesOk = 8
virtioDeviceStatusFailed = 128
)
const (
virtioDeviceReserved = iota
virtioDeviceNetworkCard
virtioDeviceBlockDevice
virtioDeviceConsole
virtioDeviceEntropySource
// there are more device types
)
// VirtIO device version 1
type virtioDevice1 struct {
magic volatile.Register32 // always 0x74726976
version volatile.Register32
deviceID volatile.Register32
vendorID volatile.Register32
hostFeatures volatile.Register32
hostFeaturesSel volatile.Register32
_ [2]uint32
guestFeatures volatile.Register32
guestFeaturesSel volatile.Register32
guestPageSize volatile.Register32
_ uint32
queueSel volatile.Register32
queueNumMax volatile.Register32
queueNum volatile.Register32
queueAlign volatile.Register32
queuePFN volatile.Register32
_ [3]uint32
queueNotify volatile.Register32
_ [3]uint32
interruptStatus volatile.Register32
interruptAck volatile.Register32
_ [2]uint32
status volatile.Register32
}
// VirtIO queue, with a single buffer.
type virtioQueue struct {
buffers [1]struct {
address uint64
length uint32
flags uint16
next uint16
} // 16 bytes
available struct {
flags uint16
index uint16
ring [1]uint16
eventIndex uint16
} // 8 bytes
_ [4096 - 16*1 - 8*1]byte // padding (to align on a 4096 byte boundary)
used struct {
flags uint16
index volatile.Register16
ring [1]struct {
index uint32
length uint32
}
availEvent uint16
}
}
func virtioFindDevice(deviceID uint32) *virtioDevice1 {
// On RISC-V, QEMU defines 8 VirtIO devices starting at 0x10001000 and
// repeating every 0x1000 bytes.
// The memory map can be seen in the QEMU source code:
// https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c
for i := 0; i < 8; i++ {
dev := (*virtioDevice1)(unsafe.Pointer(uintptr(0x10001000 + i*0x1000)))
if dev.magic.Get() != 0x74726976 || dev.version.Get() != 1 || dev.deviceID.Get() != deviceID {
continue
}
return dev
}
return nil
}
// A VirtIO queue needs to be page-aligned.
//
//go:align 4096
var rngQueue virtioQueue
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal && (nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey)
//go:build baremetal && (nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt))
// If you update the above build constraint, you'll probably also need to update
// src/crypto/rand/rand_baremetal.go.
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey)
//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt))
package runtime
+1 -1
View File
@@ -4,5 +4,5 @@
"build-tags": ["virt", "qemu"],
"default-stack-size": 4096,
"linkerscript": "targets/riscv-qemu.ld",
"emulator": "qemu-system-riscv32 -machine virt -nographic -bios none -kernel {}"
"emulator": "qemu-system-riscv32 -machine virt -nographic -bios none -device virtio-rng-device -kernel {}"
}