diff mbox series

[RFC,1/4] random: introduce getrandom() GRND_TIMESTAMP

Message ID d890f9d03c2da1eef7bdf47e5b6f3d0b7e1afb3f.1673539719.git.ydroneaud@opteya.com (mailing list archive)
State Not Applicable
Delegated to: Herbert Xu
Headers show
Series random: a simple vDSO mechanism for reseeding userspace CSPRNGs | expand

Commit Message

Yann Droneaud Jan. 12, 2023, 5:02 p.m. UTC
In the pursuit of implementing an userspace arc4random() fast
enough to be used in place of rand(), random(), lrand48(),
mrand48(), etc., and as strong as getrandom(), it was found
that calling getrandom() to generate one uint32_t at a time
is not fast enough (see [1] for example).

As noted by Florian Weimer in [2]:

    "The performance numbers suggest that we benefit from buffering in user
     space.  It might not be necessary to implement expansion in userspace.
     getrandom (or /dev/urandom) with a moderately-sized buffer could be
     sufficient."

Generating multiple values ahead of time with a proper CSPRNG
helps achieve better performances. But buffering in userspace
come with a lot of security related hurdles and pitfalls.

As noted by Jason A. Donenfeld in [3]

    "For example, the kernel reseeds itself when virtual machines fork using
     an identifier passed to the kernel via ACPI. It also reseeds itself on
     system resume, both from ordinary S3 sleep but also, more importantly,
     from hibernation."

Ignoring for now the issue of securily store the buffered
random values in memory to achieve forward secrecy [4], it's
possible to devise a mechanism to help userspace to know
when to discard the values generated/buffered from a previous
call to getrandom() so that the VM and/or resume issue can
be dealt with at userspace level.

Instead of adding a new system call, this patch shoehorns a
query mechanism in getrandom() syscall by adding a mean to
get and test a "timestamp". Currently, the "timestamp" is a
single 64bit integer, that maps to the kernel's base CSPRNG
generation, inverted, so that 0 means unintialized.

GRND_TIMESTAMP allows userspace to ask the kernel if previous
"timestamp" has changed as the result of an event that
triggered kernel CSPRNG reseed, and to update the "timestamp".

In case the "timestamp" hasn't changed, userspace CSPRNG can
consume a slice of its buffered random stream.

If it has changed, remaining userspace buffered random values
should be discarded. Userspace should call getrandom() to fill
and/or generate its buffer with updated seed.
It's advised to test again the "timestamp" to deal with the
race condition, where the kernel reseed just after the call
to getrandom() to get entropy.

How to not use it (because it doesn't have reseed on fork(),
aka. MADV_WIPEONFORK, and forward secrecy buffer protection
aka. mlock(), see [4]):

    static bool expired(void)
    {
            static uint64_t grnd_ts;

            ret = getrandom(&grnd_ts, sizeof(grnd_ts), GRND_TIMESTAMP);
            if (ret < 0)
                    abort(); /* TODO: proper fallback to unbuffered getrandom() */

            return !!ret;
    }

    uint32_t arc4random(void)
    {
            static uint32_t buffer[128]; /* TODO: mlock() buffer memory */
            static unsigned int avail;
            uint32_t val;

            while (expired() || !avail) {
                    getrandom(buffer, sizeof(buffer), 0);
                    avail = 128;
            }

            avail--;
            val = buffer[avail];
            buffer[avail] = 0;

            return val;
    }

As the "timestamp" query has to be made for each uint32_t value
generated by arc4random(), the query should be as lightweight
as possible, thus it's expected GRND_TIMESTAMP to be handled by
at the vDSO level to prevent a system call.

[1] https://lore.kernel.org/all/874jt0kndq.fsf@oldenburg.str.redhat.com/
[2] https://sourceware.org/pipermail/libc-alpha/2022-July/140963.html
[3] https://sourceware.org/pipermail/libc-alpha/2022-July/140939.html
[4] https://lore.kernel.org/all/20230101162910.710293-1-Jason@zx2c4.com/

Link: https://lore.kernel.org/all/cover.1673539719.git.ydroneaud@opteya.com/
Cc: linux-crypto@vger.kernel.org
Cc: Theodore Ts'o <tytso@mit.edu>
Cc: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>
Cc: Florian Weimer <fweimer@redhat.com>
Cc: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Yann Droneaud <ydroneaud@opteya.com>
---
 drivers/char/random.c       | 46 ++++++++++++++++++++++++++++++++++++-
 include/linux/random.h      | 31 +++++++++++++++++++++++++
 include/uapi/linux/random.h |  2 ++
 3 files changed, 78 insertions(+), 1 deletion(-)

Comments

Peter Lafreniere Jan. 13, 2023, 12:32 a.m. UTC | #1
[...]

