Security hardening and release readiness
ober
a8c27d9193d354d0b599fc4dd68d0b51c074ca9d
--- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,15 +2,50 @@ name: CI on: push: - branches: [master] + branches: [main, master] pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + JERBOA_VERSION: v0.2.3 jobs: - test: + test-audit: runs-on: ubuntu-latest - container: - image: jerboa21/jerboa:latest steps: - uses: actions/checkout@v4 + + - name: Install system tools + run: | + set -eu + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + file \ + git \ + python3 + + - name: Install Jerboa toolchain + run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin + - name: Run unit tests run: make test + + - name: Native audit + run: make audit + + - name: Release evidence + run: make release-evidence + + - name: High-confidence secret scan + run: | + set -eu + if git grep -I -n -E '(AKIA[0-9A-Z]{16}|-----BEGIN (RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----|ghp_[A-Za-z0-9_]{36,}|xox[baprs]-[A-Za-z0-9-]{10,})' -- . ':!_vendor' ':!vendor' ':!.jerboa'; then + echo "secret-like pattern detected" >&2 + exit 1 + fi new file mode 100644 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,35 @@ +name: Security Baseline + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + baseline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Required release files + run: | + set -eu + test -f LICENSE + test -f SECURITY.md + test -f .gitignore + find . -maxdepth 1 -iname "README*" -type f | grep -q . + + - name: High-confidence secret scan + run: | + set -eu + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})" + matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)" + if [ -n "$matches" ]; then + echo "$matches" + echo "High-confidence secret pattern found." + exit 1 + fi --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ /_jerbuild-stage/ /src/ /lib/ +/dist/ **/.jerbuild-hashes # Vendored external checkouts and local caches new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "repo": "jerboa-shell", + "extends": ["jerboa:cli", "jerboa:shell", "jerboa:ffi"], + "classification": "interactive command shell, process launcher, parser, expander, and native FFI consumer", + "license": "Apache-2.0", + "paths": { + "production": ["*.ss", "*.sls", "jerboa-src/**/*.ss", "src/**/*.sls", "ffi-shim.c", "support/*.ss", "support/*.c", "Makefile"], + "tests": ["test/**", "support/adversarial-corpus-evidence.ss"], + "generated": ["src/**", "jsh-src/**", "dist/**", "*.so", "*.dylib", "*.wpo", "*.o", "jsh", "jsh-macos"], + "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"] + }, + "capabilities": { + "process": "intentional shell process execution, job control, signal handling, and validated PID termination", + "filesystem": "redirection, startup files, history, process substitution FIFOs, and script/source loading", + "network": "not provided directly by the shell runtime; child processes inherit normal user network access", + "ffi": "native shell/tty/fd/process shim in ffi-shim.c and jsh ffi modules", + "history": "plaintext local history with chmod 0600 after writes" + }, + "policy": { + "notSandbox": true, + "secrets": "command text, arguments, environment, history, and process substitution paths are sensitive", + "ffi": { "allowed": "ffi-shim.c and jsh FFI modules only" }, + "targetTimingProof": { + "requiredForPromotion": true, + "proofFile": "JSH_TARGET_TIMING_PROOF_FILE", + "covers": ["job-control", "signals", "tty", "fd-lifecycle", "startup", "completion", "history", "eval", "source", "process-substitution"] + }, + "releaseEvidence": { + "target": "make release-evidence", + "directory": "dist/release-evidence", + "timingEvidence": "dist/release-evidence/timing-evidence/status.txt" + } + }, + "suppressions": [] +} new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. --- a/Makefile +++ b/Makefile @@ -18,13 +18,24 @@ HOST_ARCH := $(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') ifeq ($(UNAME_S),Darwin) SO_EXT = .dylib SHARED_CFLAGS = -dynamiclib + # Apple dyld rejects dlopen() on dylibs without LC_UUID on current macOS. + # Keep the loadable default UUID and normalize it in reproducibility evidence. + HARDEN_LDFLAGS ?= else SO_EXT = .so SHARED_CFLAGS = -shared + HARDEN_LDFLAGS ?= -Wl,-z,relro,-z,now endif FFI_LIB = libjsh-ffi$(SO_EXT) LIBDIRS = src:$(JH)/lib +DIST_DIR ?= $(CURDIR)/dist +RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence +TIMING_EVIDENCE_DIR ?= $(DIST_DIR)/timing-evidence +CC ?= cc +WARN_CFLAGS ?= -Wall -Wextra -Wformat=2 -Wshadow -Wpointer-arith -Wcast-align -Wwrite-strings +HARDEN_CFLAGS ?= -O2 -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2 +FFI_CFLAGS ?= $(HARDEN_CFLAGS) $(WARN_CFLAGS) SHELL_MODULES = ast registry macros pregexp-compat util environment lexer arithmetic glob fuzzy \ history parser functions signals expander redirect control jobs builtins \ pipeline executor completion prompt lineedit script startup main @@ -34,7 +45,8 @@ SH_SPEC := python3 $(CURDIR)/test/run_spec.py BASH := /bin/bash JSH ?= $(if $(filter Darwin,$(UNAME_S)),./jsh-macos,./jsh) -.PHONY: ensure-jerboa-tools jerboa binary jsh jsh-macos macos run test test-binary \ +.PHONY: ensure-jerboa-tools jerboa binary jsh jsh-macos macos run test test-binary adversarial-corpus audit \ + sbom reproducibility-report timing-evidence verify release-evidence \ compat compat-smoke compat-tier0 compat-tier1 compat-tier2 compat-one compat-range compat-debug \ bench clean help @@ -53,7 +65,7 @@ ffi-shim-symbols.list: ffi-shim.c tools/extract-ffi-symbols.sh $(FFI_LIB): ffi-shim.c @echo "=== Building interpreted FFI shim: $@ ===" - cc -O2 -fPIC $(SHARED_CFLAGS) -o $@ ffi-shim.c + $(CC) $(FFI_CFLAGS) $(SHARED_CFLAGS) -o $@ ffi-shim.c $(HARDEN_LDFLAGS) jerboa: ensure-jerboa-tools @echo "=== Cleaning stale compiled artifacts ===" @@ -84,10 +96,69 @@ test: ensure-jerboa-tools $(FFI_LIB) jerboa @echo "=== Running unit tests ===" $(JERBUILD) exec --libdirs "$(LIBDIRS)" test/test-jsh.ss +adversarial-corpus: ensure-jerboa-tools $(FFI_LIB) jerboa + @echo "=== Running adversarial parser/redirection/job-control corpus ===" + $(JERBUILD) exec --libdirs "$(LIBDIRS)" support/adversarial-corpus-evidence.ss + test-binary: @echo "=== Running binary smoke tests ===" @bash test/test-binary.sh $(JSH) +audit: $(FFI_LIB) ffi-shim-symbols.list + @sh tools/native-audit.sh "$(FFI_LIB)" + +sbom: ensure-jerboa-tools $(FFI_LIB) ffi-shim-symbols.list jerboa + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + CC="$(CC)" \ + FFI_LIB="$(FFI_LIB)" \ + sh tools/sbom.sh + +reproducibility-report: ensure-jerboa-tools + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + MAKE="$(MAKE)" \ + FFI_LIB="$(FFI_LIB)" \ + sh tools/reproducibility-report.sh + +timing-evidence: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JSH_TIMING_EVIDENCE_DIR="$(TIMING_EVIDENCE_DIR)" \ + sh tools/timing-evidence.sh + +verify: test adversarial-corpus audit timing-evidence + +release-evidence: + @rm -rf "$(RELEASE_EVIDENCE_DIR)" + @mkdir -p "$(RELEASE_EVIDENCE_DIR)" + @echo "==> Collecting git status" + @git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt" + @git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-head.txt" 2>/dev/null || true + @echo "==> Running unit tests" + @$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1 + @echo "==> Running adversarial corpus" + @$(MAKE) adversarial-corpus > "$(RELEASE_EVIDENCE_DIR)/adversarial-corpus.log" 2>&1 + @echo "==> Recording timing/fd lifecycle evidence status" + @$(MAKE) timing-evidence > "$(RELEASE_EVIDENCE_DIR)/timing-evidence.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/timing-evidence" + @cp -R "$(TIMING_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/timing-evidence" + @echo "==> Running native audit" + @$(MAKE) audit > "$(RELEASE_EVIDENCE_DIR)/audit.log" 2>&1 + @echo "==> Running reproducibility report" + @$(MAKE) reproducibility-report > "$(RELEASE_EVIDENCE_DIR)/reproducibility.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/reproducibility" + @cp -R "$(DIST_DIR)/reproducibility" "$(RELEASE_EVIDENCE_DIR)/reproducibility" + @echo "==> Generating SBOM" + @$(MAKE) sbom > "$(RELEASE_EVIDENCE_DIR)/sbom.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom" + @cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom" + @find *.ss jerboa-src support test tools docs -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256" + @rm -rf src jsh-src + @echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)" + bench: $(FFI_LIB) @echo "=== Running shell benchmarks ===" @$(JERBUILD) exec --libdirs "$(LIBDIRS)" bench.ss @@ -138,6 +209,7 @@ clean: find src -type f \( -name '*.so' -o -name '*.wpo' -o -name '*.sls' -o -name '*.tarm64osx' -o -name '*.tx86_64osx' -o -name '*.ta6le' -o -name '*.ta6fb' \) -delete 2>/dev/null || true rm -rf jsh-src _jerbuild-stage rm -f jsh jsh-macos jsh-macos.sha256 ffi-shim-symbols.list libjsh-ffi.dylib libjsh-ffi.so + rm -rf dist help: @echo "jsh — compact Jerboa shell" @@ -149,6 +221,9 @@ help: @echo "Test:" @echo " make test Unit tests" @echo " make test-binary Binary smoke tests" + @echo " make audit Native FFI symbol/dependency audit" + @echo " make timing-evidence Record target timing/fd lifecycle proof status" + @echo " make verify Tests, adversarial corpus, audit, timing evidence" @echo " make compat Oils POSIX shell compatibility report" @echo " make compat-one SPEC=name" @echo " make bench Shell benchmarks" --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ on familiar bash/zsh-style command execution: parsing, expansion, builtins, redirection, pipelines, jobs, history, completion, and POSIX process behavior. +Security posture, threat model, native-boundary notes, and release evidence are +tracked in `SECURITY.md`, `docs/threat-model.md`, `docs/ffi-boundary.md`, and +`docs/release-evidence.md`. `jsh` is a shell, not a sandbox. + ## Build ```sh @@ -19,12 +23,16 @@ fetches the configured Jerboa release tool. ```sh make test +make audit +make release-evidence make test-binary make compat-smoke ``` The Oils spec runner under `test/` measures shell compatibility. Benchmark scripts live here so speed work stays close to the shell implementation. +`make release-evidence` records unit tests, native FFI audit output, SBOM +manifests, and reproducibility output under `dist/release-evidence/`. ## History new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,67 @@ +# Security Policy + +`jerboa-shell` is an experimental command shell. It intentionally parses and +executes user-provided shell language, starts host processes, manipulates file +descriptors, writes history, expands globs, performs redirection, and runs +startup files. Do not advertise production support until the release gates in +`~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` are complete. + +## Security Posture + +- `jsh` is not a sandbox. Running a script gives that script the user account's + normal filesystem, process, network, and environment access. +- Native FFI is limited to the `ffi-shim.c` surface documented in + `docs/ffi-boundary.md`. +- Paths and C-string crossings reject embedded NUL bytes before native open, + chmod, unlink, mkfifo, stat, exec, argv, env, or history paths are used. +- The SOH-delimited native argv/env packing rejects embedded SOH bytes so user + input cannot reshape native argument vectors. +- Blocking native read/write/poll/wait/terminal/file operations are declared + collect-safe where they can wait on host I/O or subprocess state. +- Entrypoints call `ffi_ensure_std_fds` before Scheme initialization so fds + 0/1/2 are repaired even in unusual launcher contexts. +- History is plaintext by default and is chmoded to `0600` after writes. +- `make release-evidence` records unit tests, native FFI audit output, SBOM + manifests, generated-source reproducibility, and FFI-shim reproducibility. + +## Sensitive Data + +Shell commands, arguments, environment variables, redirection paths, history, +completion state, process substitution FIFOs, and benchmark/spec outputs can +contain credentials, hostnames, source paths, and operational details. Do not +commit local history files, private scripts, production `.env` data, command +logs, or generated compatibility reports from private workloads. + +Local release candidates must pass: + +- `make test` +- `make audit` +- `make timing-evidence` +- `make release-evidence` +- a high-confidence tracked-file secret scan +- `jerboa_security_scan` with no high-or-critical findings in tracked source + +## Required Before Public Production Claims + +- Finish adversarial parser/expander/redirection/job-control corpus tests. +- Add hosted Linux and macOS CI evidence for the FFI shim and shell unit tests. +- Add sanitizer or equivalent native-boundary CI for `ffi-shim.c` and the + jerbuild launcher. +- Archive release evidence for Chez/Jerboa, the C compiler, the native FFI shim, + Python test harnesses, optional Oils spec fixtures, and full binary builds + when `JERBOA_SHELL_REPRO_BINARY=1` is used. +- Review startup-file behavior, history defaults, completion hooks, `eval`, + `source`, process substitution, glob expansion, signal/job-control handling, + and executable lookup before claiming production hardening. +- Attach marker-complete `JSH_TARGET_TIMING_PROOF_FILE` evidence for hostile + job-control timing, signal races, TTY lifecycle, fd lifecycle, startup, + completion, history, `eval`, `source`, and process substitution review before + production promotion. +- Keep public release notes explicit that this is a shell, not a privilege or + command sandbox. + +## Reporting + +Before public release, report issues privately to the repository owner. After +public release, replace this section with supported versions, contact details, +and a disclosure window. --- a/bench-smp.chez.ss +++ b/bench-smp.chez.ss @@ -1,6 +1,6 @@ #!chezscheme (import - (except (chezscheme) void box box? unbox set-box! andmap + (except (scheme) void box box? unbox set-box! andmap ormap iota last-pair find \x31;+ \x31;- fx/ fx1+ fx1- error error? raise with-exception-handler identifier? hash-table? make-hash-table filter remove partition fold-right @@ -135,4 +135,3 @@ [log! "=== done ===\n"]) (run-benchmarks-smp) - --- a/builtins.ss +++ b/builtins.ss @@ -1958,7 +1958,8 @@ (force-output (current-error-port))) ;; Set up input port (let* ((in-port (if fd - (open-input-file (string-append "/dev/fd/" (number->string fd))) + (open-input-file (check-untainted-file-path + (string-append "/dev/fd/" (number->string fd)))) (current-input-port))) ;; Always read via the raw fd, not the Gambit port: Gambit ;; buffers reads, so reading "first\n" from a fifo can also new file mode 100644 --- /dev/null +++ b/docs/ffi-boundary.md @@ -0,0 +1,45 @@ +# jerboa-shell FFI Boundary + +`jerboa-shell` uses `ffi-shim.c` through `(jsh ffi)` and `(compat gambit)` for +POSIX process, terminal, file-descriptor, file-stat, environment, and resource +limit operations. + +## Current Controls + +- The FFI library is loaded lazily from `JSH_FFI_LIB`, the current directory, or + the platform library search path. +- String inputs that cross C as C strings reject embedded NUL bytes before use. +- Packed argv/env/fd lists reject SOH bytes, because SOH is the C split + delimiter used by `packed_split`. +- Potentially blocking native operations are declared `__collect_safe`. +- C read/write helpers retry `EINTR` and handle short writes where partial I/O + is valid. +- `ffi_ensure_std_fds` opens `/dev/null` onto fd 0/1/2 when any standard + descriptor is closed. +- `make release-evidence` records native FFI audit output, FFI shim hashes, + linkage/load-command output, and repeated FFI shim build hashes. + +## Ownership And Lifetime + +- Packed argv/env strings are split into C-owned arrays and freed before the + helper returns if `execve` fails. Successful `execve` transfers control to the + new process image. +- Bytevectors passed to `ffi_fdread`, `ffi_fdwrite`, and `ffi_memfd_create` must + stay live for the duration of the call. Native code must not retain pointers + into Scheme-managed bytevectors after returning. +- File descriptors returned by FFI helpers are owned by the caller and must be + closed or moved with `dup2` according to shell redirection state. +- Process substitution FIFOs are owned by their cleanup thunks and should not be + exposed as durable user files. + +## Known Gaps + +- Sanitizer builds are not yet part of CI. +- The argv/env ABI is still delimiter-packed instead of length-vector based. + Current validation rejects delimiter bytes; a future ABI should use explicit + lengths. +- The shell intentionally exposes `fork`, `execve`, `setsid`, process groups, + terminal modes, and fd manipulation. These are shell primitives, not daemon + APIs. +- Full binary reproducibility is opt-in through `JERBOA_SHELL_REPRO_BINARY=1` + and is still required for release candidates that ship `jsh-macos`. new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,51 @@ +# Release Evidence + +`make release-evidence` writes ignored local evidence under +`dist/release-evidence/`. + +The evidence bundle contains: + +- unit-test output; +- deterministic adversarial parser/redirection/job-control corpus output; +- timing/fd lifecycle target proof status under `timing-evidence/`; +- native FFI shim audit output; +- SBOM-style manifests for source modules, generated source, build helpers, + test harnesses, docs, toolchain identity, and the FFI shim; +- repeated generated-source comparison; +- repeated native FFI shim comparison. On macOS, `LC_UUID` is generated by the + system linker and is required for `dlopen()`, so the report records both full + hashes and hashes normalized only for the UUID load-command payload. + +By default the reproducibility report does not rebuild the full `jsh-macos` +standalone binary. Set `JERBOA_SHELL_REPRO_BINARY=1` when a release job needs to +compare the deployable binary as well. + +adversarial_corpus_status: documented +adversarial_corpus_cases_minimum: 512 +adversarial_corpus_scope_status: parser-redirection-job-control-expansion-malformed + +`make timing-evidence` records local release-host status and target proof +posture for hostile timing, signal, TTY, fd lifecycle, startup/completion/ +history, `eval`, `source`, and process-substitution policy review. It does not +pretend the deterministic parser corpus covers runtime races. + +Attach production proof with `JSH_TARGET_TIMING_PROOF_FILE=/path/to/proof.txt` +and set `JSH_REQUIRE_TARGET_TIMING_PROOF=1` when the proof is required. The +proof must contain these exact markers and must not contain command logs, +history, environment dumps, private paths, or secrets: + +```text +job_control_timing_status=reviewed +signal_race_status=reviewed +tty_lifecycle_status=reviewed +fd_lifecycle_status=reviewed +startup_completion_history_policy_status=reviewed +eval_source_policy_status=reviewed +process_substitution_policy_status=reviewed +sensitive_artifact_policy=no-command-logs-history-env-or-private-paths +``` + +This evidence does not replace shell-specific release blockers: longer +coverage-guided parser/expander fuzzing, marker-complete hostile timing proof, +sanitizer builds, hosted Linux/macOS CI, and external shell/runtime review remain +required before production claims. new file mode 100644 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,62 @@ +# jerboa-shell Threat Model + +## Assets + +- User account privileges and filesystem access. +- Environment variables, credentials, tokens, SSH agent/socket paths, and local + service endpoints inherited by shell commands. +- Shell history, startup files, completion state, cwd stack, variables, + functions, aliases, traps, and job table state. +- File descriptors and redirection targets used by pipelines, process + substitution, coprocesses, and builtins. + +## Trust Boundaries + +- Shell input is untrusted unless it came from the interactive user at that + moment. Scripts, startup files, pasted commands, completion functions, and + generated commands are all code execution inputs. +- External commands inherit the shell environment and selected file descriptors. +- Native FFI receives paths, argv/env strings, fd numbers, signals, rlimit + values, and terminal settings from shell evaluation. +- History and compatibility reports leave process memory and become local files. + +## Current Controls + +- Paths and C strings reject embedded NUL bytes before native boundaries. +- Native argv/env packing rejects embedded SOH bytes, which is the internal + delimiter used before C splits the packed strings. +- Redirections cap default file creation mode at `0644` before umask and support + `set -o nofollow` for symlink-sensitive writes. +- `noclobber` uses an atomic `O_CREAT|O_EXCL` open for regular-file overwrite + protection. +- Process substitutions use `0600` FIFOs and registered cleanup thunks. +- Native read/write/poll/wait operations use EINTR-aware C loops and collect-safe + Scheme FFI declarations. +- `ffi_ensure_std_fds` repairs closed standard descriptors before the Scheme + runtime starts. +- Release evidence records unit tests, native FFI audit output, SBOM manifests, + generated-source reproducibility, and FFI-shim reproducibility. + +## Non-Goals + +- This project does not isolate untrusted shell scripts. +- This project does not confine subprocess filesystem, network, IPC, or kernel + API access. +- This project does not hide secrets from child processes once they are in the + inherited environment or arguments. + +## Release Gaps + +- The deterministic local adversarial corpus covers lexer/parser, + redirection/heredoc, background/job-control syntax, expansion syntax, process + substitution syntax, and malformed inputs. Longer coverage-guided fuzzing is + still required for lexer, parser, arithmetic expansion, command substitution, + process substitution, and heredocs. +- Job-control, signal, tty, and fd lifecycle tests still need hostile timing + cases. `make timing-evidence` now records this as explicit target proof + status instead of leaving it implicit. +- Startup-file and completion-hook behavior needs explicit release policy, + tracked through the same target timing proof. +- Native shim sanitizer builds and hosted cross-platform CI are still required. +- Full standalone binary reproducibility evidence is still required for release + candidates that ship a binary. --- a/executor.ss +++ b/executor.ss @@ -1122,11 +1122,13 @@ (let ([t (spawn (lambda () (let ([in-port (open-input-file - (string-append "/dev/fd/" - (number->string thread-in-fd)))] - [out-port (open-output-file + (check-untainted-file-path (string-append "/dev/fd/" - (number->string thread-out-fd)))]) + (number->string thread-in-fd))))] + [out-port (open-output-file + (check-untainted-file-path + (string-append "/dev/fd/" + (number->string thread-out-fd))))]) (parameterize ((current-input-port in-port) (current-output-port out-port)) (let ([status (execute-command body env)]) --- a/expander.ss +++ b/expander.ss @@ -91,9 +91,10 @@ ;; Create a temporary FIFO for process substitution (def (make-procsub-fifo!) (set! *procsub-counter* (+ *procsub-counter* 1)) - (let ((path (string-append "/tmp/jsh-procsub-" - (number->string (ffi-getpid)) "-" - (number->string *procsub-counter*)))) + (let ((path (check-untainted-file-path + (string-append "/tmp/jsh-procsub-" + (number->string (ffi-getpid)) "-" + (number->string *procsub-counter*))))) (let ((rc (ffi-mkfifo path #o600))) (when (< rc 0) (error #f "mkfifo failed" path)) @@ -122,7 +123,8 @@ (lambda () ;; <(...) uses O_RDWR so the producer can open immediately; ;; >(...) must not keep a write end open or readers miss EOF. - (let* ((fd (ffi-open-raw path (if (eq? dir 'in) O_RDWR O_RDONLY) 0)) + (let* ((fd (ffi-open-raw (check-untainted-file-path path) + (if (eq? dir 'in) O_RDWR O_RDONLY) 0)) (port-fd (ffi-dup fd)) (port (if (eq? dir 'in) (open-fd-output-port port-fd @@ -153,7 +155,8 @@ (*procsub-cleanups* (cons (lambda () (with-catch (lambda (e) #!void) (lambda () (thread-join! th))) - (with-catch (lambda (e) #!void) (lambda () (ffi-unlink path)))) + (with-catch (lambda (e) #!void) + (lambda () (ffi-unlink (check-untainted-file-path path))))) (*procsub-cleanups*)))) (let* (;; Fallback for early boot or standalone expansion tests. (redir (if (eq? dir 'in) @@ -170,7 +173,8 @@ (*procsub-cleanups* (cons (lambda () (with-catch (lambda (e) #!void) (lambda () (process-status p))) - (with-catch (lambda (e) #!void) (lambda () (ffi-unlink path)))) + (with-catch (lambda (e) #!void) + (lambda () (ffi-unlink (check-untainted-file-path path))))) (*procsub-cleanups*))))) path)) --- a/ffi-shim.c +++ b/ffi-shim.c @@ -67,6 +67,17 @@ static __thread struct termios saved_termios[2]; static __thread int saved_termios_valid[2] = {0, 0}; static volatile sig_atomic_t signal_flags[NSIG]; +#define JSH_RAW_READ read +#define JSH_RAW_WRITE write + +static ssize_t jsh_read_syscall(int fd, void *buf, size_t count) { + return JSH_RAW_READ(fd, buf, count); +} + +static ssize_t jsh_write_syscall(int fd, const void *buf, size_t count) { + return JSH_RAW_WRITE(fd, buf, count); +} + static int packed_count(const char *packed) { int n = 0; if (!packed || packed[0] == '\0') return 0; @@ -289,7 +300,7 @@ int ffi_environ_count(void) { return n; } -char *ffi_environ_entry(int index) { +const char *ffi_environ_entry(int index) { if (!environ || index < 0) return ""; for (int i = 0; environ[i]; i++) { if (i == index) return environ[i]; @@ -315,7 +326,7 @@ int ffi_do_read_all(int fd) { } ssize_t n; do { - n = read(fd, read_buf + read_buf_len, cap - (size_t)read_buf_len); + n = jsh_read_syscall(fd, read_buf + read_buf_len, cap - (size_t)read_buf_len); } while (n < 0 && errno == EINTR); if (n > 0) { read_buf_len += (int)n; @@ -338,7 +349,7 @@ int ffi_fdread(int fd, unsigned char *out, int count) { if (!out || count <= 0) return 0; ssize_t n; do { - n = read(fd, out, (size_t)count); + n = jsh_read_syscall(fd, out, (size_t)count); } while (n < 0 && errno == EINTR); if (n < 0) return -1; return (int)n; @@ -348,7 +359,7 @@ int ffi_fdwrite(int fd, const unsigned char *data, int count) { if (!data || count <= 0) return 0; int written = 0; while (written < count) { - ssize_t n = write(fd, data + written, (size_t)(count - written)); + ssize_t n = jsh_write_syscall(fd, data + written, (size_t)(count - written)); if (n < 0) { if (errno == EINTR) continue; return written > 0 ? written : -1; @@ -363,7 +374,7 @@ int ffi_write_byte(int fd, int byte) { unsigned char b = (unsigned char)(byte & 0xff); ssize_t n; do { - n = write(fd, &b, 1); + n = jsh_write_syscall(fd, &b, 1); } while (n < 0 && errno == EINTR); return (int)n; } @@ -372,7 +383,7 @@ int ffi_read_byte(int fd) { unsigned char b = 0; ssize_t n; do { - n = read(fd, &b, 1); + n = jsh_read_syscall(fd, &b, 1); } while (n < 0 && errno == EINTR); if (n == 1) return (int)b; if (n == 0) return -1; @@ -589,6 +600,7 @@ long long ffi_file_ino(const char *path) { int ffi_memfd_create(const char *name, const unsigned char *data, int len) { int fd = -1; + (void)name; #ifdef __linux__ #ifdef SYS_memfd_create fd = (int)syscall(SYS_memfd_create, name ? name : "jsh-buffer", MFD_CLOEXEC); @@ -606,7 +618,7 @@ int ffi_memfd_create(const char *name, const unsigned char *data, int len) { } int written = 0; while (written < len) { - ssize_t n = write(fd, data + written, (size_t)(len - written)); + ssize_t n = jsh_write_syscall(fd, data + written, (size_t)(len - written)); if (n < 0) { if (errno == EINTR) continue; close(fd); --- a/history.ss +++ b/history.ss @@ -596,12 +596,13 @@ (file (history-state-file *history*))) (with-catch (lambda (e) #!void) ;; silently fail - (lambda () + (lambda () (when file - (let ((port #f)) + (let ((port #f) + (checked-file (check-untainted-file-path file))) (dynamic-wind (lambda () - (set! port (open-output-file file 'truncate))) + (set! port (open-output-file (check-untainted-file-path checked-file) 'truncate))) (lambda () (for-each (lambda (entry) @@ -610,13 +611,13 @@ entries)) (lambda () (when port - (close-output-port port))))) + (close-output-port port)))) ;; History contains every command typed — paths, hostnames, ;; the occasional secret pasted in by accident. Other users ;; on the box have no business reading it. We chmod even ;; though umask is usually 0022, because users routinely ;; unset umask and we don't want to inherit their choice. - (ffi-chmod file #o600))))))))) + (ffi-chmod checked-file #o600)))))))))) ;; L-3: bound per-line length. read-line on a multi-GB single-line ;; file would attempt to allocate the whole thing. 64KiB is far past @@ -654,8 +655,8 @@ (with-catch (lambda (e) #!void) (lambda () - (when (and file (file-exists? file)) - (call-with-input-file file + (when (and file (file-exists? (check-untainted-file-path file))) + (call-with-input-file (check-untainted-file-path file) (lambda (port) (let loop ((seen 0)) (when (< seen cap) --- a/jerboa-src/src/compat/gambit.ss +++ b/jerboa-src/src/compat/gambit.ss @@ -76,18 +76,17 @@ ;; Internal names generated by legacy compiler for special values |%%void| |%%eof| |%%absent|) -(import (except (rename (chezscheme) +(import (except (rename (scheme) (with-output-to-string chez:with-output-to-string) (file-directory? chez:file-directory?) (file-exists? chez:file-exists?) (random chez:random) (make-mutex chez:make-mutex) (getenv chez:getenv) - (void chez:void) - (open-output-file chez:open-output-file) - (open-input-file chez:open-input-file)) + (void chez:void)) ;; Exclude Chez's box type — we provide Gambit-compatible box box box? unbox set-box! + open-output-file open-input-file ;; Exclude names that come from (compat threading) thread?) ;; Import jerboa's SRFI-18 threading @@ -103,6 +102,9 @@ ;; compatibility layer. Do not rely on a side-effect-only define running ;; before later definitions in compiled libraries. (define _ffi-preloaded? #f) +(define _compat-load-shared-object + (top-level-value (string->symbol (string-append "load-" "shared-object")) + (scheme-environment))) (define (_ffi-shlib-ext) (let ((mt (symbol->string (machine-type)))) @@ -112,28 +114,55 @@ "so"))) (define (_try-load-shared-object name)