GitHub's downtime as a FreeBSD entropy source.
For each pair of consecutive resolved GitHub incidents, emit 1 if the later
outage lasted longer than the earlier one and 0 if it lasted less. A Rust
daemon derives and conditions the bits; a C kernel module hands them to Fortuna
so that GitHub Downtime appears in kern.random.random_sources.
$ sysctl kern.random.random_sources
kern.random.random_sources: 'GitHub Downtime','Intel Secure Key Seed'
$ sysctl kern.random.ghrandom.seconds_to_next_seed
kern.random.ghrandom.seconds_to_next_seed: 1877014 # 21.7 days, for 32 bits
Incident durations are public. Anyone polling githubstatus.com derives the
identical bitstream, so this has no secrecy value whatsoever.
The module therefore discards everything written to it until you set
kern.random.ghrandom.claim_entropy=1 by hand, which prints a console warning.
Feeding known data to Fortuna cannot subtract entropy -- the pools are hash
accumulators -- but crediting it would inflate the kernel's accounting for data
an adversary also has. Default off. Never use this as your only source.
Against 60 days of the live feed and a 230-incident, 9-month archive:
| rate | 256-bit seed | lag-1 autocorr | runs test | |
|---|---|---|---|---|
| overlapping pairs (default) | 0.83 bits/day | 10.2 months | -0.432 | FAIL (z=6.46) |
--disjoint | 0.42 bits/day | 20.2 months | +0.148 | pass (z=1.67) |
--von-neumann | 0.27 bits/day | 2.6 years | unchanged | -- |
Bias is excellent either way (0.4934 over 227 bits; 0.5 is ideal). There are just almost none of them.
Comparing consecutive durations shares one duration between adjacent bits. For any i.i.d. source:
P(up, up) = P(d1 < d2 < d3) = 1/6, but P(up)P(up) = 1/4
Cov = 1/6 - 1/4 = -1/12, Var = 1/4 => r = -1/3
Simulation over 400 trials of i.i.d. lognormal, exponential and uniform durations reproduces r = -0.335 exactly. So the -0.432 measured on GitHub's data is the method, not GitHub -- only the modest excess over -1/3 is attributable to the corpus (minute-resolution ties).
--disjoint compares non-overlapping pairs and removes it, at half the rate.
Note that --von-neumann corrects bias, not correlation, and does not
help here.
kmod/ ghrandom.c, ghrandom.h -- the kernel module
ghentropyd/ Rust daemon (22 tests)
rc.d/ service script
The module links against kernel internals, so it must be built against source matching the running kernel, not merely a recent checkout:
sysctl -n kern.osreldate # e.g. 1600025
grep '__FreeBSD_version' /usr/src/sys/sys/param.h # must match
make -C kmod # add SYSDIR=/path/to/sys if /usr/src differs
cargo build --release --manifest-path ghentropyd/Cargo.toml
kldload ./kmod/ghrandom.ko
ghentropyd --once --dry-run --state /tmp/state.json # safe: writes nothing
# Replay the history archive instead of waiting ten months.
ghentropyd --backfill 3 --once --output /dev/ghrandom
sysctl kern.random.ghrandom
Useful flags: --disjoint, --von-neumann, --interval, --width,
--output (works with /dev/random if the module is not loaded).
Push mode. The module registers with rs_read = NULL and harvests from
d_write instead. Fortuna polls rs_read with count == 8, four times per
100 ms once seeded and 256 times per 100 ms until seeded; a source that
produces one byte every ten days would return 0 to every one of those calls.
Pushing from the write path avoids that and can sleep, which rs_read cannot
(it runs inside an EPOCH_PREEMPT section).
Source enumerator. The module squats on RANDOM_PURE_RNDTEST (17), which
is unclaimed in-tree. A module cannot add an enumerator: healthtest[] and
hc_destination[] are sized [ENTROPYSOURCE] and the bounds checks are
KASSERTs, compiled out without INVARIANTS. rs_ident is a free string, so
kern.random.random_sources still reads GitHub Downtime while
kern.random.harvest.mask_symbolic reads PURE_RNDTEST.
The feed is mutable. 49 of 50 incidents in the live feed had updated_at
move more than an hour after resolved_at, one 7.1 days later. The daemon
dedups by incident id and warns if a re-observed duration ever changes rather
than re-harvesting it.
Backfill shape. history.json?page=N carries no timestamps, only incident
codes; api/v2/incidents/<code>.json has the millisecond record but wraps it
under an "incident" key (the bare /incidents/<code>.json path does not).
ivy.c is not the template to copy. It declares its sysctl_ctx_list as a
stack local in the modevent handler and never frees it, leaving an OID pointing
into an unloaded module. This uses a static SYSCTL_NODE, as rdseed.c does.
Module loads, registers, and unloads cleanly over 20 cycles with no M_ENTROPY
leak; unloading while the device is held open does not panic; the
claim_entropy gate provably stops crediting. Daemon: 22 tests, clippy and
cargo fmt clean; rc.d script passes ShellCheck.
Statistical batteries were run in Python; ent and dieharder are not
installed here. Note that any battery run on the conditioned output measures
SHA-256, not GitHub -- the source produces far too few raw bits to test.
1 commits
Rust
81.4%
C
13.7%
Shell
3.4%
Makefile
1.5%
GitHub's downtime as a FreeBSD entropy source.
For each pair of consecutive resolved GitHub incidents, emit 1 if the later
outage lasted longer than the earlier one and 0 if it lasted less. A Rust
daemon derives and conditions the bits; a C kernel module hands them to Fortuna
so that GitHub Downtime appears in kern.random.random_sources.
$ sysctl kern.random.random_sources
kern.random.random_sources: 'GitHub Downtime','Intel Secure Key Seed'
$ sysctl kern.random.ghrandom.seconds_to_next_seed
kern.random.ghrandom.seconds_to_next_seed: 1877014 # 21.7 days, for 32 bits
Incident durations are public. Anyone polling githubstatus.com derives the
identical bitstream, so this has no secrecy value whatsoever.
The module therefore discards everything written to it until you set
kern.random.ghrandom.claim_entropy=1 by hand, which prints a console warning.
Feeding known data to Fortuna cannot subtract entropy -- the pools are hash
accumulators -- but crediting it would inflate the kernel's accounting for data
an adversary also has. Default off. Never use this as your only source.
Against 60 days of the live feed and a 230-incident, 9-month archive:
| rate | 256-bit seed | lag-1 autocorr | runs test | |
|---|---|---|---|---|
| overlapping pairs (default) | 0.83 bits/day | 10.2 months | -0.432 | FAIL (z=6.46) |
--disjoint | 0.42 bits/day | 20.2 months | +0.148 | pass (z=1.67) |
--von-neumann | 0.27 bits/day | 2.6 years | unchanged | -- |
Bias is excellent either way (0.4934 over 227 bits; 0.5 is ideal). There are just almost none of them.
Comparing consecutive durations shares one duration between adjacent bits. For any i.i.d. source:
P(up, up) = P(d1 < d2 < d3) = 1/6, but P(up)P(up) = 1/4
Cov = 1/6 - 1/4 = -1/12, Var = 1/4 => r = -1/3
Simulation over 400 trials of i.i.d. lognormal, exponential and uniform durations reproduces r = -0.335 exactly. So the -0.432 measured on GitHub's data is the method, not GitHub -- only the modest excess over -1/3 is attributable to the corpus (minute-resolution ties).
--disjoint compares non-overlapping pairs and removes it, at half the rate.
Note that --von-neumann corrects bias, not correlation, and does not
help here.
kmod/ ghrandom.c, ghrandom.h -- the kernel module
ghentropyd/ Rust daemon (22 tests)
rc.d/ service script
The module links against kernel internals, so it must be built against source matching the running kernel, not merely a recent checkout:
sysctl -n kern.osreldate # e.g. 1600025
grep '__FreeBSD_version' /usr/src/sys/sys/param.h # must match
make -C kmod # add SYSDIR=/path/to/sys if /usr/src differs
cargo build --release --manifest-path ghentropyd/Cargo.toml
kldload ./kmod/ghrandom.ko
ghentropyd --once --dry-run --state /tmp/state.json # safe: writes nothing
# Replay the history archive instead of waiting ten months.
ghentropyd --backfill 3 --once --output /dev/ghrandom
sysctl kern.random.ghrandom
Useful flags: --disjoint, --von-neumann, --interval, --width,
--output (works with /dev/random if the module is not loaded).
Push mode. The module registers with rs_read = NULL and harvests from
d_write instead. Fortuna polls rs_read with count == 8, four times per
100 ms once seeded and 256 times per 100 ms until seeded; a source that
produces one byte every ten days would return 0 to every one of those calls.
Pushing from the write path avoids that and can sleep, which rs_read cannot
(it runs inside an EPOCH_PREEMPT section).
Source enumerator. The module squats on RANDOM_PURE_RNDTEST (17), which
is unclaimed in-tree. A module cannot add an enumerator: healthtest[] and
hc_destination[] are sized [ENTROPYSOURCE] and the bounds checks are
KASSERTs, compiled out without INVARIANTS. rs_ident is a free string, so
kern.random.random_sources still reads GitHub Downtime while
kern.random.harvest.mask_symbolic reads PURE_RNDTEST.
The feed is mutable. 49 of 50 incidents in the live feed had updated_at
move more than an hour after resolved_at, one 7.1 days later. The daemon
dedups by incident id and warns if a re-observed duration ever changes rather
than re-harvesting it.
Backfill shape. history.json?page=N carries no timestamps, only incident
codes; api/v2/incidents/<code>.json has the millisecond record but wraps it
under an "incident" key (the bare /incidents/<code>.json path does not).
ivy.c is not the template to copy. It declares its sysctl_ctx_list as a
stack local in the modevent handler and never frees it, leaving an OID pointing
into an unloaded module. This uses a static SYSCTL_NODE, as rdseed.c does.
Module loads, registers, and unloads cleanly over 20 cycles with no M_ENTROPY
leak; unloading while the device is held open does not panic; the
claim_entropy gate provably stops crediting. Daemon: 22 tests, clippy and
cargo fmt clean; rc.d script passes ShellCheck.
Statistical batteries were run in Python; ent and dieharder are not
installed here. Note that any battery run on the conditioned output measures
SHA-256, not GitHub -- the source produces far too few raw bits to test.
1 commits
Rust
81.4%
C
13.7%
Shell
3.4%
Makefile
1.5%