>GRND_TIMESTAMP allows userspace to ask the kernel if previous
>"timestamp" has changed as the result of an event that
>triggered kernel CSPRNG reseed, and to update the "timestamp".

>In case the "timestamp" hasn't changed, userspace CSPRNG can
>consume a slice of its buffered random stream.

>If it has changed, remaining userspace buffered random values
>should be discarded. Userspace should call getrandom() to fill
>and/or generate its buffer with updated seed.
>It's advised to test again the "timestamp" to deal with the
>race condition, where the kernel reseed just after the call
>to getrandom() to get entropy.

This second check is the 'safe thing' to do, but if you're that
worried about race conditions then this api is useless. You can't
ignore the inherent TOCTOU problem with the GRND_TIMESTAMP calls.

That said, the race condition is so tiny that the added overhead
of a third syscall (without vDSO support) starts to negate the
value in buffering the random numbers (not to mention adding
significant latency to every nth arc4random() call, for example).

IMO, for callers that cannot accept the risk, the getrandom(,,0)
option is the perfect alternative.

[...]

>+static ssize_t get_random_timestamp(char __user *ubuf, size_t len, unsigned int flags)
>+{
>+	u64 ts;
>+
>+	/* other combination not supported */
>+	if (WARN(flags != GRND_TIMESTAMP, "GRND_TIMESTAMP cannot be used with other flags"))
>+		return -EINVAL;

If userspace messes up the flags then it's the problem of the
caller. Why clutter the logs in that case?

At the very least this should be WARN_ONCE() to avoid log spam.

>+	/* shorter structure not supported */
>+	if (len < sizeof(ts))
>+		return -EINVAL;

This should be sizeof(u64) to match the vDSO patch and to avoid
having to change this condition if ts becomes larger.

>+
>+	if (copy_from_user(&ts, ubuf, sizeof(ts)))
>+		return -EFAULT;
>+
>+	/* longer structure supported, only if 0-padded,
>+	   timestamp might be extended in the future with more fields */
>+	if (len > sizeof(ts)) {
>+		char __user *p = ubuf + sizeof(ts);
>+		size_t l = len - sizeof(ts);
>+
>+		while (l) {
>+			char b;
>+
>+			if (get_user(b, p++))
>+				return -EFAULT;
>+
>+			if (b)
>+				return -EINVAL;
>+		}
>+	}
>+
>+	if (!get_random_timestamp_update(&ts, READ_ONCE(base_crng.generation)))
>+		return 0;
>+
>+	if (copy_to_user(ubuf, &ts, sizeof(ts)))
>+		return -EFAULT;
>+
>+	return sizeof(ts);
>+}
>+
> SYSCALL_DEFINE3(getrandom, char __user *, ubuf, size_t, len, unsigned int, flags)
> {
> 	struct iov_iter iter;
> 	struct iovec iov;
> 	int ret;
>
>-	if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE))
>+	if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE | GRND_TIMESTAMP))
> 		return -EINVAL;
>
>+	if (unlikely(flags & GRND_TIMESTAMP))
>+		return get_random_timestamp(ubuf, len, flags);
>+

I'd remove the unlikely(). I don't like to assume usage patterns.

> 	/*
> 	 * Requesting insecure and blocking randomness at the same time makes
> 	 * no sense.
>diff --git a/include/linux/random.h b/include/linux/random.h
>index b0a940af4fff..bc219b5a96a5 100644
>--- a/include/linux/random.h
>+++ b/include/linux/random.h
>@@ -161,4 +161,35 @@ int random_online_cpu(unsigned int cpu);
> extern const struct file_operations random_fops, urandom_fops;
> #endif
>
>+/*
>+ * get_random_timestamp_update()
>+ *
>+ * @generation: current CRNG generation (from base_crng.generation
>+ *              or _vdso_rng_data.generation)
>+ *
>+ * Return: timestamp size if previous timestamp is expired and is updated,
>+ *         0 if not expired (and not updated)
>+ */
>+static inline bool get_random_timestamp_update(u64 *user_ts,
>+					       u64 generation)
>+{
>+	u64 ts;
>+
>+	/* base_crng.generation is never ULONG_MAX,
>+	 * OTOH userspace will initialize its timestamp
>+	 * to 0, so inverting base_crng.generation ensure
>+	 * first call to getrandom(,,GRND_TIMESTAMP) will
>+	 * update
>+	 */

Rather than assuming that the timestamp will start zero-initilized,
expect it to be uninitilized. Either way the code works.

>+	ts = ~generation;
>+
>+	/* not expired ? no refresh suggested */
>+	if (*user_ts == ts)
>+		return false;
>+
>+	*user_ts = ts;
>+
>+	return true;
>+}
>+

