char random: fix boot id uniqueness race (v2)
PATCH 1/1 char random: fix boot id uniqueness race (v2)
The proc file /proc/sys/kernel/random/boot_id can be read concurrently by user-space processes. If two (or more) user-space processes concurrently read boot_id when sysctl_bootid is not yet assigned, a race can occur making boot_id differ between the reads. Because the whole point of the boot id is to be unique across a kernel execution, fix this by protecting this operation with a mutex, and introduce a boot_id_generated flag, along with appropriate memory barriers, to let the fast-path know if the boot ID has been generated without having to hold the mutex.
I propose this approach rather than setting it up within an initcall(), because letting execution randomness add to entropy before populating the boot id seems to be a wanted property. Also, populating it lazily rather than at boot time only makes the performance hit be taken when boot_id is being read.
Q: Why are these memory barriers required ? Aren't the mutexes already
dealing with ordering ?
The need for memory barriers is a consequence of letting the fast-path run without holding this mutex.
Here is the race dealt with by the smp_rmb()/smp_wmb(). I'm showing the result of reversed write order here:
CPU A CPU B
Load boot_id_generated
(test -> false)mutex_lock(&boot_id_mutex)
(implied memory barrier
with acquire semantic)
Load boot_id_generated again
(test -> false)boot_id_generated = 1
(both the compiler and
CPU are free to reorder
the boot_id_generated
store before uuid stores)
Load boot_id_generated
(test -> true)
Load uuid content
(races with generate_random_uuid:
result either 0 or corrupted)
Return corrupted uuid.generate_random_uuid(uuid)mutex_unlock(&boot_id_mutex)
I prefer not requiring the fast-path to take a mutex, because this would transform a read-mostly operation into an operation that requires cache-line exchanges (the mutex). However, if we want the fast-path to be mutex-free, we need to enforce order with memory barriers: smp_rmb on the read-side, smp_wmb on the update-side. Failure to do so leads to the race shown above, where a corrupted boot_id can be returned.
* Changelog since v1:
- boot_id_mutex is now declared within the proc_do_uuid scope.
- added explanation for memory barriers.
--- drivers/char/random.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-)
drivers/char/random.c +17 -3
@@ -1244,16 +1244,30 @@ static char sysctl_bootid[16];static int proc_do_uuid (ctl_table *table,int write,void __user *buffer,size_t *lenp,loff_t *ppos) { +static int boot_id_generated; +static DEFINE_MUTEX (boot_id_mutex);ctl_table fake_table;unsigned char buf[64 ], tmp_uuid[16 ], *uuid; uuid = table->data;if (!uuid) { uuid = tmp_uuid; - uuid[8 ] =0 ; - } -if (uuid[8 ] ==0 )generate_random_uuid (uuid); + }else { +if (unlikely (!ACCESS_ONCE (boot_id_generated))) { +mutex_lock (&boot_id_mutex); +if (!boot_id_generated) { +generate_random_uuid (uuid); +/* Store uuid before boot_id_generated. */ +smp_wmb (); + boot_id_generated =1 ; + } +mutex_unlock (&boot_id_mutex); + }else { +/* Load boot_id_generated before uuid */ +smp_rmb (); + } + }sprintf (buf,"%pU" , uuid);
Signature · 4 lines
Mathieu Desnoyers Operating System Efficiency R&D Consultant EfficiOS Inc. http://www.efficios.com
Quoted 99 lines · Mathieu Desnoyers
The proc file
/proc/sys/kernel/random/boot_idcan be read concurrently by user-space processes. If two (or more) user-space processes concurrently readboot_idwhensysctl_bootidis not yet assigned, a race can occur makingboot_iddiffer between the reads. Because the whole point of the boot id is to be unique across a kernel execution, fix this by protecting this operation with a mutex, and introduce aboot_id_generatedflag, along with appropriate memory barriers, to let the fast-path know if the boot ID has been generated without having to hold the mutex.I propose this approach rather than setting it up within an
initcall(), because letting execution randomness add to entropy before populating the boot id seems to be a wanted property. Also, populating it lazily rather than at boot time only makes the performance hit be taken whenboot_idis being read.Q: Why are these memory barriers required ? Aren't the mutexes already
dealing with ordering ?The need for memory barriers is a consequence of letting the fast-path run without holding this mutex.
Here is the race dealt with by the smp_rmb()/smp_wmb(). I'm showing the result of reversed write order here:
CPU A CPU B
Load
boot_id_generated
(test -> false)mutex_lock(&boot_id_mutex)
(implied memory barrier
with acquire semantic)
Loadboot_id_generatedagain
(test -> false)boot_id_generated= 1
(both the compiler and
CPU are free to reorder
theboot_id_generated
store before uuid stores)
Loadboot_id_generated
(test -> true)
Load uuid content
(races withgenerate_random_uuid:
result either 0 or corrupted)
Return corrupted uuid.generate_random_uuid(uuid)mutex_unlock(&boot_id_mutex)I prefer not requiring the fast-path to take a mutex, because this would transform a read-mostly operation into an operation that requires cache-line exchanges (the mutex). However, if we want the fast-path to be mutex-free, we need to enforce order with memory barriers:
smp_rmbon the read-side,smp_wmbon the update-side. Failure to do so leads to the race shown above, where a corruptedboot_idcan be returned.* Changelog since v1:
-boot_id_mutexis now declared within theproc_do_uuidscope.
- added explanation for memory barriers.Signed-off-by Mathieu Desnoyers <[email protected]>CC "Theodore Ts'o" <[email protected]>CC Matt Mackall <[email protected]>CC Greg Kroah-Hartman <[email protected]>--- drivers/char/random.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-)Index:
linux-2.6-lttng/drivers/char/random.c
===================================================================drivers/char/random.c +10
-3@@ -1244,16 +1244,30 @@ static char sysctl_bootid[16];static int proc_do_uuid (ctl_table *table,int write,void __user *buffer,size_t *lenp,loff_t *ppos) { +static int boot_id_generated; +static DEFINE_MUTEX (boot_id_mutex);ctl_table fake_table;unsigned char buf[64 ], tmp_uuid[16 ], *uuid; uuid = table->data;if (!uuid) { uuid = tmp_uuid; - uuid[8 ] =0 ; - } -if (uuid[8 ] ==0 )generate_random_uuid (uuid); + }else { +if (unlikely (!ACCESS_ONCE (boot_id_generated))) { +mutex_lock (&boot_id_mutex); +if (!boot_id_generated) { +generate_random_uuid (uuid); +/* Store uuid before boot_id_generated. */ +smp_wmb (); + boot_id_generated =1 ;
drivers/char/random.c static char sysctl_bootid[16]; · line 1273
Quoted 10 lines
drivers/char/random.c +7
-0+ } +mutex_unlock (&boot_id_mutex); + }else { +/* Load boot_id_generated before uuid */ +smp_rmb (); + } + }sprintf (buf,"%pU" , uuid);
This seems overly complex to me.
I doubt this is performance critical path ?
What about a basic patch like :
drivers/char/random.c +7 -3
@@ -1260,11 +1260,15 @@ static int proc_do_uuid(ctl_table *table, int write, uuid = table->data;if (!uuid) { uuid = tmp_uuid; - uuid[8 ] =0 ; - } -if (uuid[8 ] ==0 )generate_random_uuid (uuid); + }else { +static DEFINE_SPINLOCK (bootid_spinlock); +spin_lock (&bootid_spinlock); +if (!uuid[8 ]) +generate_random_uuid (uuid); +spin_unlock (&bootid_spinlock); + }sprintf (buf,"%pU" , uuid); fake_table.data = buf;
Quoted 107 lines · * Eric Dumazet ([email protected]) wrote:
Quoted 106 lines · Mathieu Desnoyers
The proc file
/proc/sys/kernel/random/boot_idcan be read concurrently by user-space processes. If two (or more) user-space processes concurrently readboot_idwhensysctl_bootidis not yet assigned, a race can occur makingboot_iddiffer between the reads. Because the whole point of the boot id is to be unique across a kernel execution, fix this by protecting this operation with a mutex, and introduce aboot_id_generatedflag, along with appropriate memory barriers, to let the fast-path know if the boot ID has been generated without having to hold the mutex.I propose this approach rather than setting it up within an
initcall(), because letting execution randomness add to entropy before populating the boot id seems to be a wanted property. Also, populating it lazily rather than at boot time only makes the performance hit be taken whenboot_idis being read.Q: Why are these memory barriers required ? Aren't the mutexes already
dealing with ordering ?The need for memory barriers is a consequence of letting the fast-path run without holding this mutex.
Here is the race dealt with by the smp_rmb()/smp_wmb(). I'm showing the result of reversed write order here:
CPU A CPU B
Load
boot_id_generated
(test -> false)mutex_lock(&boot_id_mutex)
(implied memory barrier
with acquire semantic)
Loadboot_id_generatedagain
(test -> false)boot_id_generated= 1
(both the compiler and
CPU are free to reorder
theboot_id_generated
store before uuid stores)
Loadboot_id_generated
(test -> true)
Load uuid content
(races withgenerate_random_uuid:
result either 0 or corrupted)
Return corrupted uuid.generate_random_uuid(uuid)mutex_unlock(&boot_id_mutex)I prefer not requiring the fast-path to take a mutex, because this would transform a read-mostly operation into an operation that requires cache-line exchanges (the mutex). However, if we want the fast-path to be mutex-free, we need to enforce order with memory barriers:
smp_rmbon the read-side,smp_wmbon the update-side. Failure to do so leads to the race shown above, where a corruptedboot_idcan be returned.* Changelog since v1:
-boot_id_mutexis now declared within theproc_do_uuidscope.
- added explanation for memory barriers.Signed-off-by Mathieu Desnoyers <[email protected]>CC "Theodore Ts'o" <[email protected]>CC Matt Mackall <[email protected]>CC Greg Kroah-Hartman <[email protected]>--- drivers/char/random.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-)Index:
linux-2.6-lttng/drivers/char/random.c
===================================================================drivers/char/random.c +17
-3@@ -1244,16 +1244,30 @@ static char sysctl_bootid[16];static int proc_do_uuid (ctl_table *table,int write,void __user *buffer,size_t *lenp,loff_t *ppos) { +static int boot_id_generated; +static DEFINE_MUTEX (boot_id_mutex);ctl_table fake_table;unsigned char buf[64 ], tmp_uuid[16 ], *uuid; uuid = table->data;if (!uuid) { uuid = tmp_uuid; - uuid[8 ] =0 ; - } -if (uuid[8 ] ==0 )generate_random_uuid (uuid); + }else { +if (unlikely (!ACCESS_ONCE (boot_id_generated))) { +mutex_lock (&boot_id_mutex); +if (!boot_id_generated) { +generate_random_uuid (uuid); +/* Store uuid before boot_id_generated. */ +smp_wmb (); + boot_id_generated =1 ; + } +mutex_unlock (&boot_id_mutex); + }else { +/* Load boot_id_generated before uuid */ +smp_rmb (); + } + }
Quoted 7 lines
drivers/char/random.c static int proc_do_uuid(ctl_table *table, int write, · line 1273
Quoted 3 lines
drivers/char/random.c +0
-0sprintf (buf,"%pU" , uuid);This seems overly complex to me.
I doubt this is performance critical path ?
Fair point, I don't see many use-cases where an application would like to get the boot_id value (which stays constant over an entire kernel execution) very frequently, unlike /proc/sys/kernel/random/uuid.
Quoted 12 lines
What about a basic patch like :
drivers/char/random.c +0
-1@@ -1260,11 +1260,15 @@ static int proc_do_uuid(ctl_table *table, int write, uuid = table->data;if (!uuid) { uuid = tmp_uuid; - uuid[8 ] =0 ;
drivers/char/random.c static int proc_do_uuid(ctl_table *table, int write, · line 1270
Quoted 10 lines
drivers/char/random.c +6
-2- } -if (uuid[8 ] ==0 )generate_random_uuid (uuid); + }else { +static DEFINE_SPINLOCK (bootid_spinlock); +spin_lock (&bootid_spinlock); +if (!uuid[8 ]) +generate_random_uuid (uuid); +spin_unlock (&bootid_spinlock);
That would make sense, as long as we're OK about turning a read-mostly operation into a fully serialized operation that requires to exchange the lock between processor cache-lines. But as you point out, it should be fairly unfrequently used.
Any particular reason to use a spin lock rather than a mutex ? I did put a mutex in my implementation assuming that it would be a little more RT-friendly.
Thanks,
Mathieu
Quoted 7 lines
drivers/char/random.c +1
-0+ }sprintf (buf,"%pU" , uuid); fake_table.data = buf;
Signature · 4 lines
Mathieu Desnoyers Operating System Efficiency R&D Consultant EfficiOS Inc. http://www.efficios.com
Quoted 8 lines · Mathieu Desnoyers
That would make sense, as long as we're OK about turning a read-mostly operation into a fully serialized operation that requires to exchange the lock between processor cache-lines. But as you point out, it should be fairly unfrequently used.
Any particular reason to use a spin lock rather than a mutex ? I did put a mutex in my implementation assuming that it would be a little more RT-friendly.
generate_random_uuid(uuid) is very fast and doesnt sleep.