Security hardening and release readiness

ober

e151b0da7cff7221017d0be8b1e178ffd944ff40

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6f8b545
--- /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
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..28a713e
--- /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
diff --git a/.gitignore b/.gitignore
index f002dc2..bcb695b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
 *.so
+*.dylib
 *.wpo
 .jerbuild-hashes
+.jerboa/*
+!.jerboa/
+!.jerboa/security.json
+scintilla-vendor/
+dist/
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..ac500fe
--- /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": []
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /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.
diff --git a/Makefile b/Makefile
index 2d44d7d..af68698 100644
--- 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
diff --git a/README.md b/README.md
index 9ecedf1..47b89f8 100644
--- 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.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..c587564
--- /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.
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
new file mode 100644
index 0000000..e22670f
--- /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.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..e0056c4
--- /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.
diff --git a/docs/threat-model.md b/docs/threat-model.md
new file mode 100644
index 0000000..277c424
--- /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.
diff --git a/docs/vendor-provenance.md b/docs/vendor-provenance.md
new file mode 100644
index 0000000..037588d
--- /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.
diff --git a/jerboa_scintilla_shim.c b/jerboa_scintilla_shim.c
index c6a5f88..22253a3 100644
--- 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,