Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/kconfig/core
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ config core_syscall
bool
default y

config core_reseed_on_resume
prompt "Reseed the CSPRNG on hypervisor resume"
bool
default y

config core_epoll
prompt "Include epoll"
bool
Expand Down
46 changes: 46 additions & 0 deletions drivers/kvmclock.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
#include <osv/sched.hh>
#include <mutex>
#include <atomic>
#include <osv/kernel_config_core_reseed_on_resume.h>
#if CONF_core_reseed_on_resume
#include "drivers/random.hh"
#endif

using namespace osv::clock;

Expand Down Expand Up @@ -65,9 +69,51 @@ kvmclock::kvmclock()
//
// Start a thread that will synchronize the wall clock with the host
auto t = sched::thread::make([this] {
#if CONF_core_reseed_on_resume
// Track the guest system_time across each 1 second sleep. When the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After reading the full patch and realizing you have a second detection mechanism in the /dev/random implementation in random.cc, that can detect the event immediately instead of after 1 second, I wonder - perhaps this code, in kvmclock.cc, isn't needed at all and can be removed?

If you didn't have the code in random.cc, maybe this code might be needed, but still you did do that change in random.cc, why do this change too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question, and I looked hard at removing it. I decided to keep it, because random.cc alone leaves a real gap: it only detects a resume when something reads the /dev/random device (random_read()). But the CSPRNG also has in-kernel consumers that never go through that path - arc4random()/read_random() in the BSD stack, used for TCP initial sequence numbers (bsd/sys/netinet/tcp_subr.cc:1551), PCB/syncache, etc. A guest that is snapshotted while idle on /dev/random and then, after resume, only does network activity would generate ISNs from the still-cloned CSPRNG and never trigger the read-path detector.

The kvmclock 1Hz thread closes that gap: it fires within ~1.5s of resume regardless of whether anyone reads /dev/random, so those in-kernel consumers get divergence too. So the two are complementary: kvmclock guarantees an upper bound on time-to-reseed for the no-read case; random.cc makes it immediate on the read path. I added a comment here explaining that split (and cross-referencing the random.cc detector, per your other note).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Interesting, but doesn't mean that those "in-kernel readers" also need exactly the same logic - if an in-kernel reader reads more than 0.5 second (or 1.5 second) after the previous read, we need the reseed? Maybe the "read path detector" needs to be in a lower-level read function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good push - it made me look more carefully, and the honest answer is that a lower-level read detector would not actually help the in-kernel consumers, for a reason specific to how OSv wires up its RNGs.

There are two independent RNG subsystems here, not one:

  1. The /dev/random device CSPRNG (the Yarrow/soft random_adaptor in drivers/random.cc + bsd/sys/dev/random/). This is the one reseed_on_resume() re-keys, and its only read entry point is random_read() - so the read-path detector already sits at the lowest-level read function for this pool.

  2. The in-kernel arc4random()/read_random() path used for TCP ISNs (bsd/sys/netinet/tcp_subr.cc:1551), PCB/syncache, etc. On OSv this does NOT draw from the Yarrow CSPRNG at all: read_random() links to the stub in bsd/sys/libkern/arc4random.c:38 (a getmicrotime()-based FIXME), and arc4random() runs its own arc4 S-box seeded from that stub. The #ifndef __OSV__ hook that would let the device CSPRNG "do arc4random a favour" (randomdev_soft.cc:228) is compiled out.

So putting the resume-check inside read_random()/arc4random() would not reseed the pool that reseed_on_resume() re-keys - it would reseed the wrong (and much weaker) thing, and it would put a per-call timestamp compare on the ISN hot path (arc4random is called per connection). Neither is what we want.

That is exactly why I kept the kvmclock 1Hz detector: it is the one mechanism that fires on a wall-clock discontinuity regardless of which RNG a consumer uses or whether anything reads at all, so it covers the network-only-after-resume case without instrumenting every low-level RNG call. The read-path detector in random_read() is then just the fast-path optimization for the device CSPRNG, and it is already at the lowest-level read for that pool.

(Correcting my earlier reply: I said the in-kernel readers use "the still-cloned CSPRNG" - more precisely they use a separate arc4 RNG that the device reseed does not touch, which strengthens rather than weakens the case for keeping the clock-jump detector. Properly reseeding that arc4 path on resume, or better, unifying it onto the device CSPRNG, is a worthwhile follow-up but orthogonal to this patch.)

// guest is paused and later resumed from a hypervisor snapshot, the
// system clock advances by far more than the second we slept. That
// large forward discontinuity is our resume signal: on resume we
// force the CSPRNG to re-key so that two guests restored from the
// same snapshot do not keep emitting identical OS random output.
// A normal (never-resumed) guest only ever sees ~1 second here and
// so never triggers the reseed, leaving its behavior unchanged.
//
// This 1Hz thread is the PROACTIVE resume detector: it fires within
// ~1.5s of a resume even if nothing ever reads /dev/random, which is
// what covers in-kernel CSPRNG consumers (arc4random()/read_random()
// for TCP initial sequence numbers, etc.) that never touch the
// /dev/random read path. A second, low-latency detector on that read
// path (reseed_if_resumed() in drivers/random.cc) closes the up-to-
// ~1.5s window for a program that reads /dev/random immediately after
// resume, before this thread's next tick.
//
// Both detectors are heuristics based on a clock discontinuity. The
// robust mechanism used by Linux/Firecracker is the VMGenID device
// (an ACPI-exposed generation counter the hypervisor bumps on clone),
// which detects a clone with no timing guess and covers sub-second
// clones this heuristic could miss. Adding a VMGenID driver is a
// worthwhile future enhancement; until then this clock-jump heuristic
// is a pragmatic first step that is much better than never reseeding.
u64 prev_system_time = this->system_time();
#endif
while (true) {
sched::thread::sleep(std::chrono::seconds(1));
#if CONF_core_reseed_on_resume
u64 now_system_time = this->system_time();
// We slept 1 second. Allow slack for scheduling delay, but treat
// a jump well beyond that (more than 1.5 seconds) as a hypervisor
// resume: the guest was paused across the snapshot and the system
// clock kept advancing while it was suspended.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we worried that if a clone operation takes less than 1.5 seconds, we won't notice it?
Conversely, are we worried that during roughly 1 second we are still using the same cloned seed?

I was curious how Linux handles it, and apparently it uses something called VMGenID (https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md#reusing-snapshotted-states-securely) to figure out the VM changed and a reseeding is necessary.

In any case, I think your patch is better than nothing, I'm not sure what is the best approach.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, continuing to read the code. I see that below you solved that "during roughly 1 second" problem by having yet another place where a resume is detected. Maybe you should mention that second place in the comment here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks - VMGenID is indeed the proper mechanism, and I have noted it in the code as the right future enhancement. Doing it well means an ACPI-exposed VMGenID device driver (the guest reads a generation counter the hypervisor bumps on clone), which is a larger, separate piece of work; this clock-jump heuristic is the pragmatic first step that is, as you said, better than nothing.

On the two timing worries: a clone shorter than 1.5s could indeed slip past the kvmclock detector, but the read-path detector in random.cc catches the first read after resume regardless of how brief the pause was (see the follow-up below). And the "~1 second still using the cloned seed" window is exactly what the random.cc detector closes for /dev/random consumers. I updated this comment to say all of that (VMGenID as the ideal, plus a pointer to the second detector).

if (now_system_time > prev_system_time &&
(now_system_time - prev_system_time) > 1500000000ULL) {
randomdev::reseed_on_resume();
}
#endif
this->sync_wall_clock();
#if CONF_core_reseed_on_resume
prev_system_time = this->system_time();
#endif
}
}, sched::thread::attr().name("kvm_wall_clock_sync"));
t->start();
Expand Down
128 changes: 128 additions & 0 deletions drivers/random.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,26 @@
#include <osv/device.h>
#include <osv/uio.h>
#include <osv/debug.hh>
#include <osv/clock.hh>
#include <atomic>
#include <osv/kernel_config_core_reseed_on_resume.h>

#include <dev/random/randomdev.h>
#include <dev/random/randomdev_soft.h>
#include <dev/random/random_adaptors.h>
#include <dev/random/random_harvestq.h>
#include <dev/random/live_entropy_sources.h>

#ifdef __x86_64__
#include "processor.hh"
#endif

namespace randomdev {

#if CONF_core_reseed_on_resume
void reseed_on_resume();
#endif

struct random_device_priv {
random_device* drv;
};
Expand All @@ -56,12 +68,60 @@ static random_device_priv *to_priv(device *dev)
return reinterpret_cast<random_device_priv*>(dev->private_data);
}

#if CONF_core_reseed_on_resume
// Low-latency, read-path half of the hypervisor-resume CSPRNG reseed described
// at reseed_on_resume() below. The problem it solves: a full-VM snapshot
// captures the entropy pool and CSPRNG state, so two guests restored from the
// same snapshot would replay an identical OS random stream (duplicate session
// keys, TCP sequence numbers, UUIDs) until something forces a re-key.
//
// Resume is detected in two independent places:
// 1. drivers/kvmclock.cc, in the 1Hz "kvm_wall_clock_sync" thread: a proactive
// detector that fires within ~1.5s of resume even if nothing ever reads
// /dev/random. This is the one that covers in-kernel CSPRNG consumers
// (arc4random()/read_random() for TCP ISNs etc.) which never enter this
// read path.
// 2. here, in random_read(): a reactive detector that fires immediately on
// the first /dev/random or getrandom() read after resume, closing the up
// to ~1.5s window in which detector 1 has not fired yet.
//
// We compare the monotonic uptime clock against the value seen at the previous
// read. Between two back-to-back reads uptime advances by microseconds; a jump
// far larger than any plausible gap between reads (>1.5s, matching the kvmclock
// detector's threshold: this code exists to detect the same event sooner, not
// at a lower threshold) means the guest was paused across a snapshot and
// resumed, so we re-key before serving. Reseeding is skipped on the hot read
// path (back-to-back reads never exceed the threshold) and, in the rare case it
// does fire after a long idle with no resume, an extra re-key is harmless.
static std::atomic<u64> _last_read_uptime{0};
// Set true once randomdev_init() has brought the device (and the harvest ring)
// up, so reseed_on_resume() is a true no-op if a resume is somehow detected
// before that (e.g. with --norandom).
static std::atomic<bool> _reseed_ready{false};

static void reseed_if_resumed()
{
u64 now = (u64)::clock::get()->uptime();
u64 prev = _last_read_uptime.exchange(now, std::memory_order_relaxed);
// Skip the very first read (prev == 0) and only act on a large forward jump.
// 1.5s matches the kvmclock detector; the purpose of this read-path check is
// faster detection of the same resume event, not a lower threshold.
if (prev != 0 && now > prev && (now - prev) > 1500000000ULL) {
reseed_on_resume();
}
}
#endif

static int
random_read(struct device *dev, struct uio *uio, int ioflags)
{
int c, error = 0;
char random_buf[PAGE_SIZE];

#if CONF_core_reseed_on_resume
reseed_if_resumed();
#endif

// Blocking logic
if (!random_adaptor->seeded) {
error = (*random_adaptor->block)(ioflags);
Expand Down Expand Up @@ -206,6 +266,74 @@ void randomdev_init()
{
new random_device();
debugf("random: <%s> initialized\n", random_adaptor->ident);
#if CONF_core_reseed_on_resume
_reseed_ready.store(true, std::memory_order_release);
#endif
}

#if CONF_core_reseed_on_resume
// Force the CSPRNG to re-key after a hypervisor resume so that two guests
// restored from the SAME snapshot do not keep producing identical OS random
// output. A full-VM snapshot captures the entire entropy pool and CSPRNG state,
// so without an explicit reseed every clone would replay the exact same random
// stream. This is a real correctness and security problem for a cloned fleet
// (duplicated session keys, TCP sequence numbers, UUIDs, and so on).
//
// We mix in values that are guaranteed to differ between two clones even when
// there is no live hardware entropy source (no RDRAND, no virtio-rng): the
// wall-clock time the hypervisor handed us on resume differs per clone because
// each clone is resumed at a distinct host wall-clock instant, and the TSC on
// resume differs as well. We feed that unique material into the harvest queue
// as DIVERGENCE material only (bits == 0), so it is hashed into the pool to
// force the two clones apart but is NOT credited as entropy - it must never be
// able to advance Yarrow's counters or unblock /dev/random on its own, since it
// is predictable low-quality data, not real entropy. RDRAND/RDSEED and any
// virtio-rng source continue to feed the pool as before. After mixing we
// command an explicit reseed so the re-key takes effect immediately rather than
// only after the next periodic harvest round.
//
// This runs ONLY when a resume has actually been detected (see the two callers:
// the 1Hz "kvm_wall_clock_sync" thread in drivers/kvmclock.cc, and
// reseed_if_resumed() on the /dev/random read path above), so a normally-
// running or freshly-booted guest that is never resumed follows exactly the
// same code path as before.
void reseed_on_resume()
{
// No-op until the random device has actually been initialized. random_adaptor
// is always non-null (it points at the static soft CSPRNG context), so that
// alone is not enough: with --norandom, or if a resume were somehow detected
// before randomdev_init() ran, the harvest ring would not exist yet and
// random_harvestq_internal() would dereference it. _reseed_ready is set true
// only at the end of randomdev_init(), after random_harvestq_init().
if (!random_adaptor || !_reseed_ready.load(std::memory_order_acquire)) {
return;
}
Comment on lines +300 to +310

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point - I will tighten the guard to also confirm the adaptor has been initialized (not just that random_adaptor is non-null) so reseed_on_resume() is a true no-op if called before the device is up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tightened. Since random_adaptor is always non-null (it points at the static soft CSPRNG context), that check alone was insufficient. reseed_on_resume() now also gates on a new _reseed_ready flag that is set true only at the end of randomdev_init(), after random_harvestq_init() has created the harvest ring. So with --norandom, or if a resume were somehow detected before the device is up, it is a true no-op instead of dereferencing an uninitialized ring.


// Per-resume unique material. Each field differs between two clones that
// were resumed from the same snapshot at different host wall-clock instants.
struct {
u64 wall_ns;
u64 tsc;
u64 uptime_ns;
} seed;
seed.wall_ns = (u64)::clock::get()->time();
seed.uptime_ns = (u64)::clock::get()->uptime();
#ifdef __x86_64__
seed.tsc = processor::rdtsc();
#else
seed.tsc = seed.uptime_ns;
#endif

// Mix the unique material in with a zero entropy-bit credit (divergence, not
// entropy), then force an explicit reseed so the re-key is effective before
// the next read. Any live hardware source (RDRAND / virtio-rng) is drained
// by the reseed itself.
random_harvestq_internal(seed.tsc, &seed, sizeof(seed),
0, RANDOM_PURE_RDRAND);
if (random_adaptor->reseed) {
(random_adaptor->reseed)();
}
}
#endif

}
6 changes: 6 additions & 0 deletions drivers/random.hh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public:

void randomdev_init();

// Re-key the CSPRNG after a hypervisor resume so that guests restored from the
// same full-VM snapshot diverge instead of replaying an identical random
// stream. Safe no-op if the random device is not yet initialized. Only called
// when a resume has actually been detected.
void reseed_on_resume();

}

#endif