From d2cad72cb6a02cd1095339cf4f2b28602a2a1437 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Sun, 6 Sep 2026 14:55:37 -0400 Subject: [PATCH 1/3] fix(crypto): seed the IV generator from OS entropy, not the wall clock Deferred security item from the 2026-08 review: encryption IV *unpredictability* was weak. The Mersenne Twister behind __db_generate_iv was seeded from __os_gettime() hashed wall-clock SECONDS, so an attacker who knows roughly when the environment was created can narrow the IV stream. (AES-CBC mode and IV uniqueness were already correct; only unpredictability was at issue.) - New src/os/os_csprng.c: __os_csprng() fills a buffer from the OS CSPRNG -- getrandom(2) where available, else arc4random_buf(3), else /dev/urandom via the os layer. Returns an error rather than silently degrading. - configure.ac: probe getrandom, arc4random_buf, and sys/random.h; db_int.in includes when present. Registered in srcfiles.in, Makefile.in and dist/meson.build (POSIX only; Windows keeps its own path and the historical fallback). - mt19937db.c: seed from __os_csprng(); fall back to the hashed clock ONLY if the OS has no entropy source, so encryption still functions there. Also fixes a latent tooling breakage found while doing this: our SSI work put category-9 message ids (4573/4574) into src/common/db_err.c, which lives in message category 0 (range 1-500). dist/s_message_id therefore computed a next-id of 4575, exceeded category 0's max, and reported RANGE FULL -- blocking any new DB_STR in src/os, src/common, src/crypto, src/hmac or src/fileops. Reassigned those two to 0211/0212 (DB_SNAPSHOT_CONFLICT / DB_SNAPSHOT_UNSAFE strings are unchanged) and gave os_csprng 0213. s_message_id now runs clean. Verified: probes detected (HAVE_GETRANDOM, HAVE_SYS_RANDOM_H, HAVE_ARC4RANDOM_BUF); build clean; a direct check shows two __os_csprng draws differ and are non-zero; sec001, sec002 (encryption) and test001 btree/hash pass. --- dist/Makefile.in | 4 +- dist/configure | 23 +++++++ dist/configure.ac | 7 ++- dist/meson.build | 1 + dist/srcfiles.in | 1 + src/common/db_err.c | 4 +- src/crypto/mersenne/mt19937db.c | 20 ++++--- src/dbinc/db_int.in | 10 ++++ src/dbinc_auto/os_ext.h | 1 + src/os/os_csprng.c | 102 ++++++++++++++++++++++++++++++++ 10 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 src/os/os_csprng.c diff --git a/dist/Makefile.in b/dist/Makefile.in index b821770d5..70722af76 100644 --- a/dist/Makefile.in +++ b/dist/Makefile.in @@ -291,7 +291,7 @@ DTRACE_OBJS= @ADDITIONAL_OBJS@ @REPLACEMENT_OBJS@ @CRYPTO_OBJS@ \ mp_fmethod@o@ mp_fopen@o@ mp_fput@o@ mp_fset@o@ mp_method@o@ \ mp_mvcc@o@ mp_region@o@ mp_register@o@ mp_resize@o@ mp_stat@o@ \ mp_sync@o@ mp_trickle@o@ openflags@o@ os_abort@o@ os_abs@o@ os_aio@o@ os_aio_iocp@o@ os_aio_kqueue@o@ os_aio_pool@o@ os_aio_posix@o@ os_aio_uring@o@ \ - os_alloc@o@ os_atomic@o@ os_clock@o@ os_cpu@o@ os_ctime@o@ os_config@o@ \ + os_alloc@o@ os_atomic@o@ os_clock@o@ os_cpu@o@ os_csprng@o@ os_ctime@o@ os_config@o@ \ os_dir@o@ os_errno@o@ os_fid@o@ os_flock@o@ os_fsync@o@ \ os_getenv@o@ os_handle@o@ os_map@o@ os_method@o@ os_mkdir@o@ \ os_open@o@ os_path@o@ os_pid@o@ os_rename@o@ os_root@o@ \ @@ -2705,6 +2705,8 @@ os_clock@o@: $(srcdir)/@OSDIR@/os_clock.c $(CC) $(CFLAGS) $(DEPFLAGS) $< os_config@o@: $(srcdir)/@OSDIR@/os_config.c $(CC) $(CFLAGS) $(DEPFLAGS) $< +os_csprng@o@: $(srcdir)/os/os_csprng.c + $(CC) $(CFLAGS) $(DEPFLAGS) $< os_cpu@o@: $(srcdir)/@OSDIR@/os_cpu.c $(CC) $(CFLAGS) $(DEPFLAGS) $< os_ctime@o@: $(srcdir)/os/os_ctime.c diff --git a/dist/configure b/dist/configure index 94bc4d4c0..9c9a73ec0 100755 --- a/dist/configure +++ b/dist/configure @@ -22094,6 +22094,12 @@ if test "x$ac_cv_header_execinfo_h" = xyes then : printf '%s\n' "#define HAVE_EXECINFO_H 1" >>confdefs.h +fi +ac_fn_c_check_header_compile "$LINENO" "sys/random.h" "ac_cv_header_sys_random_h" "$ac_includes_default" +if test "x$ac_cv_header_sys_random_h" = xyes +then : + printf '%s\n' "#define HAVE_SYS_RANDOM_H 1" >>confdefs.h + fi ac_fn_c_check_header_compile "$LINENO" "sys/select.h" "ac_cv_header_sys_select_h" "$ac_includes_default" if test "x$ac_cv_header_sys_select_h" = xyes @@ -27320,6 +27326,23 @@ then : fi +# Cryptographically strong OS entropy, used to seed encryption IVs. +# getrandom(2) is Linux/glibc 2.25+; arc4random_buf(3) is the BSDs and macOS. +# Neither is required: os_csprng.c falls back to reading /dev/urandom. +ac_fn_c_check_func "$LINENO" "getrandom" "ac_cv_func_getrandom" +if test "x$ac_cv_func_getrandom" = xyes +then : + printf '%s\n' "#define HAVE_GETRANDOM 1" >>confdefs.h + +fi +ac_fn_c_check_func "$LINENO" "arc4random_buf" "ac_cv_func_arc4random_buf" +if test "x$ac_cv_func_arc4random_buf" = xyes +then : + printf '%s\n' "#define HAVE_ARC4RANDOM_BUF 1" >>confdefs.h + +fi + + ac_fn_c_check_func "$LINENO" "gettimeofday" "ac_cv_func_gettimeofday" diff --git a/dist/configure.ac b/dist/configure.ac index 869ccc45a..856599068 100644 --- a/dist/configure.ac +++ b/dist/configure.ac @@ -742,7 +742,7 @@ fi AC_HEADER_STAT AC_CHECK_HEADERS([sys/time.h time.h]) AC_HEADER_DIRENT -AC_CHECK_HEADERS(execinfo.h sys/select.h sys/socket.h sys/time.h) +AC_CHECK_HEADERS(execinfo.h sys/random.h sys/select.h sys/socket.h sys/time.h) AC_CHECK_MEMBERS([struct stat.st_blksize]) AM_TYPES @@ -847,6 +847,11 @@ AC_CHECK_FUNCS(\ pthread_self pthread_yield random sched_yield select setgid setuid\ sigaction snprintf stat sysconf vsnprintf yield) +# Cryptographically strong OS entropy, used to seed encryption IVs. +# getrandom(2) is Linux/glibc 2.25+; arc4random_buf(3) is the BSDs and macOS. +# Neither is required: os_csprng.c falls back to reading /dev/urandom. +AC_CHECK_FUNCS(getrandom arc4random_buf) + AC_TIMERS # Ftruncate. diff --git a/dist/meson.build b/dist/meson.build index f6c0ca7bd..60e5262aa 100644 --- a/dist/meson.build +++ b/dist/meson.build @@ -327,6 +327,7 @@ libdb_srcnames = [ 'src/os/os_atomic.c', 'src/os/os_clock.c', 'src/os/os_config.c', + 'src/os/os_csprng.c', 'src/os/os_cpu.c', 'src/os/os_ctime.c', 'src/os/os_dir.c', diff --git a/dist/srcfiles.in b/dist/srcfiles.in index f91288609..db3fa2632 100644 --- a/dist/srcfiles.in +++ b/dist/srcfiles.in @@ -230,6 +230,7 @@ src/os/os_aio_iocp.c android src/os/os_alloc.c android src/os/os_clock.c android src/os/os_config.c android +src/os/os_csprng.c android src/os/os_cpu.c android src/os/os_ctime.c android src/os/os_dir.c android diff --git a/src/common/db_err.c b/src/common/db_err.c index e21fa1c09..c674263cc 100644 --- a/src/common/db_err.c +++ b/src/common/db_err.c @@ -357,10 +357,10 @@ db_strerror(error) return (DB_STR("0088", "DB_SECONDARY_BAD: Secondary index inconsistent with primary")); case DB_SNAPSHOT_CONFLICT: - return (DB_STR("4573", + return (DB_STR("0211", "DB_SNAPSHOT_CONFLICT: Serializable snapshot update conflict")); case DB_SNAPSHOT_UNSAFE: - return (DB_STR("4574", + return (DB_STR("0212", "DB_SNAPSHOT_UNSAFE: Potential serializable snapshot anomaly")); case DB_TIMEOUT: return (DB_STR("0089", "DB_TIMEOUT: Operation timed out")); diff --git a/src/crypto/mersenne/mt19937db.c b/src/crypto/mersenne/mt19937db.c index 62076dfe0..1a57f4eb1 100644 --- a/src/crypto/mersenne/mt19937db.c +++ b/src/crypto/mersenne/mt19937db.c @@ -154,14 +154,20 @@ __db_genrand(env) if (env->mti == N+1) { /* if sgenrand() has not been called, */ /* - * Seed the generator with the hashed time. The __db_mac - * function will return 4 bytes if we don't send in a key. + * Seed the generator from the OS entropy source. This used to + * hash the wall-clock seconds, which is low-entropy and + * guessable -- an attacker who knows roughly when the + * environment was created could narrow the IV stream. Fall + * back to the hashed clock only if the OS has no entropy + * source at all, so encryption still functions on such a + * platform (with the historical, weaker seeding). */ - do { - __os_gettime(env, &ts, 1); - __db_chksum(NULL, (u_int8_t *)&ts.tv_sec, - sizeof(ts.tv_sec), NULL, (u_int8_t *)&seed); - } while (seed == 0); + if (__os_csprng(env, &seed, sizeof(seed)) != 0 || seed == 0) + do { + __os_gettime(env, &ts, 1); + __db_chksum(NULL, (u_int8_t *)&ts.tv_sec, + sizeof(ts.tv_sec), NULL, (u_int8_t *)&seed); + } while (seed == 0); __db_sgenrand((unsigned long)seed, env->mt, &env->mti); } diff --git a/src/dbinc/db_int.in b/src/dbinc/db_int.in index 21d828df7..018ffbc97 100644 --- a/src/dbinc/db_int.in +++ b/src/dbinc/db_int.in @@ -25,6 +25,16 @@ #endif #endif +/* + * getrandom(2) is declared in on glibc and in on + * some others; include the header when we have it so os_csprng.c can call it. + */ +#ifdef HAVE_GETRANDOM +#ifdef HAVE_SYS_RANDOM_H +#include +#endif +#endif + #if TIME_WITH_SYS_TIME #include #include diff --git a/src/dbinc_auto/os_ext.h b/src/dbinc_auto/os_ext.h index 41a28fb0a..9be5a7a01 100644 --- a/src/dbinc_auto/os_ext.h +++ b/src/dbinc_auto/os_ext.h @@ -320,6 +320,7 @@ int __os_support_direct_io __P((void)); int __os_support_db_register __P((void)); int __os_support_replication __P((void)); u_int32_t __os_cpu_count __P((void)); +int __os_csprng __P((ENV *, void *, size_t)); char *__os_ctime __P((const time_t *, char *)); int __os_dirlist __P((ENV *, const char *, int, char ***, int *)); void __os_dirfree __P((ENV *, char **, int)); diff --git a/src/os/os_csprng.c b/src/os/os_csprng.c new file mode 100644 index 000000000..809e0230b --- /dev/null +++ b/src/os/os_csprng.c @@ -0,0 +1,102 @@ +/*- + * See the file LICENSE for redistribution information. + * + * Copyright (c) 2026 The libdb contributors. + * + * $Id$ + */ + +#include "db_config.h" + +#include "db_int.h" + +/* + * __os_csprng -- + * Fill a buffer with cryptographically strong random bytes from the + * operating system. + * + * This is the entropy source for security-sensitive values (currently + * encryption initialization vectors). It is deliberately separate from + * the Mersenne Twister in src/crypto/mersenne: that generator is fine for + * sequence quality but is not a CSPRNG and was historically seeded from + * hashed wall-clock seconds, which is guessable. + * + * Returns 0 on success, or a non-zero error if no OS entropy source is + * available; callers MUST handle failure rather than silently falling back + * to a weak source. + * + * PUBLIC: int __os_csprng __P((ENV *, void *, size_t)); + */ +int +__os_csprng(env, buf, len) + ENV *env; + void *buf; + size_t len; +{ + u_int8_t *p; + size_t need; + ssize_t n; + int fd, ret; + + p = buf; + need = len; + +#ifdef HAVE_GETRANDOM + /* + * Linux (glibc 2.25+) / others: getrandom(2). It can return a short + * read, and can be interrupted, so loop. GRND_NONBLOCK is NOT used: + * we would rather block briefly at first use than fail or fall back to + * a weak source. + */ + while (need > 0) { + n = getrandom(p, need, 0); + if (n < 0) { + if (__os_get_syserr() == EINTR) + continue; + break; /* Fall through to /dev/urandom. */ + } + p += n; + need -= (size_t)n; + } + if (need == 0) + return (0); + /* Reset and try the device. */ + p = buf; + need = len; +#endif + +#ifdef HAVE_ARC4RANDOM_BUF + /* + * The BSDs and macOS: arc4random_buf() cannot fail and needs no fd. + */ + arc4random_buf(p, need); + return (0); +#else + /* + * Portable fallback: read /dev/urandom. Use the OS layer so the file + * handling matches the rest of the library. + */ + if ((ret = __os_open(env, "/dev/urandom", 0, + DB_OSO_RDONLY, DB_MODE_600, &fd)) != 0) + return (ret); + + while (need > 0) { + if ((ret = __os_read(env, fd, p, need, &n)) != 0) { + (void)__os_closehandle(env, fd); + return (ret); + } + if (n == 0) /* Unexpected EOF. */ + break; + p += n; + need -= (size_t)n; + } + (void)__os_closehandle(env, fd); + + if (need != 0) { + __db_errx(env, DB_STR("0213", + "Unable to obtain random bytes from the operating system")); + return (EIO); + } + return (0); +#endif +} From 953acd123463b590483839e781e417bacbd8ded8 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Sun, 6 Sep 2026 15:38:41 -0400 Subject: [PATCH 2/3] fix(os): use the DB_FH handle API in the os_csprng urandom fallback My first version of the /dev/urandom fallback used a raw `int fd` with __os_open/__os_read/__os_closehandle, but this OS layer takes an opaque DB_FH * (__os_open's last arg is DB_FH **, __os_read takes DB_FH * and size_t *nr). That broke 26 CI jobs (macOS, clang, Windows, nix, meson). Why my local check missed it: HAVE_GETRANDOM is defined on this host, so the fallback branch was preprocessed out and never compiled -- I validated only the configuration that skips the buggy code. Now compiled AND run in all three configurations: getrandom, arc4random_buf, and the urandom fallback (0 errors, 0 warnings each); clang and --disable-cryptography builds clean; two draws differ in both the default and the forced-fallback build. --- src/os/os_csprng.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/os/os_csprng.c b/src/os/os_csprng.c index 809e0230b..9b90c3b04 100644 --- a/src/os/os_csprng.c +++ b/src/os/os_csprng.c @@ -35,8 +35,14 @@ __os_csprng(env, buf, len) { u_int8_t *p; size_t need; +#if defined(HAVE_GETRANDOM) ssize_t n; - int fd, ret; +#endif +#if !defined(HAVE_ARC4RANDOM_BUF) + DB_FH *fhp; + size_t nr; + int ret; +#endif p = buf; need = len; @@ -74,23 +80,24 @@ __os_csprng(env, buf, len) #else /* * Portable fallback: read /dev/urandom. Use the OS layer so the file - * handling matches the rest of the library. + * handling matches the rest of the library (DB_FH handle, not a raw fd). */ if ((ret = __os_open(env, "/dev/urandom", 0, - DB_OSO_RDONLY, DB_MODE_600, &fd)) != 0) + DB_OSO_RDONLY, DB_MODE_600, &fhp)) != 0) return (ret); while (need > 0) { - if ((ret = __os_read(env, fd, p, need, &n)) != 0) { - (void)__os_closehandle(env, fd); + nr = 0; + if ((ret = __os_read(env, fhp, p, need, &nr)) != 0) { + (void)__os_closehandle(env, fhp); return (ret); } - if (n == 0) /* Unexpected EOF. */ + if (nr == 0) /* Unexpected EOF. */ break; - p += n; - need -= (size_t)n; + p += nr; + need -= nr; } - (void)__os_closehandle(env, fd); + (void)__os_closehandle(env, fhp); if (need != 0) { __db_errx(env, DB_STR("0213", From fc81b62536607e48b9dede8b49802c4355f76ca0 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Sun, 6 Sep 2026 15:56:16 -0400 Subject: [PATCH 3/3] fix(os_windows): add the Windows __os_csprng implementation Windows compiles mt19937db.c (which now calls __os_csprng) but had no implementation, so the DLL link failed with LNK2019: unresolved external symbol __os_csprng referenced in function __db_genrand. I had registered os_csprng.c only in the POSIX build registries; Windows keeps its sources in the VS project files, not dist/srcfiles.in. src/os_windows/os_csprng.c uses RtlGenRandom (SystemFunction036 via advapi32) rather than BCryptGenRandom: no provider handle, available since XP, and no new bcrypt.lib dependency. Registered in VS10/db.vcxproj and db_small.vcxproj. POSIX build re-verified clean; message ids stay in category 0's range (0213 POSIX / 0214 Windows) and s_message_id runs clean. --- build_windows/VS10/db.vcxproj | 1 + build_windows/VS10/db_small.vcxproj | 1 + src/os_windows/os_csprng.c | 64 +++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/os_windows/os_csprng.c diff --git a/build_windows/VS10/db.vcxproj b/build_windows/VS10/db.vcxproj index b4c784d86..8f8172fdc 100644 --- a/build_windows/VS10/db.vcxproj +++ b/build_windows/VS10/db.vcxproj @@ -340,6 +340,7 @@ + diff --git a/build_windows/VS10/db_small.vcxproj b/build_windows/VS10/db_small.vcxproj index 565561c48..01ec6343d 100644 --- a/build_windows/VS10/db_small.vcxproj +++ b/build_windows/VS10/db_small.vcxproj @@ -316,6 +316,7 @@ + diff --git a/src/os_windows/os_csprng.c b/src/os_windows/os_csprng.c new file mode 100644 index 000000000..f91a56deb --- /dev/null +++ b/src/os_windows/os_csprng.c @@ -0,0 +1,64 @@ +/*- + * See the file LICENSE for redistribution information. + * + * Copyright (c) 2026 The libdb contributors. + * + * $Id$ + */ + +#include "db_config.h" + +#include "db_int.h" + +/* + * __os_csprng -- + * Fill a buffer with cryptographically strong random bytes from the + * operating system (Windows). + * + * This is the Windows counterpart of src/os/os_csprng.c and the entropy + * source for security-sensitive values (currently encryption + * initialization vectors). RtlGenRandom (exposed as SystemFunction036) + * is used rather than BCryptGenRandom: it needs no provider handle, has + * been available since Windows XP, and avoids linking bcrypt.lib. + * + * Returns 0 on success, or a non-zero error if the OS declines; callers + * MUST handle failure rather than silently falling back to a weak source. + * + * PUBLIC: int __os_csprng __P((ENV *, void *, size_t)); + */ +int +__os_csprng(env, buf, len) + ENV *env; + void *buf; + size_t len; +{ + /* + * RtlGenRandom is declared as SystemFunction036 in ntsecapi.h, which + * pulls in a large amount of unrelated interface; declare it directly + * the way Microsoft documents for this use. + */ + BOOLEAN (APIENTRY *pRtlGenRandom)(PVOID, ULONG); + HMODULE advapi; + int ret; + + if (len == 0) + return (0); + + ret = 0; + if ((advapi = LoadLibraryA("advapi32.dll")) == NULL) + goto err; + + pRtlGenRandom = (BOOLEAN (APIENTRY *)(PVOID, ULONG)) + GetProcAddress(advapi, "SystemFunction036"); + if (pRtlGenRandom == NULL || + !pRtlGenRandom(buf, (ULONG)len)) + ret = EIO; + + (void)FreeLibrary(advapi); + if (ret == 0) + return (0); + +err: __db_errx(env, DB_STR("0214", + "Unable to obtain random bytes from the operating system")); + return (ret == 0 ? EIO : ret); +}