From d46a4b878c2b61a2bee3c7c350213806069ab909 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Thu, 26 Sep 2024 12:26:54 +0200 Subject: [PATCH 1/4] Assume getrandom on linux --- src/crystal/system/unix/getrandom.cr | 136 +++++------------- src/crystal/system/unix/urandom.cr | 2 - src/lib_c/aarch64-android/c/sys/random.cr | 7 + src/lib_c/aarch64-linux-gnu/c/sys/random.cr | 5 + src/lib_c/aarch64-linux-musl/c/sys/random.cr | 5 + src/lib_c/arm-linux-gnueabihf/c/sys/random.cr | 5 + src/lib_c/i386-linux-gnu/c/sys/random.cr | 5 + src/lib_c/i386-linux-musl/c/sys/random.cr | 5 + src/lib_c/x86_64-linux-gnu/c/sys/random.cr | 5 + src/lib_c/x86_64-linux-musl/c/sys/random.cr | 5 + src/random/secure.cr | 2 +- 11 files changed, 77 insertions(+), 105 deletions(-) create mode 100644 src/lib_c/aarch64-android/c/sys/random.cr create mode 100644 src/lib_c/aarch64-linux-gnu/c/sys/random.cr create mode 100644 src/lib_c/aarch64-linux-musl/c/sys/random.cr create mode 100644 src/lib_c/arm-linux-gnueabihf/c/sys/random.cr create mode 100644 src/lib_c/i386-linux-gnu/c/sys/random.cr create mode 100644 src/lib_c/i386-linux-musl/c/sys/random.cr create mode 100644 src/lib_c/x86_64-linux-gnu/c/sys/random.cr create mode 100644 src/lib_c/x86_64-linux-musl/c/sys/random.cr diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 229716a3d846..9c4d0e92c9ef 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -1,116 +1,48 @@ -{% 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 - LibC.getrandom(buf, buflen, flags) +require "c/sys/random" + +{% if !LibC.has_method?(:getrandom) %} + # getrandom isn't available in android before API LEVEL 28 + require "./urandom" +{% else %} + module Crystal::System::Random + # Reads n random bytes using the Linux `getrandom(2)` syscall. + def self.random_bytes(buffer : Bytes) : Nil + getrandom(buffer) end - end -{% end %} - -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!" + def self.next_u : UInt8 + buffer = uninitialized UInt8 + getrandom(pointerof(buffer).to_slice(1)) + buffer end - 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 - end + # Reads n random bytes using the Linux `getrandom(2)` syscall. + 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 - # Reads n random bytes using the Linux `getrandom(2)` syscall. - private def self.getrandom(buf) - # getrandom(2) may only read up to 256 bytes at once without being - # interrupted or returning early - chunk_size = 256 + while buffer.size > 0 + if buffer.size < chunk_size + chunk_size = buffer.size + end - while buf.size > 0 - if buf.size < chunk_size - chunk_size = buf.size - end + 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.to_unsafe, LibC::SizeT.new(buffer.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 + + buffer += read_bytes end end 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 From 0377321e12caf13fa8648942d401ac063ec70049 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Tue, 8 Oct 2024 10:16:15 +0200 Subject: [PATCH 2/4] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/crystal/system/unix/getrandom.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 9c4d0e92c9ef..13053032d7f6 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -32,7 +32,7 @@ require "c/sys/random" loop do # pass GRND_NONBLOCK flag so that it fails with EAGAIN if the requested # entropy was not available - read_bytes = LibC.getrandom(buffer.to_unsafe, LibC::SizeT.new(buffer.size), LibC::GRND_NONBLOCK) + read_bytes = LibC.getrandom(buffer, buffer.size, LibC::GRND_NONBLOCK) break unless read_bytes == -1 err = Errno.value From 337a6d23e3e4568662d9dc01338de7dc67ce7985 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Tue, 8 Oct 2024 10:26:48 +0200 Subject: [PATCH 3/4] Fix: call getrandom in chunks of 256 bytes maximum --- src/crystal/system/unix/getrandom.cr | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 13053032d7f6..0109fc773fc7 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -23,16 +23,12 @@ require "c/sys/random" chunk_size = 256 while buffer.size > 0 - if buffer.size < chunk_size - chunk_size = buffer.size - end - read_bytes = 0 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, LibC::GRND_NONBLOCK) + read_bytes = LibC.getrandom(buffer, buffer.size.clamp(..chunk_size), LibC::GRND_NONBLOCK) break unless read_bytes == -1 err = Errno.value From 6744811ada570d0b9d3a1dc595e1fd944dbb6804 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Tue, 8 Oct 2024 17:24:47 +0200 Subject: [PATCH 4/4] Fix: avoid wrapping whole getrandom file in macro --- src/crystal/system/random.cr | 7 +++- src/crystal/system/unix/getrandom.cr | 61 +++++++++++++--------------- 2 files changed, 34 insertions(+), 34 deletions(-) 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 0109fc773fc7..6ad217c7cbf2 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -1,44 +1,39 @@ require "c/sys/random" -{% if !LibC.has_method?(:getrandom) %} - # getrandom isn't available in android before API LEVEL 28 - require "./urandom" -{% else %} - module Crystal::System::Random - # Reads n random bytes using the Linux `getrandom(2)` syscall. - def self.random_bytes(buffer : Bytes) : Nil - getrandom(buffer) - end - - def self.next_u : UInt8 - buffer = uninitialized UInt8 - getrandom(pointerof(buffer).to_slice(1)) - buffer - end +module Crystal::System::Random + # Reads n random bytes using the Linux `getrandom(2)` syscall. + def self.random_bytes(buffer : Bytes) : Nil + getrandom(buffer) + end - # Reads n random bytes using the Linux `getrandom(2)` syscall. - 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 + def self.next_u : UInt8 + buffer = uninitialized UInt8 + getrandom(pointerof(buffer).to_slice(1)) + buffer + end - while buffer.size > 0 - read_bytes = 0 + # Reads n random bytes using the Linux `getrandom(2)` syscall. + 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 - 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 + while buffer.size > 0 + read_bytes = 0 - err = Errno.value - raise RuntimeError.from_os_error("getrandom", err) unless err.in?(Errno::EINTR, Errno::EAGAIN) + 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 - ::Fiber.yield - end + err = Errno.value + raise RuntimeError.from_os_error("getrandom", err) unless err.in?(Errno::EINTR, Errno::EAGAIN) - buffer += read_bytes + ::Fiber.yield end + + buffer += read_bytes end end -{% end %} +end