Skip to content

Commit

Permalink
Assume getrandom on Linux (#15040)
Browse files Browse the repository at this point in the history
The `getrandom(2)` syscall was added in 2017 and at the time we couldn't expect the glibc 2.25 to be widely available, but we're in 2024 now, and even Ubuntu 18.04 LTS that is now EOL had a compatible glibc release (2.27). I assume musl-libc also added the symbol at the same time.

We can simplify the implementation to assume `getrandom` is available, which avoids the initial check, initialization and fallback to urandom. We still fallback to urandom at compile time when targeting android api level < 28 (we support 24+).

An issue is that executables will now expect glibc 2.25+ (for example), though the interpreter already did.
We also expect kernel 2.6.18 to be compatible, but `getrandom` was added in 3.17 which means it depends on how the libc symbol is implemented —does it fallback to urandom, does it fail?

Related to #15034.

Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
ysbaddaden and straight-shoota authored Oct 10, 2024
1 parent b3a052c commit 8b4fe41
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 103 deletions.
7 changes: 6 additions & 1 deletion src/crystal/system/random.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ end
{% if flag?(:wasi) %}
require "./wasi/random"
{% elsif flag?(:linux) %}
require "./unix/getrandom"
require "c/sys/random"
\{% if LibC.has_method?(:getrandom) %}
require "./unix/getrandom"
\{% else %}
require "./unix/urandom"
\{% end %}
{% elsif flag?(:bsd) || flag?(:darwin) %}
require "./unix/arc4random"
{% elsif flag?(:unix) %}
Expand Down
118 changes: 19 additions & 99 deletions src/crystal/system/unix/getrandom.cr
Original file line number Diff line number Diff line change
@@ -1,119 +1,39 @@
{% skip_file unless flag?(:linux) %}

require "c/unistd"
require "./syscall"

{% if flag?(:interpreted) %}
lib LibC
fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : LibC::SSizeT
end

module Crystal::System::Syscall
GRND_NONBLOCK = 1u32

# TODO: Implement syscall for interpreter
def self.getrandom(buf : UInt8*, buflen : LibC::SizeT, flags : UInt32) : LibC::SSizeT
# the syscall returns the negative of errno directly, the C function
# doesn't, so we mimic the syscall behavior
read_bytes = LibC.getrandom(buf, buflen, flags)
read_bytes >= 0 ? read_bytes : LibC::SSizeT.new(-Errno.value.value)
end
end
{% end %}
require "c/sys/random"

module Crystal::System::Random
@@initialized = false
@@getrandom_available = false
@@urandom : ::File?

private def self.init
@@initialized = true

if has_sys_getrandom
@@getrandom_available = true
else
urandom = ::File.open("/dev/urandom", "r")
return unless urandom.info.type.character_device?

urandom.close_on_exec = true
urandom.read_buffering = false # don't buffer bytes
@@urandom = urandom
end
end

private def self.has_sys_getrandom
sys_getrandom(Bytes.new(16))
true
rescue
false
end

# Reads n random bytes using the Linux `getrandom(2)` syscall.
def self.random_bytes(buf : Bytes) : Nil
init unless @@initialized

if @@getrandom_available
getrandom(buf)
elsif urandom = @@urandom
urandom.read_fully(buf)
else
raise "Failed to access secure source to generate random bytes!"
end
def self.random_bytes(buffer : Bytes) : Nil
getrandom(buffer)
end

def self.next_u : UInt8
init unless @@initialized

if @@getrandom_available
buf = uninitialized UInt8
getrandom(pointerof(buf).to_slice(1))
buf
elsif urandom = @@urandom
urandom.read_byte.not_nil!
else
raise "Failed to access secure source to generate random bytes!"
end
buffer = uninitialized UInt8
getrandom(pointerof(buffer).to_slice(1))
buffer
end

# Reads n random bytes using the Linux `getrandom(2)` syscall.
private def self.getrandom(buf)
private def self.getrandom(buffer)
# getrandom(2) may only read up to 256 bytes at once without being
# interrupted or returning early
chunk_size = 256

while buf.size > 0
if buf.size < chunk_size
chunk_size = buf.size
end
while buffer.size > 0
read_bytes = 0

read_bytes = sys_getrandom(buf[0, chunk_size])
loop do
# pass GRND_NONBLOCK flag so that it fails with EAGAIN if the requested
# entropy was not available
read_bytes = LibC.getrandom(buffer, buffer.size.clamp(..chunk_size), LibC::GRND_NONBLOCK)
break unless read_bytes == -1

buf += read_bytes
end
end
err = Errno.value
raise RuntimeError.from_os_error("getrandom", err) unless err.in?(Errno::EINTR, Errno::EAGAIN)

# Low-level wrapper for the `getrandom(2)` syscall, returns the number of
# bytes read or the errno as a negative number if an error occurred (or the
# syscall isn't available). The GRND_NONBLOCK=1 flag is passed as last argument,
# so that it returns -EAGAIN if the requested entropy was not available.
#
# We use the kernel syscall instead of the `getrandom` C function so any
# binary compiled for Linux will always use getrandom if the kernel is 3.17+
# and silently fallback to read from /dev/urandom if not (so it's more
# portable).
private def self.sys_getrandom(buf : Bytes)
loop do
read_bytes = Syscall.getrandom(buf.to_unsafe, LibC::SizeT.new(buf.size), Syscall::GRND_NONBLOCK)
if read_bytes < 0
err = Errno.new(-read_bytes.to_i)
if err.in?(Errno::EINTR, Errno::EAGAIN)
::Fiber.yield
else
raise RuntimeError.from_os_error("getrandom", err)
end
else
return read_bytes
::Fiber.yield
end

buffer += read_bytes
end
end
end
2 changes: 0 additions & 2 deletions src/crystal/system/unix/urandom.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{% skip_file unless flag?(:unix) && !flag?(:netbsd) && !flag?(:openbsd) && !flag?(:linux) %}

module Crystal::System::Random
@@initialized = false
@@urandom : ::File?
Expand Down
7 changes: 7 additions & 0 deletions src/lib_c/aarch64-android/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
lib LibC
{% if ANDROID_API >= 28 %}
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
{% end %}
end
5 changes: 5 additions & 0 deletions src/lib_c/aarch64-linux-gnu/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/aarch64-linux-musl/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/arm-linux-gnueabihf/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/i386-linux-gnu/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/i386-linux-musl/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/x86_64-linux-gnu/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
5 changes: 5 additions & 0 deletions src/lib_c/x86_64-linux-musl/c/sys/random.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib LibC
GRND_NONBLOCK = 1_u32

fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT
end
2 changes: 1 addition & 1 deletion src/random/secure.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require "crystal/system/random"
# ```
#
# On BSD-based systems and macOS/Darwin, it uses [`arc4random`](https://man.openbsd.org/arc4random),
# on Linux [`getrandom`](http://man7.org/linux/man-pages/man2/getrandom.2.html) (if the kernel supports it),
# on Linux [`getrandom`](http://man7.org/linux/man-pages/man2/getrandom.2.html),
# on Windows [`RtlGenRandom`](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom),
# and falls back to reading from `/dev/urandom` on UNIX systems.
module Random::Secure
Expand Down

0 comments on commit 8b4fe41

Please sign in to comment.