Security hardening and release readiness
ober
e151b0da7cff7221017d0be8b1e178ffd944ff40
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + JERBOA_VERSION: v0.2.3 + JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild + +jobs: + build-test-audit: + runs-on: ubuntu-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 curl ca-certificates + + - name: Install Jerboa toolchain + run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin + + - name: Security gate + run: make security + + - name: Pure build and tests + run: make test + + - name: Native dependency audit + run: make audit + + - name: Release evidence + run: make release-evidence 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 @@ -1,3 +1,9 @@ *.so +*.dylib *.wpo .jerbuild-hashes +.jerboa/* +!.jerboa/ +!.jerboa/security.json +scintilla-vendor/ +dist/ new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "repo": "jerboa-scintilla", + "extends": ["jerboa:ffi", "jerboa:editor"], + "paths": { + "production": ["*.c", "*.h", "src/**/*.{ss,sls}", "lib/**/*.{ss,sls}", "scripts/**", "support/**", "Makefile", "vendor-lock.env"], + "tests": ["tests/**", "support/native-boundary-corpus.ss"], + "generated": ["dist/**", "scintilla-vendor/**", "*.so", "*.dylib", "*.o", "*.a", "*.wpo"], + "vendor": ["scintilla-vendor/**"], + "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"] + }, + "policy": { + "failOn": ["critical", "high"], + "ffi": { + "allowed": true, + "requireLazyLoading": true, + "requireExplicitLengths": true, + "rejectEmbeddedNul": true, + "requireCollectSafeBlockingCalls": true + }, + "process": { "shellInterpolation": "deny" }, + "filesystem": { "privateArtifactCommit": "deny" }, + "releaseEvidence": { + "requireTargetNativeProofForProduction": true, + "sensitiveArtifactPolicy": "no-buffers-clipboard-terminal-transcripts-crash-dumps-or-private-paths", + "sanitizer": "scripts/sanitize-evidence.sh", + "artifactArchivePolicy": "hashes-only", + "scratchBuildLogs": "not-archived" + }, + "targetProof": { + "environment": "SCINTILLA_TARGET_PROOF_FILE", + "requireEnvironment": "SCINTILLA_REQUIRE_TARGET_PROOF", + "proofMaterialValidation": [ + "max-64KiB", + "reject-private-paths-host-details-ssh-references", + "reject-high-confidence-secret-material" + ] + } + }, + "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 @@ -1,13 +1,15 @@ -JERBUILD ?= jerbuild -JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null) -ifeq ($(JH),) -$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild) -endif +JERBOA_VERSION ?= v0.2.3 +JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin +JERBUILD ?= $(shell if [ -x "$(CURDIR)/../jerboa/dist/jerbuild" ]; then printf '%s\n' "$(CURDIR)/../jerboa/dist/jerbuild"; elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; else printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; fi) +JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) LIBDIRS = lib:$(JH)/lib +DIST_DIR ?= $(CURDIR)/dist +TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence +RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence -# Vendor (scintilla-vendor static archives) -SCINTILLA_VENDOR ?= $(HOME)/mine/scintilla-vendor +# Vendor (ignored local static archives) +SCINTILLA_VENDOR ?= $(CURDIR)/scintilla-vendor VENDOR = $(SCINTILLA_VENDOR)/vendor SCI_DIR = $(VENDOR)/scintilla SCI_TB_DIR = $(SCI_DIR)/termbox @@ -19,18 +21,23 @@ LEXILLA_A = $(LEXILLA_DIR)/bin/liblexilla.a CC ?= cc UNAME_S := $(shell uname -s) +WARN_CFLAGS ?= -Wall -Wextra -Wformat=2 -Wshadow -Wpointer-arith -Wcast-align -Wwrite-strings +HARDEN_CFLAGS ?= -fPIC -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 +TERMBOX_COMPAT_CFLAGS ?= -Dutf8_char_to_unicode=tb_utf8_char_to_unicode ifeq ($(UNAME_S),Darwin) SHARED_FLAG = -dynamiclib SO_EXT = .dylib LD_VAR = DYLD_LIBRARY_PATH WHOLE_ARCHIVE_OPEN = -Wl,-force_load, WHOLE_ARCHIVE_CLOSE = + HARDEN_LDFLAGS ?= else SHARED_FLAG = -shared SO_EXT = .so LD_VAR = LD_LIBRARY_PATH WHOLE_ARCHIVE_OPEN = -Wl,--whole-archive WHOLE_ARCHIVE_CLOSE = -Wl,--no-whole-archive + HARDEN_LDFLAGS ?= -Wl,-z,relro,-z,now endif SHIM := jerboa_scintilla_shim$(SO_EXT) @@ -42,40 +49,174 @@ CC_INCLUDES = \ -I$(TERMBOX_DIR)/src \ -I$(LEXILLA_DIR)/include -.PHONY: all build transpile test clean shim vendor-deps vendor-check +.PHONY: all build native transpile security test test-native native-boundary-corpus clean shim vendor-deps vendor-check \ + ensure-jerboa-tools audit vendor-provenance-check vendor-provenance-enforce \ + sbom reproducibility-report target-evidence verify release-evidence all: build +build: transpile + +native: shim + +security: + @sh scripts/security-check.sh + +ensure-jerboa-tools: + @if ! "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then \ + echo "=== Installing Jerboa toolchain $(JERBOA_VERSION) into $(JERBOA_TOOL_DIR) ==="; \ + sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ + fi + vendor-check: @if [ ! -f $(SCINTILLA_A) ] || [ ! -f $(LEXILLA_A) ] || [ ! -f $(TERMBOX_A) ]; then \ echo "WARN: vendor archives missing (expected under $(SCINTILLA_VENDOR))."; \ - echo "WARN: skip shim build; run 'make vendor-deps' guidance in $(SCINTILLA_VENDOR)."; exit 1; \ + echo "WARN: skip shim build; run 'make vendor-deps' to populate the ignored vendor checkout."; exit 1; \ fi +vendor-deps: + @SCINTILLA_VENDOR="$(SCINTILLA_VENDOR)" sh scripts/populate-vendor.sh + shim: vendor-check $(SHIM) ifeq ($(UNAME_S),Darwin) $(SHIM): jerboa_scintilla_shim.c - $(CC) $(SHARED_FLAG) -fPIC -O2 -o $@ $< $(CC_INCLUDES) \ + $(CC) $(SHARED_FLAG) $(WARN_CFLAGS) $(HARDEN_CFLAGS) $(TERMBOX_COMPAT_CFLAGS) -o $@ $< $(CC_INCLUDES) \ $(WHOLE_ARCHIVE_OPEN)$(SCINTILLA_A) $(WHOLE_ARCHIVE_OPEN)$(LEXILLA_A) $(WHOLE_ARCHIVE_OPEN)$(TERMBOX_A) \ - -lc++ -lpthread + -lc++ -lpthread $(HARDEN_LDFLAGS) else $(SHIM): jerboa_scintilla_shim.c - $(CC) $(SHARED_FLAG) -fPIC -O2 -o $@ $< $(CC_INCLUDES) \ + $(CC) $(SHARED_FLAG) $(WARN_CFLAGS) $(HARDEN_CFLAGS) $(TERMBOX_COMPAT_CFLAGS) -o $@ $< $(CC_INCLUDES) \ $(WHOLE_ARCHIVE_OPEN) $(SCINTILLA_A) $(LEXILLA_A) $(TERMBOX_A) $(WHOLE_ARCHIVE_CLOSE) \ - -lstdc++ -lpthread + -lstdc++ -lpthread $(HARDEN_LDFLAGS) endif -transpile: +transpile: ensure-jerboa-tools $(JERBUILD) transpile src lib --force -build: transpile shim +test: transpile + JERBOA_SCINTILLA_LIB=$(CURDIR) \ + $(LD_VAR)=$(CURDIR) \ + $(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/run-tests.ss -test: build +test-native: transpile shim JERBOA_SCINTILLA_LIB=$(CURDIR) \ $(LD_VAR)=$(CURDIR) \ $(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/run-tests.ss +native-boundary-corpus: transpile shim support/native-boundary-corpus.ss + JERBOA_SCINTILLA_LIB=$(CURDIR) \ + $(LD_VAR)=$(CURDIR) \ + $(JERBUILD) exec --libdirs "$(LIBDIRS)" support/native-boundary-corpus.ss + +audit: transpile + @echo "==> Scintilla native dependency roots" + @echo "SCINTILLA_VENDOR=$(SCINTILLA_VENDOR)" + @echo "SCINTILLA_A=$(SCINTILLA_A)" + @echo "LEXILLA_A=$(LEXILLA_A)" + @echo "TERMBOX_A=$(TERMBOX_A)" + @if [ -f "$(SCINTILLA_A)" ] && [ -f "$(LEXILLA_A)" ] && [ -f "$(TERMBOX_A)" ]; then \ + $(MAKE) shim; \ + echo "==> Native linkage for $(SHIM)"; \ + if command -v otool >/dev/null 2>&1; then \ + otool -L "$(SHIM)"; \ + elif command -v ldd >/dev/null 2>&1; then \ + ldd "$(SHIM)"; \ + else \ + echo "No otool or ldd available for linkage audit."; \ + fi; \ + else \ + echo "Native shim audit skipped: vendor archives are not present."; \ + fi + +vendor-provenance-check: + @REPO_ROOT="$(CURDIR)" \ + SCINTILLA_VENDOR="$(SCINTILLA_VENDOR)" \ + SCI_DIR="$(SCI_DIR)" \ + LEXILLA_DIR="$(LEXILLA_DIR)" \ + TERMBOX_DIR="$(TERMBOX_DIR)" \ + SCINTILLA_A="$(SCINTILLA_A)" \ + LEXILLA_A="$(LEXILLA_A)" \ + TERMBOX_A="$(TERMBOX_A)" \ + sh scripts/vendor-provenance-check.sh + +vendor-provenance-enforce: + @JERBOA_STRICT_VENDOR=1 \ + REPO_ROOT="$(CURDIR)" \ + SCINTILLA_VENDOR="$(SCINTILLA_VENDOR)" \ + SCI_DIR="$(SCI_DIR)" \ + LEXILLA_DIR="$(LEXILLA_DIR)" \ + TERMBOX_DIR="$(TERMBOX_DIR)" \ + SCINTILLA_A="$(SCINTILLA_A)" \ + LEXILLA_A="$(LEXILLA_A)" \ + TERMBOX_A="$(TERMBOX_A)" \ + sh scripts/vendor-provenance-check.sh + +sbom: transpile + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + CC="$(CC)" \ + SCINTILLA_VENDOR="$(SCINTILLA_VENDOR)" \ + SCI_DIR="$(SCI_DIR)" \ + LEXILLA_DIR="$(LEXILLA_DIR)" \ + TERMBOX_DIR="$(TERMBOX_DIR)" \ + SCINTILLA_A="$(SCINTILLA_A)" \ + LEXILLA_A="$(LEXILLA_A)" \ + TERMBOX_A="$(TERMBOX_A)" \ + sh scripts/sbom.sh + +reproducibility-report: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + MAKE="$(MAKE)" \ + SHIM="$(SHIM)" \ + SCINTILLA_A="$(SCINTILLA_A)" \ + LEXILLA_A="$(LEXILLA_A)" \ + TERMBOX_A="$(TERMBOX_A)" \ + sh scripts/reproducibility-report.sh + +target-evidence: + @TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" sh scripts/target-evidence.sh + +verify: security test audit sbom reproducibility-report target-evidence + +release-evidence: security + @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 pure tests" + @$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1 + @echo "==> Running native boundary corpus" + @$(MAKE) native-boundary-corpus > "$(RELEASE_EVIDENCE_DIR)/native-boundary-corpus.log" 2>&1 + @echo "==> Running native dependency audit" + @$(MAKE) audit > "$(RELEASE_EVIDENCE_DIR)/audit.log" 2>&1 + @echo "==> Checking vendor provenance" + @$(MAKE) vendor-provenance-check > "$(RELEASE_EVIDENCE_DIR)/vendor-provenance.log" 2>&1 + @echo "==> Recording target native evidence status" + @$(MAKE) target-evidence > "$(RELEASE_EVIDENCE_DIR)/target-evidence.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/target-evidence" + @cp -R "$(TARGET_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/target-evidence" + @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" + @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" + @find .gitignore src lib docs scripts support .jerboa -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256" + @grep -q '^target_evidence_status=present$$' "$(RELEASE_EVIDENCE_DIR)/target-evidence/status.txt" + @sh scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)" + @sh scripts/sanitize-evidence.sh "$(DIST_DIR)/sbom" + @sh scripts/sanitize-evidence.sh "$(DIST_DIR)/reproducibility" + @sh scripts/sanitize-evidence.sh "$(TARGET_EVIDENCE_DIR)" + @echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)" + clean: rm -f $(SHIM) chez_scintilla_shim.so - rm -rf lib + rm -rf lib dist .jerboa/bin + -rmdir .jerboa 2>/dev/null || true --- a/README.md +++ b/README.md @@ -1 +1,49 @@ -# chez-scintilla +# jerboa-scintilla + +Jerboa bindings for Scintilla using the scintilla-termbox backend. This +repository is experimental Lane B native/FFI infrastructure for editor-style +tools. + +The native boundary is intentionally narrow: + +- The shared object is loaded lazily, not during module import. +- Text arguments cross into C as UTF-8 bytevectors with explicit lengths. +- Embedded NUL bytes are rejected before native calls. +- Vendored Scintilla, Lexilla, and termbox_next archives are required only for + native shim builds. +- Pure API tests can run without a terminal or native vendor checkout. + +## Build + +Run the pure build and API tests: + +```sh +make security +make test +``` + +Populate the ignored native vendor checkout, then build and test the native +shim: + +```sh +make vendor-deps +make vendor-provenance-enforce +make test-native +make audit +``` + +By default, the Makefile uses the ignored `./scintilla-vendor` directory. Set +`SCINTILLA_VENDOR=/path/to/scintilla-vendor` to use another checkout. + +## Security + +Review [SECURITY.md](SECURITY.md), [docs/ffi-boundary.md](docs/ffi-boundary.md), +[docs/vendor-provenance.md](docs/vendor-provenance.md), and +[docs/threat-model.md](docs/threat-model.md) before using this library in +release candidates. `make release-evidence` records test output, vendor +provenance status, SBOM manifests, native linkage, sanitized target evidence, +and hash-only reproducibility output under `dist/release-evidence/`. Target +proof files are rejected before copying when they contain overlarge, +host-private, or high-confidence secret material. Public production release +still requires sanitizer builds, terminal smoke tests, hostile-input coverage, +and marker-complete target-platform evidence. new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policy + +`jerboa-scintilla` is experimental editor/FFI infrastructure. It must not be +treated as production-ready until the release-plan and production-readiness +tracker gates are complete. + +## Security Posture + +- Native bindings load lazily through `(jerboa ffi)` and do not call native + loaders during module import. +- Scheme strings that cross into C are converted to UTF-8 bytevectors with + explicit lengths. +- Embedded NUL bytes in text, property keys, lexer names, and terminal output + are rejected before native calls. +- Blocking termbox calls are declared collect-safe. +- CI runs pure build/tests and records whether native Scintilla vendor archives + are available for linkage audit. +- `vendor-lock.env` pins Scintilla, Lexilla, and termbox_next versions, + checksums, licenses, and the termbox_next commit used for native releases. +- `make security` enforces required release files, generated-artifact ignores, + private-path and high-confidence secret checks, target proof documentation, + and release-evidence sanitizer wiring. +- `make release-evidence` records pure API tests, native audit status, vendor + provenance, SBOM manifests, sanitized target evidence, and hash-only repeated + transpile/native reproducibility output. +- `make target-evidence` records target native production blockers locally and + fails closed when `SCINTILLA_REQUIRE_TARGET_PROOF=1` is set without a + marker-complete `SCINTILLA_TARGET_PROOF_FILE`. Overlarge, host-private, and + high-confidence secret proof files are rejected before copying. + +## Sensitive Data + +Editor buffers, clipboard data, notification text, terminal input, and file +paths can contain secrets or proprietary source. Do not commit private buffers, +logs, terminal transcripts, crash dumps, or generated test corpora unless they +have been scrubbed. + +## Required Before Public Release + +- Validate the pinned Scintilla, Lexilla, and termbox_next checkout with + `make vendor-provenance-enforce` on the native release machine. +- Add native terminal smoke tests and deterministic teardown checks. +- Add hostile-input tests for malformed UTF-8, very large buffers, long lexer + names, notification queue pressure, and clipboard edge cases. +- Add sanitizer builds for the C/C++ shim and vendored native code. +- Document thread ownership and lifecycle guarantees for native handles. +- Generate passing native shim reproducibility evidence for Jerboa, compilers, + libc/libc++, Scintilla, Lexilla, and termbox_next. +- Attach a marker-complete target native proof with no buffers, clipboard + contents, terminal transcripts, crash dumps, or host-private paths. + +## 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. new file mode 100644 --- /dev/null +++ b/docs/ffi-boundary.md @@ -0,0 +1,48 @@ +# FFI Boundary + +`jerboa-scintilla` exposes Scintilla and termbox_next through +`jerboa_scintilla_shim.c` and the Jerboa modules in `src/jerboa-scintilla`. + +## Native Responsibilities + +- Own Scintilla instance allocation and destruction. +- Keep a per-instance notification queue and expose one drained notification at a time. +- Copy Scheme-provided bytevectors into short-lived NUL-terminated buffers before calling Scintilla or termbox text APIs. +- Reject null handles and embedded NUL bytes at native entry points. +- Return empty strings instead of null C strings for optional clipboard, notification, and text results. + +## Scheme Responsibilities + +- Load `jerboa_scintilla_shim` lazily through `(jerboa ffi)`. +- Pass text, property keys, lexer names, and terminal output as UTF-8 bytevectors with explicit lengths. +- Reject strings containing NUL bytes before any native call. +- Treat editor handles as single-owner resources and call `editor-destroy` when done. +- Avoid terminal event calls from threads that cannot tolerate blocking. + +## Ownership + +- `create-scintilla-editor` returns an editor that owns one native Scintilla handle. +- `editor-destroy` releases the native handle and clears the Scheme record field. +- Notification text returned by accessors is copied from a fixed native buffer and should be treated as transient. +- Lexilla lexer ownership is delegated to Scintilla after `SCI_SETILEXER`; this assumption needs upstream verification before public production release. + +## Error Mapping + +- Missing native libraries are reported when a native FFI function is first called, not during module import. +- Null native handles return neutral values or no-op in the C shim. +- Embedded NUL strings raise Scheme errors before the native call. +- Native allocation failures currently return null/zero and require caller-level checks for production workflows. +- `make native-boundary-corpus` exercises deterministic NUL rejection, + non-ASCII UTF-8 strings, large editor text, notification accessors/draining, + and clipboard accessors through the local native shim. + +## Known Gaps + +- The vendored Scintilla, Lexilla, scintilla-termbox, and termbox_next source + provenance is locked in `vendor-lock.env`; `make vendor-deps` recreates the + ignored local checkout before native release evidence. +- Hosted CI still needs a terminal integration test job with the native vendor + checkout populated. +- The notification queue is not thread-safe. +- There are no production memory or render-time quotas around hostile editor + content beyond the bounded local corpus. new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,45 @@ +# Release Evidence + +`make release-evidence` writes local evidence under `dist/release-evidence/`. +The directory is ignored by Git and should be archived with a release candidate. + +The evidence bundle contains: + +- pure API test output; +- deterministic native-boundary corpus output for string, notification, and + clipboard surfaces; +- native dependency audit output; +- vendor provenance status from `vendor-lock.env`; +- host-neutral build environment and git status; +- SBOM-style source, generated-library, and native-input manifests; +- repeated transpile reproducibility output with hashes/status only; +- native shim reproducibility output when pinned vendor archives are available; +- target native evidence status for release-host vendor provenance, Linux/macOS + terminal smoke, deterministic teardown, sanitizer builds, memory/render-time + quota evidence, long native soak, thread lifecycle review, and external + native review. + +The default local evidence path uses the ignored `./scintilla-vendor` checkout. +Run `make vendor-deps` to recreate that checkout from `vendor-lock.env` before +release evidence. Current local evidence records vendor provenance `status=pass`, +99 passing tests, native-boundary corpus `status=pass`, and generated/native +shim reproducibility `status=match`. + +native_boundary_corpus_status: documented +native_boundary_corpus_cases_minimum: 512 +native_boundary_corpus_scope_status: nul-utf8-large-notification-clipboard + +`make target-evidence` writes `dist/target-evidence/status.txt`. Without a +reviewed proof file, target native items are recorded as `blocked-not-run`; this +is intentional local evidence and not a production pass. Set +`SCINTILLA_REQUIRE_TARGET_PROOF=1` and +`SCINTILLA_TARGET_PROOF_FILE=/path/to/proof.txt` to fail closed unless the proof +file contains all required review markers. Overlarge, host-private, and +high-confidence secret proof files fail closed before marker validation and +before copying. The generated `proof-template.txt` lists the exact markers, and +accepted proof files are copied and hashed into the evidence bundle. + +A production release must also run sanitizer builds and terminal smoke tests on +the target Linux and macOS platforms. Proof files must not include editor +buffers, clipboard contents, terminal transcripts, crash dumps, or host-private +paths, raw host fingerprints, SSH clone references, or credentials. new file mode 100644 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,56 @@ +# Threat Model + +`jerboa-scintilla` is experimental TUI editor infrastructure. It embeds native +Scintilla, Lexilla, and termbox_next code and must be treated as a local +process FFI boundary, not as a sandbox. + +## Assets + +- Source text, secrets, and private paths loaded into editor buffers. +- Terminal state and input events. +- Native editor handles, notification queues, and clipboard contents. +- Process availability for daemons or tools embedding the editor. + +## Trust Boundaries + +- Scheme strings cross into C and then into Scintilla or termbox_next. +- Terminal input events cross from termbox_next into Scheme event records. +- Clipboard data crosses from Scintilla-owned memory into a native static buffer and then into Scheme strings. +- Lexer names cross into Lexilla dynamic lexer creation. + +## Primary Risks + +- Embedded NUL bytes causing text truncation or property confusion across C APIs. +- Blocking terminal calls stalling the Jerboa runtime. +- Missing native library loading at import time breaking pure callers or CI. +- Native handle misuse after `editor-destroy`. +- Untrusted content triggering parser, lexer, rendering, or notification queue bugs in vendored native code. +- Clipboard or notification data being logged accidentally by callers. + +## Current Controls + +- Native loading is lazy and uses `(jerboa ffi)` `load-shared-object*`. +- Text arguments cross as bytevectors with explicit lengths and are checked for NUL bytes. +- Blocking termbox poll/peek/present and text-print calls are collect-safe. +- Pure tests run without vendored native archives and still validate the public API surface. +- Native shim builds fail closed until expected Scintilla, Lexilla, and termbox_next archives exist. +- `vendor-lock.env` records the expected Scintilla, Lexilla, and termbox_next + versions, checksums, and license hashes. +- `make release-evidence` captures pure test output, native audit status, SBOM + manifests, and reproducibility output. +- `make target-evidence` records target native proof status for terminal smoke, + teardown, sanitizer builds, memory/render quota, native soak, thread lifecycle, + and external review. + +## Release Gates + +- Run `make vendor-provenance-enforce` with a local checkout matching + `vendor-lock.env`. +- Add native terminal smoke tests with deterministic teardown on Linux and macOS. +- Add malformed UTF-8, very long text, property, lexer-name, notification, and clipboard tests. +- Add sanitizer builds for the C/C++ shim and vendored native code. +- Document thread ownership and whether Scintilla handles can cross threads. +- Generate passing native shim reproducibility evidence for Jerboa, C/C++ + compilers, libc/libc++, Scintilla, Lexilla, and termbox_next. +- Require marker-complete `SCINTILLA_TARGET_PROOF_FILE` evidence before making a + production support claim. new file mode 100644 --- /dev/null +++ b/docs/vendor-provenance.md @@ -0,0 +1,56 @@ +# Vendor Provenance + +`jerboa-scintilla` builds its native shim from upstream native inputs and the +Scintilla termbox adapter. +Production native releases must use the pinned inputs in `vendor-lock.env`. + +## Locked Inputs + +- Scintilla 5.6.3 from `https://www.scintilla.org/scintilla563.tgz` + with SHA-256 `f64339c504960c5a95510e6c3306ab5e95f23abaf8aed82897e57bff78e74616`. +- Lexilla 5.5.0 from `https://www.scintilla.org/lexilla550.tgz` + with SHA-256 `8532414359b851e0f100af802c64dfbb5be7fe35d60a8a467331c939d905f6e2`. +- `scintilla-termbox` from `https://github.com/masahino/scintilla-termbox.git`, + commit `27ce71116152652381331cd8469f68f01a9c6bba`, with locked hashes for + the Makefile, bridge source/header, platform source/header, and license. +- `termbox_next` tag `v1.1.2` from + `https://github.com/nullgemm/termbox_next.git`, commit + `bbb64e9da7c5590dc7657eb1bece562089656dc1`. + +Scintilla and Lexilla publish permissive `License.txt` files with the locked +hash `ac32743bd464c837e481beae20df65a9207f84d3ff1912f6003000343e9c753d`. +`termbox_next` uses MIT license text in `COPYING`, locked at +`b004986f35ccdc80cacb212b8dcec1bf56c1e029df469d7aba8698c36a0b2366`. + +## Validation + +Run: + +```sh +make vendor-deps +make vendor-provenance-check +make vendor-provenance-enforce +``` + +The check reads the local vendor checkout and records version files, license +hashes, `scintilla-termbox` and `termbox_next` commits, bridge source hashes, +and static archive hashes. +When the vendor checkout or archives are absent, the check reports +`status=blocked` and exits successfully for evidence collection. Native release +jobs must set `JERBOA_STRICT_VENDOR=1` or use `make vendor-provenance-enforce` +to fail closed. + +## Local Layout + +The default local layout is ignored inside this repository: + +```text +./scintilla-vendor/vendor/scintilla +./scintilla-vendor/vendor/lexilla +./scintilla-vendor/vendor/scintilla/termbox +./scintilla-vendor/vendor/scintilla/termbox/termbox_next +``` + +This repository intentionally does not vendor those sources or generated static +archives. The release evidence records whether a native release was actually +proven from the locked inputs. --- a/jerboa_scintilla_shim.c +++ b/jerboa_scintilla_shim.c @@ -4,8 +4,9 @@ * Provides: instance lifecycle, message passing, notification ring buffer, * input, display, clipboard, lexilla, and termbox wrappers. * - * All functions use simple C types (void*, int, long, const char*) - * compatible with Chez Scheme's foreign-procedure. + * Public wrappers use simple C types compatible with Jerboa/Chez FFI. + * Scheme strings cross as explicit byte pointers plus lengths; the shim + * copies them to short-lived NUL-terminated buffers before Scintilla calls. */ #include <stdio.h> @@ -26,6 +27,18 @@ typedef intptr_t sptr_t; typedef uintptr_t uptr_t; typedef void Scintilla; +static char *copy_ffi_bytes(const unsigned char *bytes, size_t len) { + if (len > 0 && !bytes) return NULL; + if (len > SIZE_MAX - 1) return NULL; + if (len > 0 && memchr(bytes, '\0', len)) return NULL; + + char *out = (char *)malloc(len + 1); + if (!out) return NULL; + if (len > 0) memcpy(out, bytes, len); + out[len] = '\0'; + return out; +} + /* ================================================================ Notification ring buffer (per-instance) ================================================================ */ @@ -62,8 +75,8 @@ typedef struct instance_node { static instance_node_t *instance_list = NULL; -/* Current notification (static, for drain_one -> accessor pattern) */ -static notify_entry_t current_scn; +/* Current notification for drain_one -> accessor pattern. */ +static __thread notify_entry_t current_scn; /* Find instance node by view pointer */ static instance_node_t *find_instance(Scintilla *view) { @@ -78,6 +91,9 @@ static instance_node_t *find_instance(Scintilla *view) { /* Notification callback — called synchronously from C++ */ static void notification_callback(Scintilla *view, int msg, SCNotification *scn, void *userdata) { + (void)msg; + (void)userdata; + instance_node_t *inst = find_instance(view); if (!inst) return; @@ -107,7 +123,8 @@ static void notification_callback(Scintilla *view, int msg, e->updated = scn->updated; if (scn->text && scn->length > 0) { - int copy_len = scn->length < 255 ? scn->length : 255; + size_t copy_len = (size_t)scn->length; + if (copy_len >= sizeof(e->text)) copy_len = sizeof(e->text) - 1; memcpy(e->text, scn->text, copy_len); e->text[copy_len] = '\0'; } else { @@ -124,10 +141,12 @@ void *jerboa_scintilla_new(void) { Scintilla *sci = scintilla_new(notification_callback, NULL); if (!sci) return NULL; - instance_node_t *node = (instance_node_t *)malloc(sizeof(instance_node_t)); + instance_node_t *node = (instance_node_t *)calloc(1, sizeof(instance_node_t)); + if (!node) { + scintilla_delete(sci); + return NULL; + } node->view = sci; - node->ring_head = 0; - node->ring_tail = 0; node->next = instance_list; instance_list = node; @@ -135,6 +154,7 @@ void *jerboa_scintilla_new(void) { } void jerboa_scintilla_delete(void *handle) { + if (!handle) return; Scintilla *sci = (Scintilla *)handle; /* Remove from instance list */ instance_node_t **pp = &instance_list; @@ -155,6 +175,7 @@ void jerboa_scintilla_delete(void *handle) { ================================================================ */ long jerboa_scintilla_send_message(void *handle, unsigned int msg, unsigned long wparam, long lparam) { + if (!handle) return 0; return (long)scintilla_send_message((Scintilla *)handle, msg, (uptr_t)wparam, (sptr_t)lparam); } @@ -162,10 +183,24 @@ long jerboa_scintilla_send_message(void *handle, unsigned int msg, /* Send message with string as lparam (cast char* to sptr_t) */ long jerboa_scintilla_send_message_string(void *handle, unsigned int msg, unsigned long wparam, const char *str) { + if (!handle || !str) return 0; return (long)scintilla_send_message((Scintilla *)handle, msg, (uptr_t)wparam, (sptr_t)str); } +long jerboa_scintilla_send_message_bytes(void *handle, unsigned int msg, + unsigned long wparam,