Skip to content

kernel: the SG2002 TRNG as a hw_random driver, loaded on demand - #375

Merged
dormancygrace merged 2 commits into
mainfrom
kernel/hw-trng
Oct 21, 2026
Merged

dormancygrace merged 2 commits into
mainfrom
kernel/hw-trng

Conversation

@dormancygrace

@dormancygrace dormancygrace commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

What

The SG2002's true random number generator (TRM 22.2) gets a driver of our own, cv181x_trng, which registers it with the kernel's hw_random core as /dev/hwrng.

  • It is built and installed like the accelerator drivers of kernel: TPU, IVE, DWA, RGN and DDR bandwidth drivers, loaded on demand #361, and blacklisted in nanokvm-accelerators.conf, so udev does not load it at boot. modprobe cv181x_trng loads it.
  • Nothing in NanoKVM OS loads the module or reads /dev/hwrng.
  • The driver sets no quality, so while it is loaded the hw_random core credits it fully and feeds the kernel's entropy pool from it. /sys/class/misc/hw_random/rng_quality lowers that.

Changes:

  • platform/modules/cv181x-trng: the driver, built in platform/build.sh.
  • firmware/boards/sg2002-nanokvm-common.dtsi: node rng@2070000 with the two clocks.
  • the blacklist line;
  • docs/ACCELERATORS.md.

No kernel patch, no config change (CONFIG_HW_RANDOM=y already), no kernel release bump. The DTB and the kmod package change content.

The clock

  • The block sits behind the efuse clock gates (clk_efuse, clk_apb_efuse), which Linux turns off as unused at boot.
  • With them off, any register access hangs the bus, and the board restarts through the watchdog.
  • U-Boot leaves every gate on. Comparing its clk_en_0..4 words (all 0xffffffff) with Linux's and enabling only those two gates made the block answer.
  • The node names both clocks, and the driver enables them before it touches a register.

The core

The register layout and COREKIT_REL (0x1c) = 0x200a indicate the Synopsys NIST TRNG core. This identification is inferred from the hardware; no vendor document says so. The TRM documents only part of the core and omits the following, all found on the stand:

  • ALARMS (0x18) is a register the TRM does not list.
  • ZEROIZE is command 0xf and completes in ISTAT bit 0.
  • A second GEN_NOISE, or a RENEW_STATE, on an instantiated generator raises the illegal-command-sequence alarm (ALARMS bit 4). After that the core ignores every command.
  • ZEROIZE with that alarm still pending hung the bus.

The driver's sequences:

  • Seed: write all ones to ALARMS and ISTAT, then ZEROIZE, GEN_NOISE and CREATE_STATE.
  • When seeds happen: when the device becomes the current hw_random source, and again after every 1024 outputs (16 KiB).
  • Unload: ZEROIZE.
  • Every command: checks ALARMS, and returns -EIO on an alarm. The next read then seeds again.

Tested on the stand (NanoKVM PCIe, Linux 7.3)

  • 1 MiB read in 3.6 s, across 64 seeds.
  • 256 KiB does not compress: gzip -9 output is 262202 bytes. This is a sanity check, not a statistical test; no rngtest/FIPS run was made.
  • Three unloads and loads in a row, each followed by new output.
  • The efuse clocks have an enable count of 1 while the module is loaded and 0 after unload.
  • No alarms in the kernel log.

The stand restarted twice through the watchdog during development: once on a register read with the clocks gated, and once on ZEROIZE with an alarm pending. The driver cannot do either, and the doc warns against devmem on the block.

Not covered

  • Nonblocking reads do not poll the hardware: they consume only any bytes already buffered by hw_random, or return EAGAIN. Fresh output requires a blocking read. Regression tests exercise the actual driver code with mocked MMIO, including reseeding, alarms, timeouts, partial reads and cleanup (added in review).
  • Not tested on the other board variants. The node is in the shared dtsi, and the SoC is the same.

🤖 Generated with Claude Code

The true random number generator of the security subsystem (TRM 22.2)
gets a driver of our own, cv181x_trng: it registers 128-bit outputs of
the core's DRBG with the hw_random core as /dev/hwrng, seeds it from the
noise source when it becomes the current source and again after every
1024 outputs, and zeroizes it on unload. Like the other accelerator
drivers it is built and installed but blacklisted, so udev does not load
it at boot; modprobe cv181x_trng loads it.

The block sits behind the efuse clock gates (clk_efuse, clk_apb_efuse),
which Linux turns off as unused: with them off, a register read hangs
the bus and the board restarts through the watchdog. U-Boot leaves every
gate on; comparing its clk_en words with Linux's and enabling only the
two efuse gates made the block answer. The device-tree node names both
clocks, and the driver enables them before it touches a register.

