security: gate shstk defaults on no-callcc binaries
Jaime Fournier <jaimef@linbsd.org>
a5561dedba6a1da51f00c07b8cf0d74cebf36e1c
diff --git a/docs/chez-hardening.md b/docs/chez-hardening.md
index 1825558..8bd763f 100644
--- a/docs/chez-hardening.md
+++ b/docs/chez-hardening.md
@@ -317,7 +317,8 @@ Don't enable SHSTK by default in the Chez kernel. Instead:
5. **For programs that do use `call/cc`** — punt until Phase 3.4.
**Implemented for generated binaries:** the Jerboa binary launchers now
-include an x86_64 Linux startup hook before `Sscheme_init`. It is opt-in:
+include an x86_64 Linux startup hook before `Sscheme_init`. It is opt-in for
+general runtime/tooling binaries:
```bash
JERBOA_ENABLE_SHSTK=1 ./dist/jerboa ...
@@ -326,10 +327,19 @@ JERBOA_ENABLE_SHSTK=try ./dist/jerboa ... # continue if unsupported
This covers `make binary`, `make jerboa`, standalone `jerbuild`,
`jerbuild binary`, `make jmcp`, and native `make jlsp`. The hook is deliberately
-not enabled by default until the continuation-aware kernel work in 6.4 lands.
+not enabled by default for REPL/tooling binaries until the continuation-aware
+kernel work in 6.4 lands. Standalone generated binaries may opt into a stricter
+build contract with `JERBOA_BINARY_NO_CALLCC=1`; under that contract the
+launcher compiles with `JERBOA_SHSTK_DEFAULT="try"` and enables SHSTK before
+Scheme startup unless runtime `JERBOA_ENABLE_SHSTK` overrides it. Use that only
+for entries built from a profile that excludes `call/cc` and
+`call-with-current-continuation`.
Set `JERBOA_SECURITY_REPORT=1` on those generated launchers to print the
-compiled CET/BTI/PAC state and the current SHSTK runtime status, then exit
-without booting Scheme or enabling SHSTK.
+compiled CET/BTI/PAC state, the current SHSTK runtime status,
+`shstk-default`, and `shstk-continuation-contract`, then exit without booting
+Scheme. On x86_64 Linux the report path applies the effective SHSTK request
+inside the reporting process before reading status, so release evidence can
+show whether a continuation-free binary defaulted to `try`.
The same launcher build paths also apply target-aware hardening flags:
Linux x86_64 gets `-fcf-protection=full` and, by default,
diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index a5f4486..40f9928 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -978,6 +978,15 @@ error messages/logs. Scanner rules from P2-05 enforce going forward.
evidence; keep opt-in for REPL/tooling. Record the decision and the
call/cc hazard in `release-security.md`.
+- **Status:** complete for the repository-local launcher policy. General
+ runtime/tooling binaries remain opt-in because Chez continuation restoration
+ is not yet shadow-stack-aware. Standalone generated binaries built with
+ `JERBOA_BINARY_NO_CALLCC=1` now compile with `JERBOA_SHSTK_DEFAULT="try"` and
+ `JERBOA_SHSTK_CONTINUATION_FREE=1`; runtime `JERBOA_ENABLE_SHSTK` still
+ overrides. `JERBOA_SECURITY_REPORT=1` now records `shstk-default` and
+ `shstk-continuation-contract` so release evidence can distinguish
+ continuation-free daemon/parser binaries from REPL/tooling artifacts.
+
---
## 9. P3 — Research and frontier
diff --git a/docs/release-security.md b/docs/release-security.md
index 41d03ec..65c4133 100644
--- a/docs/release-security.md
+++ b/docs/release-security.md
@@ -46,6 +46,18 @@ For reproducible-build investigations, `jerbuild binary` honors
absolute and its basename must start with `jerbuild-binary-`; this keeps the
default per-process temporary directory for normal builds while allowing release
evidence jobs to remove one source of path nondeterminism safely.
+For standalone daemon/parser binaries that are built from a profile that
+excludes `call/cc` and `call-with-current-continuation`, set
+`JERBOA_BINARY_NO_CALLCC=1` for the `jerbuild binary` or
+`support/build-binary.sh` invocation. That build contract compiles the launcher
+with `JERBOA_SHSTK_DEFAULT="try"` and `JERBOA_SHSTK_CONTINUATION_FREE=1`, so
+Linux x86_64 SHSTK is attempted before Scheme startup unless the runtime
+environment explicitly sets `JERBOA_ENABLE_SHSTK`. Do not set this for
+`jerboa`, `jerbuild`, `jmcp`, `jlsp`, REPL-capable artifacts, or any program
+that can invoke first-class continuations. `JERBOA_SECURITY_REPORT=1` evidence
+for such binaries must include `shstk-default: try` and
+`shstk-continuation-contract: no-callcc`; general tooling should continue to
+report `shstk-default: <unset>` and `shstk-continuation-contract: unchecked`.
On macOS targets, `jerbuild binary` also passes `-Wl,-no_uuid` by default so
the linker does not inject a fresh Mach-O `LC_UUID` into each standalone binary.
Set `JERBOA_BINARY_DETERMINISTIC_LINK=0` only for local linker diagnostics.
diff --git a/docs/single-binary.md b/docs/single-binary.md
index 4edd238..5298247 100644
--- a/docs/single-binary.md
+++ b/docs/single-binary.md
@@ -407,6 +407,11 @@ symbols automatically:
jerbuild binary --static-native --libdirs lib entry.ss app
```
+For static daemon/parser artifacts that are built from a continuation-free
+profile, add `JERBOA_BINARY_NO_CALLCC=1` to the build environment. That makes
+the generated launcher default Linux x86_64 SHSTK to `try`; leave it unset for
+REPL-capable tools or code that can use `call/cc`.
+
In a `.jerbuild` manifest, use:
```scheme
diff --git a/jerbuild.ss b/jerbuild.ss
index 1ded6be..33198e8 100644
--- a/jerbuild.ss
+++ b/jerbuild.ss
@@ -1777,11 +1777,22 @@
""))]
[else ""]))
-(define (binary-hardening-cflags mt)
- (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
- (target-hardening-cflags mt)
+(define (binary-no-callcc?)
+ (env-enabled? "JERBOA_BINARY_NO_CALLCC" #f))
+
+(define (binary-shstk-contract-cflags)
+ (if (and (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+ (binary-no-callcc?))
+ " -DJERBOA_SHSTK_DEFAULT=\\\"try\\\" -DJERBOA_SHSTK_CONTINUATION_FREE=1"
""))
+(define (binary-hardening-cflags mt)
+ (string-append
+ (if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
+ (target-hardening-cflags mt)
+ "")
+ (binary-shstk-contract-cflags)))
+
(define (binary-hardening-ldflags mt)
(if (env-enabled? "JERBOA_BINARY_HARDEN" #t)
(target-hardening-ldflags mt)
@@ -2454,6 +2465,16 @@
#endif
#endif
+#ifndef JERBOA_SHSTK_DEFAULT
+#define JERBOA_SHSTK_DEFAULT \"\"
+#endif
+
+#ifdef JERBOA_SHSTK_CONTINUATION_FREE
+#define JERBOA_SHSTK_CONTINUATION_CONTRACT \"no-callcc\"
+#else
+#define JERBOA_SHSTK_CONTINUATION_CONTRACT \"unchecked\"
+#endif
+
static int jerboa_env_flag_enabled(const char *v) {
return v && *v &&
strcmp(v, \"0\") != 0 &&
@@ -2472,23 +2493,43 @@ static int jerboa_security_report_requested(void) {
return jerboa_env_flag_enabled(getenv(\"JERBOA_SECURITY_REPORT\"));
}
-static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
-#if defined(__linux__) && defined(__x86_64__)
+static const char *jerboa_shadow_stack_default(void) {
+ return JERBOA_SHSTK_DEFAULT;
+}
+
+static const char *jerboa_shadow_stack_request(void) {
const char *v = getenv(\"JERBOA_ENABLE_SHSTK\");
- if (!jerboa_env_flag_enabled(v)) return;
+ if (v) return v;
+ v = jerboa_shadow_stack_default();
+ return (v && *v) ? v : NULL;
+}
+
+static int jerboa_try_enable_shadow_stack(const char *program_name,
+ const char *request) {
+#if defined(__linux__) && defined(__x86_64__)
+ if (!jerboa_env_flag_enabled(request)) return 0;
if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {
- if (strcmp(v, \"try\") == 0) return;
+ if (strcmp(request, \"try\") == 0) return 0;
fprintf(stderr, \"%s: ARCH_SHSTK_ENABLE failed: %s\\n\",
program_name ? program_name : \"jerboa\", strerror(errno));
exit(127);
}
+ return 1;
#else
(void)program_name;
+ (void)request;
+ return 0;
#endif
}
+static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
+ (void)jerboa_try_enable_shadow_stack(program_name,
+ jerboa_shadow_stack_request());
+}
+
static void jerboa_print_security_report(const char *program_name) {
- const char *request = getenv(\"JERBOA_ENABLE_SHSTK\");
+ const char *request = jerboa_shadow_stack_request();
+ const char *default_request = jerboa_shadow_stack_default();
printf(\"program: %s\\n\", program_name ? program_name : \"jerboa\");
#if defined(__linux__)
printf(\"os: linux\\n\");
@@ -2527,6 +2568,7 @@ static void jerboa_print_security_report(const char *program_name) {
printf(\"arm-pac-compiled: no\\n\");
#endif
#if defined(__linux__) && defined(__x86_64__)
+ (void)jerboa_try_enable_shadow_stack(program_name, request);
{
unsigned long features = 0;
errno = 0;
@@ -2542,6 +2584,10 @@ static void jerboa_print_security_report(const char *program_name) {
#else
printf(\"shstk-runtime: not-applicable\\n\");
#endif
+ printf(\"shstk-default: %s\\n\",
+ default_request && *default_request ? default_request : \"<unset>\");
+ printf(\"shstk-continuation-contract: %s\\n\",
+ JERBOA_SHSTK_CONTINUATION_CONTRACT);
printf(\"shstk-request: %s\\n\", request && *request ? request : \"<unset>\");
}
diff --git a/support/build-binary.sh b/support/build-binary.sh
index fe151fd..d528c03 100755
--- a/support/build-binary.sh
+++ b/support/build-binary.sh
@@ -196,6 +196,9 @@ STATIC_CFLAGS=""
if [ -n "$BINARY_STATIC_ENV" ] && [ "$BINARY_STATIC_ENV" != 0 ]; then
STATIC_CFLAGS="-DJERBOA_BINARY_STATIC_ENV=1"
fi
+if flag_enabled "${JERBOA_BINARY_NO_CALLCC:-0}"; then
+ STATIC_CFLAGS="$STATIC_CFLAGS -DJERBOA_SHSTK_DEFAULT=\\\"try\\\" -DJERBOA_SHSTK_CONTINUATION_FREE=1"
+fi
# ── Determine target machine type ───────────────────────────────────────────
if [ "$CROSS_BUILD" = yes ]; then
diff --git a/support/hardening-report.h b/support/hardening-report.h
index 44b6742..885613a 100644
--- a/support/hardening-report.h
+++ b/support/hardening-report.h
@@ -2,8 +2,12 @@
*
* Normal startup behavior is unchanged unless one of the documented
* environment variables is set:
- * JERBOA_ENABLE_SHSTK=1|try opt in to Linux x86_64 shadow stacks
+ * JERBOA_ENABLE_SHSTK=0|1|try override Linux x86_64 shadow-stack startup
* JERBOA_SECURITY_REPORT=1 print build/runtime hardening state and exit
+ *
+ * Builders may define JERBOA_SHSTK_DEFAULT as a string literal, typically
+ * "try", and JERBOA_SHSTK_CONTINUATION_FREE when the entry profile excludes
+ * call/cc. Runtime JERBOA_ENABLE_SHSTK is still the final override.
*/
#ifndef JERBOA_HARDENING_REPORT_H
#define JERBOA_HARDENING_REPORT_H
@@ -30,6 +34,16 @@
#endif
#endif
+#ifndef JERBOA_SHSTK_DEFAULT
+#define JERBOA_SHSTK_DEFAULT ""
+#endif
+
+#ifdef JERBOA_SHSTK_CONTINUATION_FREE
+#define JERBOA_SHSTK_CONTINUATION_CONTRACT "no-callcc"
+#else
+#define JERBOA_SHSTK_CONTINUATION_CONTRACT "unchecked"
+#endif
+
static int jerboa_env_flag_enabled(const char *v) {
return v && *v &&
strcmp(v, "0") != 0 &&
@@ -48,23 +62,44 @@ static int jerboa_security_report_requested(void) {
return jerboa_env_flag_enabled(getenv("JERBOA_SECURITY_REPORT"));
}
-static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
-#if defined(__linux__) && defined(__x86_64__)
+static const char *jerboa_shadow_stack_default(void) {
+ return JERBOA_SHSTK_DEFAULT;
+}
+
+static const char *jerboa_shadow_stack_request(void) {
const char *v = getenv("JERBOA_ENABLE_SHSTK");
- if (!jerboa_env_flag_enabled(v)) return;
+ if (v) return v;
+
+ v = jerboa_shadow_stack_default();
+ return (v && *v) ? v : NULL;
+}
+
+static int jerboa_try_enable_shadow_stack(const char *program_name,
+ const char *request) {
+#if defined(__linux__) && defined(__x86_64__)
+ if (!jerboa_env_flag_enabled(request)) return 0;
if (syscall(SYS_arch_prctl, ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK) != 0) {
- if (strcmp(v, "try") == 0) return;
+ if (strcmp(request, "try") == 0) return 0;
fprintf(stderr, "%s: ARCH_SHSTK_ENABLE failed: %s\n",
program_name ? program_name : "jerboa", strerror(errno));
exit(127);
}
+ return 1;
#else
(void)program_name;
+ (void)request;
+ return 0;
#endif
}
+static void jerboa_maybe_enable_shadow_stack(const char *program_name) {
+ (void)jerboa_try_enable_shadow_stack(program_name,
+ jerboa_shadow_stack_request());
+}
+
static void jerboa_print_security_report(const char *program_name) {
- const char *request = getenv("JERBOA_ENABLE_SHSTK");
+ const char *request = jerboa_shadow_stack_request();
+ const char *default_request = jerboa_shadow_stack_default();
printf("program: %s\n", program_name ? program_name : "jerboa");
@@ -112,6 +147,7 @@ static void jerboa_print_security_report(const char *program_name) {
#endif
#if defined(__linux__) && defined(__x86_64__)
+ (void)jerboa_try_enable_shadow_stack(program_name, request);
{
unsigned long features = 0;
errno = 0;
@@ -128,6 +164,10 @@ static void jerboa_print_security_report(const char *program_name) {
printf("shstk-runtime: not-applicable\n");
#endif
+ printf("shstk-default: %s\n",
+ default_request && *default_request ? default_request : "<unset>");
+ printf("shstk-continuation-contract: %s\n",
+ JERBOA_SHSTK_CONTINUATION_CONTRACT);
printf("shstk-request: %s\n", request && *request ? request : "<unset>");
}