diff --git a/src/crystal/system/random.cr b/src/crystal/system/random.cr index 1a5b3c8f4677..ccf9d6dfa344 100644 --- a/src/crystal/system/random.cr +++ b/src/crystal/system/random.cr @@ -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) %} diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index e759ff0406e6..6ad217c7cbf2 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -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 diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr index 7ac025f43e6b..fe81129a8ade 100644 --- a/src/crystal/system/unix/urandom.cr +++ b/src/crystal/system/unix/urandom.cr @@ -1,5 +1,3 @@ -{% skip_file unless flag?(:unix) && !flag?(:netbsd) && !flag?(:openbsd) && !flag?(:linux) %} - module Crystal::System::Random @@initialized = false @@urandom : ::File? diff --git a/src/lib_c/aarch64-android/c/sys/random.cr b/src/lib_c/aarch64-android/c/sys/random.cr new file mode 100644 index 000000000000..77e193958ff2 --- /dev/null +++ b/src/lib_c/aarch64-android/c/sys/random.cr @@ -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 diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/random.cr b/src/lib_c/aarch64-linux-gnu/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/aarch64-linux-musl/c/sys/random.cr b/src/lib_c/aarch64-linux-musl/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/random.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/i386-linux-gnu/c/sys/random.cr b/src/lib_c/i386-linux-gnu/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/i386-linux-musl/c/sys/random.cr b/src/lib_c/i386-linux-musl/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/random.cr b/src/lib_c/x86_64-linux-gnu/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/lib_c/x86_64-linux-musl/c/sys/random.cr b/src/lib_c/x86_64-linux-musl/c/sys/random.cr new file mode 100644 index 000000000000..2c74de96abfb --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/sys/random.cr @@ -0,0 +1,5 @@ +lib LibC + GRND_NONBLOCK = 1_u32 + + fun getrandom(buf : Void*, buflen : SizeT, flags : UInt32) : SSizeT +end diff --git a/src/random/secure.cr b/src/random/secure.cr index 1722b5e6e884..a6b9df03063f 100644 --- a/src/random/secure.cr +++ b/src/random/secure.cr @@ -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