Not that it matters much, but you could make generation a u64* that
gets dereferenced by get_random_timestamp_update(). It's cleaner for
the caller, barely changes this function, and will get inlined anyway.
I might just be imposing my personal style in this case though.

After a cursory skimming of the rest of the series I think that this
is a worthwhile direction to pursue. Jason's series is growing bulky
and this provides the needed slimming while solving the root problem.

The only thing I see immediately is the TOCTOU problem and the extra
steps needed to guarantee forward secrecy.

I should mention that I'm not a security or rng expert at all.

Cheers,
Peter Lafreniere
<peter@n8pjl.ca>
diff mbox series

Patch

diff --git a/drivers/char/random.c b/drivers/char/random.c
index ce3ccd172cc8..9e2a37e432c0 100644
--- a/drivers/char/random.c
+++ b/drivers/char/random.c
@@ -1361,15 +1361,59 @@  static void __cold try_to_generate_entropy(void)
  *
  **********************************************************************/
 
+static ssize_t get_random_timestamp(char __user *ubuf, size_t len, unsigned int flags)
+{
+	u64 ts;
+
+	/* other combination not supported */
+	if (WARN(flags != GRND_TIMESTAMP, "GRND_TIMESTAMP cannot be used with other flags"))
+		return -EINVAL;
+
+	/* shorter structure not supported */
+	if (len < sizeof(ts))
+		return -EINVAL;
+
+	if (copy_from_user(&ts, ubuf, sizeof(ts)))
+		return -EFAULT;
+
+	/* longer structure supported, only if 0-padded,
+	   timestamp might be extended in the future with more fields */
+	if (len > sizeof(ts)) {
+		char __user *p = ubuf + sizeof(ts);
+		size_t l = len - sizeof(ts);
+
+		while (l) {
+			char b;
+
+			if (get_user(b, p++))
+				return -EFAULT;
+
+			if (b)
+				return -EINVAL;
+		}
+	}
+
+	if (!get_random_timestamp_update(&ts, READ_ONCE(base_crng.generation)))
+		return 0;
+
+	if (copy_to_user(ubuf, &ts, sizeof(ts)))
+		return -EFAULT;
+
+	return sizeof(ts);
+}
+
 SYSCALL_DEFINE3(getrandom, char __user *, ubuf, size_t, len, unsigned int, flags)
 {
 	struct iov_iter iter;
 	struct iovec iov;
 	int ret;
 
-	if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE))
+	if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE | GRND_TIMESTAMP))
 		return -EINVAL;
 
+	if (unlikely(flags & GRND_TIMESTAMP))
+		return get_random_timestamp(ubuf, len, flags);
+
 	/*
 	 * Requesting insecure and blocking randomness at the same time makes
 	 * no sense.
diff --git a/include/linux/random.h b/include/linux/random.h
index b0a940af4fff..bc219b5a96a5 100644
--- a/include/linux/random.h
+++ b/include/linux/random.h
@@ -161,4 +161,35 @@  int random_online_cpu(unsigned int cpu);
 extern const struct file_operations random_fops, urandom_fops;
 #endif
 
+/*
+ * get_random_timestamp_update()
+ *
+ * @generation: current CRNG generation (from base_crng.generation
+ *              or _vdso_rng_data.generation)
+ *
+ * Return: timestamp size if previous timestamp is expired and is updated,
+ *         0 if not expired (and not updated)
+ */
+static inline bool get_random_timestamp_update(u64 *user_ts,
+					       u64 generation)
+{
+	u64 ts;
+
+	/* base_crng.generation is never ULONG_MAX,
+	 * OTOH userspace will initialize its timestamp
+	 * to 0, so inverting base_crng.generation ensure
+	 * first call to getrandom(,,GRND_TIMESTAMP) will
+	 * update
+	 */
+	ts = ~generation;
+
+	/* not expired ? no refresh suggested */
+	if (*user_ts == ts)
+		return false;
+
+	*user_ts = ts;
+
+	return true;
+}
+
 #endif /* _LINUX_RANDOM_H */
diff --git a/include/uapi/linux/random.h b/include/uapi/linux/random.h
index e744c23582eb..b433fb8d79ac 100644
--- a/include/uapi/linux/random.h
+++ b/include/uapi/linux/random.h
@@ -50,9 +50,11 @@  struct rand_pool_info {
  * GRND_NONBLOCK	Don't block and return EAGAIN instead
  * GRND_RANDOM		No effect
  * GRND_INSECURE	Return non-cryptographic random bytes
+ * GRND_TIMESTAMP	Interpret buffer as an opaque timestamp structure
  */
 #define GRND_NONBLOCK	0x0001
 #define GRND_RANDOM	0x0002
 #define GRND_INSECURE	0x0004
+#define GRND_TIMESTAMP	0x0008
 
 #endif /* _UAPI_LINUX_RANDOM_H */