common: add randbytes() wrapper to override cryptographic entropy: $CLN_DEV_ENTROPY_SEED
What changed, and why it matters
This commit adds a developer-only feature that lets Core Lightning use predictable fake random numbers instead of real cryptographic randomness when a special environment variable is set. It is intended only for testing and reproducible debugging, not for production use. The code is gated behind developer mode and is not itself a vulnerability, but any accidental use in production would severely weaken security that depends on randomness.
Treat this as a test/debugging aid, not a security fix. Verify that the override cannot be activated in production builds (developer mode disabled), that the environment variable is not documented for end users, and that CI/release builds do not enable it. No patch is required unless the override can be triggered in non-developer builds.
Security signals we found
Addition of a deterministic entropy override mechanism gated on developer mode
Use of SipHash of argv[0] plus environment seed to derive per-process predictable randomness
Assertion that the override is not installed after randbytes() has already been used
Macro-based per-caller offset to keep deterministic output stable across call-order changes
Developer-mode-only environment variable CLN_DEV_ENTROPY_SEED controls activation
Evidence from the diff
The commit introduces a randbytes() wrapper around libsodium’s randombytes_buf(). In developer mode, setting CLN_DEV_ENTROPY_SEED overrides cryptographic entropy with a deterministic sequence derived from a SipHash of argv[0] and the seed. Per-caller static offsets are used to keep output stable despite timing differences. The override is initialized in daemon_developer_mode() and only applies when the binary is built/started in developer mode. The main change in lightningd.c moves trace_span_start() after daemon_setup() because tracing may call pseudorand() and the entropy override must be set up first. Most other changes add common/memleak.h includes to unit tests, likely because the new randbytes code pulls in memleak-related dependencies.
Changed components
common/randbytes.ccommon/randbytes.hcommon/daemon.clightningd/lightningd.ccommon/Makefilemultiple unit test files (memleak.h includes)Inspect captured patch +109 / −2
diff --git a/channeld/test/run-commit_tx.c b/channeld/test/run-commit_tx.c
index b57e50a..cc0a8cb 100644
--- a/channeld/test/run-commit_tx.c
+++ b/channeld/test/run-commit_tx.c
@@ -16,6 +16,7 @@ static bool print_superverbose;
#include <common/channel_id.h>
#include <common/daemon.h>
#include <common/key_derive.h>
+#include <common/memleak.h>
#include <common/setup.h>
#include <common/status.h>
diff --git a/common/Makefile b/common/Makefile
index 91e95e2..e5d3fa6 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -86,6 +86,7 @@ COMMON_SRC_NOGEN := \
common/psbt_keypath.c \
common/psbt_open.c \
common/pseudorand.c \
+ common/randbytes.c \
common/random_select.c \
common/read_peer_msg.c \
common/route.c \
diff --git a/common/daemon.c b/common/daemon.c
index 6515828..f7170ac 100644
--- a/common/daemon.c
+++ b/common/daemon.c
@@ -10,6 +10,7 @@
#include <ccan/tal/str/str.h>
#include <common/daemon.h>
#include <common/memleak.h>
+#include <common/randbytes.h>
#include <common/setup.h>
#include <common/utils.h>
#include <common/version.h>
@@ -201,6 +202,7 @@ void daemon_shutdown(void)
bool daemon_developer_mode(char *argv[])
{
bool developer = false, debug = false;
+ const char *entropy_override;
for (int i = 1; argv[i]; i++) {
if (streq(argv[i], "--dev-debug-self"))
@@ -225,6 +227,12 @@ bool daemon_developer_mode(char *argv[])
kill(getpid(), SIGSTOP);
}
+ /* We can override cryptographic randomness with this var in development
+ * mode, for reproducible results */
+ entropy_override = getenv("CLN_DEV_ENTROPY_SEED");
+ if (entropy_override)
+ dev_override_randbytes(argv[0], atol(entropy_override));
+
/* This checks for any tal_steal loops, but it's not free:
* only use if we're already using the fairly heavy memleak
* detection. */
diff --git a/common/randbytes.c b/common/randbytes.c
new file mode 100644
index 0000000..d78b520
--- /dev/null
+++ b/common/randbytes.c
@@ -0,0 +1,62 @@
+#include "config.h"
+#include <assert.h>
+#include <ccan/crypto/siphash24/siphash24.h>
+#include <ccan/endian/endian.h>
+#include <ccan/tal/tal.h>
+#include <common/memleak.h>
+#include <common/pseudorand.h>
+#include <common/randbytes.h>
+#include <common/utils.h>
+#include <sodium/randombytes.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static bool used = false;
+static u64 dev_seed = 0;
+
+bool randbytes_overridden(void)
+{
+ return dev_seed != 0;
+}
+
+void randbytes_(void *bytes, size_t num_bytes, u64 *offset)
+{
+ static u64 offset_init;
+ be64 pattern;
+
+ used = true;
+ if (!randbytes_overridden()) {
+ randombytes_buf(bytes, num_bytes); /* discouraged: use randbytes() */
+ return;
+ }
+
+ /* First time, start callers at different offsets */
+ if (*offset == 0) {
+ *offset = offset_init;
+ offset_init += 1000;
+ }
+
+ /* Somewhat recognizable pattern */
+ pattern = cpu_to_be64(dev_seed + (*offset)++);
+ for (size_t i = 0; i < num_bytes; i += sizeof(pattern)) {
+ size_t copy = num_bytes - i;
+ if (copy > sizeof(pattern))
+ copy = sizeof(pattern);
+
+ memcpy((u8 *)bytes + i, &pattern, copy);
+ }
+}
+
+/* We want different seeds for each plugin (hence argv0), and for each
+ * lightmingd instance, (hence seed from environment) */
+void dev_override_randbytes(const char *argv0, long int seed)
+{
+ struct siphash_seed hashseed;
+ assert(!used);
+
+ hashseed.u.u64[0] = seed;
+ hashseed.u.u64[1] = 0;
+
+ dev_seed = siphash24(&hashseed, argv0, strlen(argv0));
+ assert(randbytes_overridden());
+}
diff --git a/common/randbytes.h b/common/randbytes.h
new file mode 100644
index 0000000..18aa485
--- /dev/null
+++ b/common/randbytes.h
@@ -0,0 +1,20 @@
+#ifndef LIGHTNING_COMMON_RANDBYTES_H
+#define LIGHTNING_COMMON_RANDBYTES_H
+#include "config.h"
+#include <ccan/short_types/short_types.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+/* Usually the libsodium routine randombytes_buf, but dev options can make this deterministic */
+#define randbytes(bytes, num_bytes) \
+ do { \
+ static u64 offset; \
+ randbytes_((bytes), (num_bytes), &offset); \
+ } while(0)
+
+void randbytes_(void *bytes, size_t num_bytes, u64 *offset);
+
+void dev_override_randbytes(const char *argv0, long int seed);
+
+bool randbytes_overridden(void);
+#endif /* LIGHTNING_COMMON_RANDBYTES_H */
diff --git a/common/test/run-htable.c b/common/test/run-htable.c
index 64ac0b5..267ad8c 100644
--- a/common/test/run-htable.c
+++ b/common/test/run-htable.c
@@ -6,6 +6,7 @@
#include <ccan/short_types/short_types.h>
#include <ccan/tal/tal.h>
#include <common/amount.h>
+#include <common/memleak.h>
#include <common/pseudorand.h>
#include <common/setup.h>
#include <common/utils.h>
diff --git a/common/test/run-param.c b/common/test/run-param.c
index 3cb94bf..749f6a4 100644
--- a/common/test/run-param.c
+++ b/common/test/run-param.c
@@ -7,6 +7,7 @@
#include <assert.h>
#include <ccan/array_size/array_size.h>
#include <common/channel_type.h>
+#include <common/memleak.h>
#include <common/setup.h>
#include <stdio.h>
diff --git a/common/test/run-route-infloop.c b/common/test/run-route-infloop.c
index c2e532f..7556fd2 100644
--- a/common/test/run-route-infloop.c
+++ b/common/test/run-route-infloop.c
@@ -7,6 +7,7 @@
#include <common/dijkstra.h>
#include <common/gossmap.h>
#include <common/gossip_store.h>
+#include <common/memleak.h>
#include <common/route.h>
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
diff --git a/common/test/run-route-specific.c b/common/test/run-route-specific.c
index 93f23de..759b614 100644
--- a/common/test/run-route-specific.c
+++ b/common/test/run-route-specific.c
@@ -12,6 +12,7 @@
#include <common/dijkstra.h>
#include <common/gossmap.h>
#include <common/gossip_store.h>
+#include <common/memleak.h>
#include <common/route.h>
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
diff --git a/common/test/run-route.c b/common/test/run-route.c
index 6b06dab..d9fb4d3 100644
--- a/common/test/run-route.c
+++ b/common/test/run-route.c
@@ -5,6 +5,7 @@
#include <common/dijkstra.h>
#include <common/gossmap.h>
#include <common/gossip_store.h>
+#include <common/memleak.h>
#include <common/route.h>
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
diff --git a/connectd/test/run-crc32_of_update.c b/connectd/test/run-crc32_of_update.c
index 4555c12..8552139 100644
--- a/connectd/test/run-crc32_of_update.c
+++ b/connectd/test/run-crc32_of_update.c
@@ -7,6 +7,7 @@ int unused_main(int argc, char *argv[]);
#include <common/channel_type.h>
#include <common/ecdh.h>
#include <common/json_stream.h>
+#include <common/memleak.h>
#include <common/onionreply.h>
#include <common/setup.h>
#include <stdio.h>
diff --git a/connectd/test/run-netaddress.c b/connectd/test/run-netaddress.c
index 889ef88..a24b0d9 100644
--- a/connectd/test/run-netaddress.c
+++ b/connectd/test/run-netaddress.c
@@ -1,6 +1,7 @@
#include "config.h"
#include <assert.h>
#include <common/amount.h>
+#include <common/memleak.h>
#include <common/node_id.h>
#include <common/setup.h>
#include <common/status.c>
diff --git a/connectd/test/run-websocket.c b/connectd/test/run-websocket.c
index 809618d..d31e889 100644
--- a/connectd/test/run-websocket.c
+++ b/connectd/test/run-websocket.c
@@ -1,6 +1,7 @@
#include "config.h"
#include <assert.h>
#include <common/amount.h>
+#include <common/memleak.h>
#include <ccan/io/io.h>
#include <ccan/read_write_all/read_write_all.h>
#include <wire/wire.h>
diff --git a/gossipd/test/run-check_channel_announcement.c b/gossipd/test/run-check_channel_announcement.c
index b16b887..d396ebb 100644
--- a/gossipd/test/run-check_channel_announcement.c
+++ b/gossipd/test/run-check_channel_announcement.c
@@ -35,6 +35,7 @@ In particular, we set feature bit 19. The spec says we should set feature bit 1
#include <common/channel_type.h>
#include <common/ecdh.h>
#include <common/json_stream.h>
+#include <common/memleak.h>
#include <common/onionreply.h>
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
diff --git a/gossipd/test/run-extended-info.c b/gossipd/test/run-extended-info.c
index 417c0eb..d01c93d 100644
--- a/gossipd/test/run-extended-info.c
+++ b/gossipd/test/run-extended-info.c
@@ -8,6 +8,7 @@
#include <common/ecdh.h>
#include <common/json_parse.h>
#include <common/json_stream.h>
+#include <common/memleak.h>
#include <common/onionreply.h>
#include <common/setup.h>
#include <stdio.h>
diff --git a/gossipd/test/run-txout_failure.c b/gossipd/test/run-txout_failure.c
index 4d3df88..c4c45e1 100644
--- a/gossipd/test/run-txout_failure.c
+++ b/gossipd/test/run-txout_failure.c
@@ -6,6 +6,7 @@
#include <common/daemon_conn.h>
#include <common/ecdh.h>
#include <common/json_stream.h>
+#include <common/memleak.h>
#include <common/onionreply.h>
#include <common/sciddir_or_pubkey.h>
#include <common/setup.h>
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index e5e3ded..d2d4e52 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -1189,8 +1189,6 @@ int main(int argc, char *argv[])
bool try_reexec;
size_t num_channels;
- trace_span_start("lightningd/startup", argv);
-
/*~ What happens in strange locales should stay there. */
setup_locale();
@@ -1214,6 +1212,10 @@ int main(int argc, char *argv[])
* backtraces when we crash (if supported on this platform). */
daemon_setup(argv[0], log_backtrace_print, log_backtrace_exit);
+ /*~ We enable trace as early as possible, but it uses support functions
+ * (particularly if we're avoid entropy) so do it after daemon_setup. */
+ trace_span_start("lightningd/startup", argv);
+
/*~ There's always a battle between what a constructor like this
* should do, and what should be added later by the caller. In
* general, because we use valgrind heavily for testing, we prefer not
diff --git a/wire/test/run-peer-wire.c b/wire/test/run-peer-wire.c
index 8949204..f8b17c3 100644
--- a/wire/test/run-peer-wire.c
+++ b/wire/test/run-peer-wire.c
@@ -12,6 +12,7 @@
#include <stdio.h>
#include <common/channel_type.h>
+#include <common/memleak.h>
#include <common/setup.h>
#include <common/sphinx.h>
#include <common/wireaddr.h>
diff --git a/wire/test/run-tlvstream.c b/wire/test/run-tlvstream.c
index 68420fb..2cb0c8c 100644
--- a/wire/test/run-tlvstream.c
+++ b/wire/test/run-tlvstream.c
@@ -9,6 +9,7 @@ static const char *reason;
#include <common/amount.c>
#include <common/bigsize.c>
#include <common/channel_type.h>
+#include <common/memleak.h>
#include <common/node_id.c>
#include <common/setup.h>
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.