The registers are the Synopsys NIST TRNG core's (COREKIT_REL 0x200a),
which the TRM documents only in part. A second GEN_NOISE or a
RENEW_STATE on an instantiated generator raises its illegal-sequence
alarm, after which the core ignores every command, and ZEROIZE with that
alarm pending hung the bus. Each seed therefore clears ALARMS and ISTAT,
zeroizes (command 0xf, ISTAT bit 0) and instantiates from fresh noise;
every command checks ALARMS.

On the stand, Linux 7.3: 1 MiB in 3.6 s across 64 seeds, output that
does not compress, three unloads and loads in a row, and the efuse
clocks on only while the module is loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Build outputs

Compared with the build of main at 9c1b999.

32 changed, 1 added, 0 removed of 561 outputs.

directory files
changed boot 16 files
changed boot-fit boot-fit/alpha.sd
boot-fit/beta.sd
boot-fit/detect.sd
boot-fit/lite.sd
boot-fit/pcie.sd
changed dtb dtb/alpha.dtb
dtb/beta.dtb
dtb/detect.dtb
dtb/lite.dtb
dtb/pcie.dtb
changed lib lib/modules/7.3.0-nanokvm-os-r0/modules.alias
lib/modules/7.3.0-nanokvm-os-r0/modules.alias.bin
lib/modules/7.3.0-nanokvm-os-r0/modules.dep
lib/modules/7.3.0-nanokvm-os-r0/modules.dep.bin
lib/modules/7.3.0-nanokvm-os-r0/modules.symbols.bin
changed opensbi opensbi/build-manifest.json (records the hash of platform/build.sh)
added lib lib/modules/7.3.0-nanokvm-os-r0/extra/cv181x_trng.ko
Hashes: outputs-sha256 artifact of this run.

@dormancygrace dormancygrace left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found one API correctness issue: the read callback ignores the nonblocking request and enters sleepable hardware polling. Fix and regression coverage are being prepared. The original firmware build passed. The Go job failed in TestAutostartPreservesNamesAndSeparateLogs (empty log immediately after the wrapper published exited); this code is unchanged by this PR and its wrapper publishes status before the pipe reader finishes writing the log. Checking that separately from the TRNG change.

if (ret)
break;
}
ret = trng_command(trng, CMD_GEN_RANDOM, ISTAT_DONE);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Honor wait=false before issuing sleepable commands. hw_random passes false for an O_NONBLOCK read, but this path calls readl_poll_timeout (and may first run three seed commands), each with a 100 ms timeout. Thus a nonblocking consumer can sleep repeatedly and receive ETIMEDOUT instead of promptly observing no available data. Since this implementation has no asynchronous output queue, a minimal fix is to return 0 before touching hardware when !wait; hw_random will return EAGAIN if its own buffer is empty, while normal blocking reads and the entropy feeder retain the tested sequence. Add execution coverage for seeded, unseeded and reseed-due states. Confirmed with the actual callback compiled against an MMIO model; the nonblocking assertion fails on this head.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 264dbd7. wait=false returns 0 before MMIO or polling, so hw_random uses any existing buffered bytes or returns EAGAIN. Blocking reads and the entropy feeder keep the original hardware command sequence. tests/test-trng.py executes the actual driver code with injected MMIO failures, covers all three nonblocking states plus alarms/timeouts/recovery/reseeding/partial reads/cleanup, and is wired into CI. The nonblocking test demonstrably fails on the original commit. Documentation now states the behavior explicitly.

@dormancygrace dormancygrace left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final review of 264dbd7: the nonblocking-read issue is fixed; no remaining blocking findings.

Reviewed the MMIO command sequence, alarm clearing before zeroization, timeout/error propagation, output withholding after alarms, periodic reseeding, devres teardown ordering (hwrng unregister before clocks are disabled), DT clock references, build installation and autoload blacklist. The hardware command sequence for blocking reads is unchanged by the review fix.

Validation: tests/test-trng.py executes the production command/read functions with a fault-injecting MMIO model. Covers seeded/unseeded/reseed-due nonblocking calls without hardware access, seed order, periodic reseeding, alarm and timeout recovery, partial output on later errors, failed initialization and cleanup. Negative control fails on the original implementation. Also passed platform-build, package-versions, platform-config, startup-aes-policy and git diff --check. Existing autostart test passed 30 local repetitions.

CI for this final head: Init scripts and tooling (including the new test), Go server, Web UI, RustDesk, C/C++ analysis, Python analysis and Actions analysis passed. The earlier Go failure did not recur. Full firmware build/hash, pion-ice and Go analysis are still running; not waiting for the full run under the existing instruction. CodeQL aggregate currently reports neutral/missing configurations, so this is not a claim that every CI check is green. Original-head full firmware build passed.

No new device testing or deployment in this review. The author's PCIe tests remain the hardware evidence; MMIO tests do not establish entropy quality or other-board behavior. Module remains explicitly loaded only. Ready to merge.

@dormancygrace
dormancygrace merged commit c2d146f into main Oct 21, 2026
15 checks passed
@dormancygrace
dormancygrace deleted the kernel/hw-trng branch October 21, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant