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.

Test suites
SuiteCasesCovers
test_secret26The 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_atexit7Wipe 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_gc6Survival across minor collection and compaction, finalizer release and zeroing, GC pressure accounting, minor-heap scrubbing.
test_domains6Parallel creation, concurrent reads, atomic entropy-source replacement, cross-domain finalization, and concurrent double destroy. OCaml 5 only.
test_guard4Overflow and underflow hitting a guard page, canary corruption aborting, in-bounds writes surviving. Runs a child that is expected to die.
test_fork3`Keep, `Wipe_in_child, and switching back.
test_unix2Descriptor read and write, and the error paths.
test_unix_wipe1A partially filled secret is destroyed when a read fails.
test_c_apisecret.h exercised from C: borrowing, length, destruction.
test_mlock_limitmlock 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:

  1. Add the probe to config/discover.ml.
  2. Guard the implementation in the stubs with the new switch, and give it a portable fallback.
  3. Surface the outcome through capabilities for the build and through status for the value.
  4. 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

What runs where
JobTargetRuns
CIUbuntu, OCaml 4.14 and every 5.0–5.5 minor@install @runtest
CIUbuntu, bytecode-only and Flambda compilers@install @runtest
CIUbuntu, 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
CImacOS, OCaml 5.5@install @runtest
PortabilityAlpine musl, OCaml 5.4@install @runtest
PortabilityAlpine i386, OCaml 4.14The 32-bit word size, where block arithmetic and pool size classes can alias
PortabilityWindows mingw, OCaml 5.4Best effort
Portabilitysolo5 cross-buildBest 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.