Development
The library is a thin OCaml module over C stubs. Almost everything interesting — the two allocation tiers, the reuse pool, the zeroization primitive, the constant-time comparison — is in the C, and the OCaml side exists to make the unsafe parts nameable.
Build
git clone https://github.com/thevilledev/ocaml-secret.git
cd ocaml-secret
opam install . --deps-only --with-test --with-doc
dune build @install @runtest @doc
There is nothing to configure. config/discover.ml probes
the platform during the build and writes
secret_config.h, and every probe failure is a fallback
rather than an error.
Tests
dune build @runtest
Fifty-five alcotest cases across eight suites, plus two standalone executables. The optional census is separate. Several checks are only meaningful as subprocesses or as a second domain, because what they assert is how the process dies or what another thread can still see.
| Suite | Cases | Covers |
|---|---|---|
test_secret | 26 | The API surface: construction, scoped constructors, blits, exposure, scoped-view liveness, permanent unscoped-view isolation, both tiers, required hardening, pool reuse and size classes, scratch buffers, capabilities, process hardening. |
test_atexit | 7 | Wipe on normal exit, exit n, an uncaught exception and exit from a domain; ordering against user at_exit handlers; the documented Unix._exit gap. Each runs a child process. |
test_gc | 6 | Survival across minor collection and compaction, finalizer release and zeroing, GC pressure accounting, minor-heap scrubbing. |
test_domains | 6 | Parallel creation, concurrent reads, atomic entropy-source replacement, cross-domain finalization, and concurrent double destroy. OCaml 5 only. |
test_guard | 4 | Overflow and underflow hitting a guard page, canary corruption aborting, in-bounds writes surviving. Runs a child that is expected to die. |
test_fork | 3 | `Keep, `Wipe_in_child, and switching back. |
test_unix | 2 | Descriptor read and write, and the error paths. |
test_unix_wipe | 1 | A partially filled secret is destroyed when a read fails. |
test_c_api | — | secret.h exercised from C: borrowing, length, destruction. |
test_mlock_limit | — | mlock failing under a lowered RLIMIT_MEMLOCK is reported, not fatal. |
The two harnesses, allocation check, and census are run by hand or in CI:
dune exec bench/ct_equal.exe -- 20000 200
dune exec bench/alloc_bench.exe
dune exec bench/alloc_check.exe
SECRET_CENSUS=true dune build @census
What each of them measures, and the numbers from the last run, are on the benchmarks page.
Layout
src/
secret.mli the API and its stated limits
secret.ml the OCaml surface over the stubs
secret_stubs.c OCaml entry points, the custom block, views
secret_alloc.c the two tiers, guard pages, canary, reuse pool
secret_zero.c zeroization primitive selection
secret_ct.c constant-time comparison
secret_random.c OS entropy
secret_process.c process-wide hardening
secret_gc.c minor-heap scrubbing
secret.h installed C interface for other libraries
config/discover.ml build-time feature probes
unix/ Secret_unix: read(2)/write(2) on secret memory
bench/ constant-time and allocation harnesses
test/ alcotest suites, C-API test, leak census
website/ this static documentation site
secret_zero.c is deliberately its own translation unit:
keeping the zeroization call across a unit boundary means only link-time
optimisation could let a compiler see through it and elide the write.
Adding a platform feature
Features are not detected by #ifdef __linux__. Each one is
a tiny C program that config/discover.ml compiles and links
with exactly the flags the real build uses, since feature macros such as
_GNU_SOURCE change what the headers declare. The result
becomes a SECRET_HAVE_* switch.
Four steps, in order:
- Add the probe to
config/discover.ml. - Guard the implementation in the stubs with the new switch, and give it a portable fallback.
- Surface the outcome through
capabilitiesfor the build and throughstatusfor the value. - State it in
secret.mli. A feature that is not in the documented list of guarantees is not a guarantee.
Cross-compilation to a freestanding target is handled ahead of the
probes: secret_platform.h forces the freestanding profile
whenever the target compiler defines __ocaml_solo5__ or
__ocaml_freestanding__, so a configurator run with the wrong
compiler cannot switch on OS features that do not exist there.
Continuous integration
| Job | Target | Runs |
|---|---|---|
| CI | Ubuntu, OCaml 4.14 and every 5.0–5.5 minor | @install @runtest |
| CI | Ubuntu, bytecode-only and Flambda compilers | @install @runtest |
| CI | Ubuntu, OCaml 5.5 | @fmt @doc, generated OPAM metadata, clean-archive install, installed OCaml/C consumer, allocation assertions, ASan/UBSan, scan-build, and the optional Mirage Crypto census |
| CI | macOS, OCaml 5.5 | @install @runtest |
| Portability | Alpine musl, OCaml 5.4 | @install @runtest |
| Portability | Alpine i386, OCaml 4.14 | The 32-bit word size, where block arithmetic and pool size classes can alias |
| Portability | Windows mingw, OCaml 5.4 | Best effort |
| Portability | solo5 cross-build | Best effort |
The Clang static analyzer runs on a clean rebuild, and the
clean-archive leg catches anything that only builds because it is still
sitting in the working tree. The full timing and throughput harnesses
run on schedule, on demand, and for release tags; pull requests run a
short timing smoke test and deterministic allocation assertions. The
portability jobs run for relevant pull requests, weekly, on demand,
and for release tags.
The 32-bit leg is not a formality: the C stubs size several things off
value, and a size-class index that aliases two classes
would hand a short block to a longer secret.
Contributing
The bar for a change here is a test that would have caught the bug, and
an .mli that still describes what the code does. A new
hardening feature needs its outcome reported through
status; a new way to reach the bytes needs
expose, unsafe or view in its
name, so that the greppable list stays complete.
Use issues for bugs and proposals. Contributions are ISC licensed.