Security hardening and release readiness

ober

0d3109a90cecb723f1ff21a648298b48d74b0f61

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3433cbe
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+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 pkg-config libpcre2-dev
+
+      - name: Install Jerboa toolchain
+        run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
+
+      - name: Test
+        run: make test
+
+      - name: Audit PCRE2 linkage
+        run: make audit
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 5381726..d2a55ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,7 @@ lib/
 *.wpo
 *.o
 .jerbuild-hashes
+.jerboa/*
+!.jerboa/
+!.jerboa/security.json
+dist/
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..6495eab
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,39 @@
+{
+  "repository": "jerboa-pcre2",
+  "riskClass": "native-ffi-hostile-input",
+  "status": "experimental",
+  "releaseEvidence": {
+    "requiredTargets": [
+      "security",
+      "test",
+      "hostile-regex-corpus",
+      "audit",
+      "pcre2-advisory-check",
+      "sbom",
+      "reproducibility-report",
+      "target-evidence"
+    ],
+    "sensitiveArtifactPolicy": "no-private-pattern-corpora-secrets-private-paths-or-host-data"
+  },
+  "targetEvidence": {
+    "proofFileEnv": "JPCRE2_TARGET_PROOF_FILE",
+    "requireProofEnv": "JPCRE2_REQUIRE_TARGET_PROOF",
+    "statusFile": "dist/target-evidence/status.txt",
+    "requiredMarkers": [
+      "target_pcre2_advisory_status=reviewed",
+      "target_pcre2_linkage_status=reviewed",
+      "platform_cve_backport_status=reviewed",
+      "hostile_regex_soak_status=reviewed",
+      "downstream_consumer_integration_status=reviewed",
+      "external_regex_ffi_review_status=reviewed",
+      "cross_platform_smoke_status=reviewed",
+      "sensitive_artifact_policy=no-private-pattern-corpora-secrets-private-paths-or-host-data"
+    ]
+  },
+  "ffiBoundary": {
+    "nativeLoading": "lazy-via-jerboa-ffi",
+    "collectSafeCalls": true,
+    "cShim": "jerboa_pcre2_shim.c",
+    "resourceLimitGate": "hostile-regex-corpus"
+  }
+}
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 38d97e9..5eab866 100644
--- a/Makefile
+++ b/Makefile
@@ -1,49 +1,152 @@
-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
-
-LIBDIRS      = lib:$(JH)/lib
+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
 
-PCRE2_CFLAGS := $(shell pkg-config --cflags libpcre2-8 2>/dev/null)
-PCRE2_LIBS   := $(shell pkg-config --libs libpcre2-8 2>/dev/null || echo "-lpcre2-8")
-PCRE2_LIBDIR := $(shell pkg-config --variable=libdir libpcre2-8 2>/dev/null)
+PCRE2_CFLAGS ?= $(shell pkg-config --cflags libpcre2-8 2>/dev/null)
+PCRE2_LIBS ?= $(shell pkg-config --libs libpcre2-8 2>/dev/null || printf '%s\n' -lpcre2-8)
+PCRE2_LIBDIR ?= $(shell pkg-config --variable=libdir libpcre2-8 2>/dev/null)
+PCRE2_CONFIG ?= pcre2-config
 
-CC       ?= cc
-CFLAGS   ?= -O2 -fPIC -Wall
+CC ?= cc
+DIST_DIR ?= dist/release-evidence
+TARGET_EVIDENCE_DIR ?= dist/target-evidence
+WARN_CFLAGS ?= -Wall -Wextra -Werror
+HARDEN_CFLAGS ?= -D_FORTIFY_SOURCE=2 -fstack-protector-strong
+HARDEN_LDFLAGS ?=
+CFLAGS ?= -O2 -fPIC $(WARN_CFLAGS) $(HARDEN_CFLAGS)
 
 UNAME_S := $(shell uname -s)
 ifeq ($(UNAME_S),Darwin)
-  SHLIB_EXT   := dylib
+  SHLIB_EXT := dylib
   SHLIB_FLAGS := -dynamiclib -Wl,-undefined,dynamic_lookup
 else
-  SHLIB_EXT   := so
+  SHLIB_EXT := so
   SHLIB_FLAGS := -shared
+  HARDEN_LDFLAGS += -Wl,-z,relro -Wl,-z,now
 endif
 
 SHIM := jerboa_pcre2_shim.$(SHLIB_EXT)
 
-.PHONY: all build transpile test clean shim
+.PHONY: all build transpile test hostile-regex-corpus clean shim ensure-jerboa-tools security audit pcre2-advisory-check sbom reproducibility-report target-evidence verify release-evidence
 
 all: build
 
+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
+
 shim: $(SHIM)
 
 $(SHIM): jerboa_pcre2_shim.c
-	$(CC) $(CFLAGS) $(SHLIB_FLAGS) $(PCRE2_CFLAGS) -o $@ $< $(PCRE2_LIBS)
+	$(CC) $(CFLAGS) $(SHLIB_FLAGS) $(HARDEN_LDFLAGS) $(PCRE2_CFLAGS) -o $@ $< $(PCRE2_LIBS)
 
-transpile:
-	$(JERBUILD) transpile src lib --force
+transpile: ensure-jerboa-tools
+	@$(JERBUILD) transpile src lib --force
 
-build: shim transpile
+build: ensure-jerboa-tools shim transpile
 
 test: build
-	JERBOA_PCRE2_LIB=$(CURDIR) \
+	@JERBOA_PCRE2_LIB=$(CURDIR) \
 	DYLD_LIBRARY_PATH=$(PCRE2_LIBDIR):$(CURDIR):$$DYLD_LIBRARY_PATH \
 	LD_LIBRARY_PATH=$(PCRE2_LIBDIR):$(CURDIR):$$LD_LIBRARY_PATH \
 	$(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/pcre2-test.ss
 
+hostile-regex-corpus: build
+	@mkdir -p dist
+	@JERBOA_PCRE2_LIB=$(CURDIR) \
+	DYLD_LIBRARY_PATH=$(PCRE2_LIBDIR):$(CURDIR):$$DYLD_LIBRARY_PATH \
+	LD_LIBRARY_PATH=$(PCRE2_LIBDIR):$(CURDIR):$$LD_LIBRARY_PATH \
+	$(JERBUILD) exec --libdirs "$(LIBDIRS)" support/hostile-regex-corpus.ss > dist/hostile-regex-corpus.txt
+	@cat dist/hostile-regex-corpus.txt
+	@grep -q '^status=pass$$' dist/hostile-regex-corpus.txt
+
+security: scripts/security-check.sh
+	@sh scripts/security-check.sh
+
+audit: shim
+	@echo "==> PCRE2 version"
+	@if command -v "$(PCRE2_CONFIG)" >/dev/null 2>&1; then printf 'pcre2-config: '; "$(PCRE2_CONFIG)" --version; else echo "pcre2-config not found."; fi
+	@if command -v pkg-config >/dev/null 2>&1; then printf 'pkg-config libpcre2-8: '; pkg-config --modversion libpcre2-8 || true; fi
+	@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
+
+pcre2-advisory-check:
+	@PCRE2_CONFIG="$(PCRE2_CONFIG)" scripts/pcre2-advisory-check.sh
+
+sbom:
+	@mkdir -p dist
+	@{ \
+		printf 'project=jerboa-pcre2\n'; \
+		printf 'sbom_status=present\n'; \
+		printf 'target_evidence_status=present\n'; \
+		printf 'component=libpcre2-8\n'; \
+		printf 'hostile_regex_corpus=support/hostile-regex-corpus.ss\n'; \
+		if command -v "$(PCRE2_CONFIG)" >/dev/null 2>&1; then printf 'pcre2_config_version='; "$(PCRE2_CONFIG)" --version; fi; \
+		if command -v pkg-config >/dev/null 2>&1; then printf 'pkg_config_version='; pkg-config --modversion libpcre2-8 || true; fi; \
+		if command -v pkg-config >/dev/null 2>&1; then printf 'pkg_config_libdir='; pkg-config --variable=libdir libpcre2-8 || true; fi; \
+		printf 'pcre2_cflags=%s\n' "$(PCRE2_CFLAGS)"; \
+		printf 'pcre2_libs=%s\n' "$(PCRE2_LIBS)"; \
+		printf 'official_source=https://www.pcre.org/\n'; \
+		printf 'official_releases=https://github.com/PCRE2Project/pcre2/releases\n'; \
+		printf 'official_security=https://pcre2project.github.io/pcre2/project/security/\n'; \
+	} > dist/sbom.txt
+
+reproducibility-report:
+	@JERBUILD="$(JERBUILD)" PCRE2_CONFIG="$(PCRE2_CONFIG)" scripts/reproducibility-report.sh dist/reproducibility
+
+target-evidence: scripts/target-evidence.sh
+	@REPO_ROOT="$(CURDIR)" TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" sh scripts/target-evidence.sh
+
+verify: security test hostile-regex-corpus audit pcre2-advisory-check sbom reproducibility-report target-evidence
+
+release-evidence: security test hostile-regex-corpus audit sbom target-evidence
+	rm -rf "$(DIST_DIR)"
+	mkdir -p "$(DIST_DIR)"
+	grep -q 'hostile_regex_status: expanded' docs/ffi-boundary.md
+	grep -q 'hostile_regex_cases_minimum: 192' docs/ffi-boundary.md
+	grep -q 'hostile_regex_scope_status: nested-alternation-optional-quote-limits' docs/ffi-boundary.md
+	PCRE2_CONFIG="$(PCRE2_CONFIG)" scripts/pcre2-advisory-check.sh "$(DIST_DIR)/pcre2-advisory-check.txt"
+	JERBUILD="$(JERBUILD)" PCRE2_CONFIG="$(PCRE2_CONFIG)" scripts/reproducibility-report.sh dist/reproducibility
+	rm -rf "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/target-evidence"
+	cp -R dist/reproducibility "$(DIST_DIR)/reproducibility"
+	cp -R "$(TARGET_EVIDENCE_DIR)" "$(DIST_DIR)/target-evidence"
+	cp dist/sbom.txt "$(DIST_DIR)/sbom.txt"
+	cp dist/hostile-regex-corpus.txt "$(DIST_DIR)/hostile-regex-corpus.txt"
+	git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt"
+	git status --short > "$(DIST_DIR)/git-status.txt"
+	{ printf 'platform='; uname -srm; printf 'jerbuild_version='; "$(JERBUILD)" --version; if "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then printf 'jerboa_home_status=present\n'; else printf 'jerboa_home_status=missing\n'; fi; } > "$(DIST_DIR)/build-environment.txt"
+	($(CC) --version || $(CC) -v) > "$(DIST_DIR)/cc-version.txt" 2>&1 || true
+	$(MAKE) security > "$(DIST_DIR)/security.log" 2>&1
+	$(MAKE) test > "$(DIST_DIR)/test.log" 2>&1
+	$(MAKE) audit > "$(DIST_DIR)/audit.log" 2>&1
+	@if command -v "$(PCRE2_CONFIG)" >/dev/null 2>&1; then "$(PCRE2_CONFIG)" --version > "$(DIST_DIR)/pcre2-version.txt"; else echo "pcre2-config not found." > "$(DIST_DIR)/pcre2-version.txt"; fi
+	@if command -v otool >/dev/null 2>&1; then \
+		otool -L "$(SHIM)" > "$(DIST_DIR)/native-linkage.txt"; \
+	elif command -v ldd >/dev/null 2>&1; then \
+		ldd "$(SHIM)" > "$(DIST_DIR)/native-linkage.txt"; \
+	else \
+		echo "No native linkage inspector found." > "$(DIST_DIR)/native-linkage.txt"; \
+	fi
+	{ test -f .jerboa/security.json && printf '%s\n' .jerboa/security.json; find Makefile jerboa_pcre2_shim.c src tests docs scripts support .github -type f \( -name 'Makefile' -o -name '*.c' -o -name '*.sh' -o -name '*.ss' -o -name '*.sls' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' \) -print; } | sort > "$(DIST_DIR)/source-inputs.txt"
+	shasum -a 256 jerboa_pcre2_shim.c "$(SHIM)" > "$(DIST_DIR)/native-sha256.txt"
+	sh scripts/sanitize-evidence.sh "$(DIST_DIR)"
+	sh scripts/sanitize-evidence.sh dist/reproducibility
+	sh scripts/sanitize-evidence.sh "$(TARGET_EVIDENCE_DIR)"
+	grep -q '^status=pass$$' "$(DIST_DIR)/hostile-regex-corpus.txt"
+	grep -q '^status=match$$' "$(DIST_DIR)/reproducibility/result.txt"
+	grep -q '^sbom_status=present$$' "$(DIST_DIR)/sbom.txt"
+	grep -q '^target_evidence_status=present$$' "$(DIST_DIR)/sbom.txt"
+
 clean:
 	rm -f jerboa_pcre2_shim.so jerboa_pcre2_shim.dylib pcre2_shim.so pcre2_shim.dylib
-	rm -rf lib
+	rm -rf lib .jerboa/bin
diff --git a/README.md b/README.md
index 360e085..f4eedd6 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,106 @@
-# chez-pcre2
+# jerboa-pcre2
 
-PCRE2 bindings for Chez Scheme, providing API parity with [pcre2-upstream](../pcre2-upstream/).
+PCRE2 bindings for Jerboa.
+
+Status: experimental. Treat this repository as a native FFI and hostile-input
+regular expression component until the release gates in `SECURITY.md` and
+`docs/ffi-boundary.md` are complete.
 
 ## Requirements
 
-- Chez Scheme 10.x
-- libpcre2-8 (with pkg-config support)
-- GCC or compatible C compiler
+- Jerboa `jerbuild`
+- C compiler
+- `libpcre2-8` development headers and library
+- `pkg-config` recommended
 
-## Build
+On Debian/Ubuntu:
 
-```bash
-make          # builds pcre2_shim.so
-make test     # runs the test suite
+```sh
+sudo apt-get install build-essential pkg-config libpcre2-dev
 ```
 
-## Usage
+## Build And Test
 
-```scheme
-(import (chez-pcre2 pcre2))
+```sh
+make test
+make hostile-regex-corpus
+make audit
+make pcre2-advisory-check
+make reproducibility-report
+make target-evidence
+make release-evidence
+```
 
-;; Compile a pattern
-(define rx (pcre2-compile "(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})"))
+`make test` builds `jerboa_pcre2_shim`, transpiles the Jerboa libraries, and
+runs the PCRE2 regression tests. If `jerbuild` is not installed, the Makefile
+bootstraps the pinned Jerboa release into `.jerboa/bin`.
+
+`make hostile-regex-corpus` runs an expanded deterministic adversarial-pattern
+corpus under low PCRE2 match/depth limits and records fail-closed behavior for
+backtracking-heavy and quoted-literal shapes.
+
+`make audit` records the PCRE2 version and native linkage for release notes.
+`make release-evidence` writes the local release artifact bundle under
+`dist/release-evidence/`, including toolchain identity, native linkage, a
+minimal SBOM manifest, hostile-regex corpus output, PCRE2 advisory status,
+source inputs, hashes, and a two-clean-build reproducibility report that also
+compares repeated corpus output.
+
+Production support is blocked until `make target-evidence` records a
+marker-complete proof through `JPCRE2_TARGET_PROOF_FILE`; production release
+hosts should set `JPCRE2_REQUIRE_TARGET_PROOF=1`. The proof covers target PCRE2
+advisory/linkage review, platform CVE/backport review, hostile-regex soak,
+downstream consumer integration, external regex/FFI review, and cross-platform
+smoke. It must not contain private pattern corpora, secrets, private paths,
+hostnames, or raw command output.
 
-;; Search
-(define m (pcre2-search rx "Today is 2024-03-15"))
-(pcre-match-group m 0)         ;=> "2024-03-15"
-(pcre-match-named m "year")    ;=> "2024"
+## Usage
 
-;; Replace
-(pcre2-replace "\\d+" "abc 42 def" "NUM")  ;=> "abc NUM def"
-(pcre2-replace-all "\\d+" "1 and 2" "X")   ;=> "X and X"
+```scheme
+(import (jerboa-pcre2 pcre2))
 
-;; Split
-(pcre2-split ",\\s*" "a, b, c")  ;=> ("a" "b" "c")
+(define rx (pcre2-compile "(?P<year>\\d{4})-(?P<month>\\d{2})"))
+(define m (pcre2-search rx "Today is 2026-06"))
 
-;; Extract all matches
-(pcre2-extract "\\d+" "a1 b22 c333")  ;=> ("1" "22" "333")
+(pcre-match-group m 0)      ; => "2026-06"
+(pcre-match-named m "year") ; => "2026"
 
-;; Boolean test
-(pcre2-matches? "^\\d+$" "12345")  ;=> #t
+(pcre2-release! rx)
 ```
 
+PCRE2 is a backtracking engine. For untrusted patterns, set conservative match
+limits with `(pcre2-set-match-limits! match-limit depth-limit)` and use an
+admission/review step before running user-authored regular expressions.
+
 ## API
 
-### Compilation
-- `(pcre2-compile pattern [options] [jit?])` - Compile a PCRE2 pattern
-- `(pcre2-regex pattern keyword: value ...)` - Compile with named options
-
-### Matching
-- `(pcre2-match rx subject [start])` - Full match (anchored)
-- `(pcre2-search rx subject [start])` - Search (unanchored)
-- `(pcre2-matches? rx subject [start])` - Boolean test
-
-### Match Access
-- `(pcre-match-group m [n])` - Get group n as string
-- `(pcre-match-named m name)` - Get named group
-- `(pcre-match-positions m [n])` - Get (start . end) positions
-- `(pcre-match->list m)` - All groups as list
-- `(pcre-match->alist m)` - Named groups as alist
-
-### Substitution
-- `(pcre2-replace rx subject replacement [start] [extended?])` - Replace first
-- `(pcre2-replace-all rx subject replacement [start] [extended?])` - Replace all
-
-### Iteration
-- `(pcre2-find-all rx subject [start])` - List of match objects
-- `(pcre2-extract rx subject [start])` - List of matched strings
-- `(pcre2-fold rx kons knil subject [start])` - Fold over matches
-- `(pcre2-split rx subject [limit])` - Split by pattern
-- `(pcre2-partition rx subject)` - Alternating non-match/match list
-
-### Utilities
-- `(pcre2-quote str)` - Escape metacharacters
-- `(pcre2-release! rx)` - Explicitly free resources
-
-### Pregexp-Compatible
-- `(pcre2-pregexp-match pattern subject [start] [end])`
-- `(pcre2-pregexp-match-positions pattern subject [start] [end])`
-- `(pcre2-pregexp-replace pattern subject replacement)`
-- `(pcre2-pregexp-replace* pattern subject replacement)`
-- `(pcre2-pregexp-quote str)`
-# chez-pcre2
+| Function | Description |
+| --- | --- |
+| `(pcre2-compile pattern [options] [jit?])` | Compile a PCRE2 pattern |
+| `(pcre2-regex pattern keyword: value ...)` | Compile with named options |
+| `(pcre2-match rx subject [start])` | Full anchored match |
+| `(pcre2-search rx subject [start])` | Unanchored search |
+| `(pcre2-matches? rx subject [start])` | Boolean search |
+| `(pcre-match-group m [n])` | Return group string or `#f` |
+| `(pcre-match-named m name)` | Return named group |
+| `(pcre-match-positions m [n])` | Return `(start . end)` |
+| `(pcre2-replace rx subject replacement [start] [extended?])` | Replace first match |
+| `(pcre2-replace-all rx subject replacement [start] [extended?])` | Replace all matches |
+| `(pcre2-find-all rx subject [start])` | Return all match objects |
+| `(pcre2-extract rx subject [start])` | Return matched strings |
+| `(pcre2-fold rx kons knil subject [start])` | Fold over matches |
+| `(pcre2-split rx subject [limit])` | Split by pattern |
+| `(pcre2-partition rx subject)` | Alternating non-match/match list |
+| `(pcre2-set-match-limits! match depth)` | Set per-thread PCRE2 match/depth limits |
+| `(pcre2-quote str)` | Escape metacharacters |
+| `(pcre2-release! rx)` | Free native resources |
+
+Pregexp-compatible helpers are also provided:
+`pcre2-pregexp-match`, `pcre2-pregexp-match-positions`,
+`pcre2-pregexp-replace`, `pcre2-pregexp-replace*`, and
+`pcre2-pregexp-quote`.
+
+## Security
+
+See `SECURITY.md`, `docs/ffi-boundary.md`,
+`docs/pcre2-advisory-review.md`, and `docs/release-evidence.md`.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..d56333c
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,70 @@
+# Security Policy
+
+`jerboa-pcre2` is experimental. Do not make production claims for this
+repository until the release gates below are complete from a clean checkout.
+
+## Supported Status
+
+No public production-support commitment exists yet. Before a security-sensitive
+release:
+
+- `git status --short` shows only intentional release changes.
+- `make test` and `make audit` pass locally and in CI.
+- `make hostile-regex-corpus` records fail-closed behavior for known
+  backtracking-heavy pattern shapes under low match/depth limits.
+- `make pcre2-advisory-check` confirms the linked PCRE2 package is on an
+  accepted version. The current production minimum is PCRE2 10.46; 10.47 is the
+  reviewed current release.
+- `make target-evidence` records release-host proof status. Production release
+  hosts set `JPCRE2_REQUIRE_TARGET_PROOF=1` and provide
+  `JPCRE2_TARGET_PROOF_FILE` with every required review marker.
+- `make release-evidence` archives native linkage, PCRE2 advisory status, a
+  minimal SBOM manifest, source inputs, hashes, and a two-clean-build
+  reproducibility report under `dist/release-evidence/`.
+- The high-confidence secret scan is clean for the working tree, and repository
+  history has been reviewed or intentionally reset before first public release.
+- PCRE2 dependency version, CVE posture, native linkage, and platform support
+  are documented in release notes.
+- SBOM/platform gaps are documented.
+
+Target proof files are marker-only release attestations. Do not include private
+pattern corpora, customer data, secrets, private filesystem paths, hostnames, or
+raw command output in proof files. The target-evidence script rejects missing,
+overlarge, marker-incomplete, host-private, or high-confidence secret proof
+material.
+
+## Regular Expression Risks
+
+- PCRE2 is a backtracking engine. User-authored patterns can trigger
+  denial-of-service through catastrophic backtracking.
+- The shim sets default per-thread match and depth limits, and callers can tune
+  them with `(pcre2-set-match-limits! match-limit depth-limit)`.
+- The release gate includes a hostile-regex corpus smoke. This is not a full
+  fuzzing substitute; it proves the current limit plumbing fails closed for
+  representative nested quantifier and ambiguous alternation shapes.
+- Treat untrusted pattern sets as hostile input. Prefer reviewed pattern
+  catalogs, literal matching, or stricter admission rules for production.
+- JIT improves speed but is not a security boundary.
+- PCRE2 10.45 is rejected for production because it is affected by
+  CVE-2025-58050. Older distro packages require explicit backport evidence
+  before any production claim.
+
+## Native Boundary
+
+The FFI boundary is documented in `docs/ffi-boundary.md`.
+
+Security-sensitive expectations:
+
+- Dynamic native loading is lazy and must not run at library import time.
+- Blocking or CPU-heavy native calls use collect-safe FFI declarations.
+- Match-data and compiled-pattern handles must be freed on success, no-match,
+  and exception paths.
+- Released regex objects must not be reusable.
+- C entry points reject null pointers, invalid offsets, and unsafe allocation
+  sizes.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After
+public release, replace this section with a dedicated advisory contact,
+supported versions, and disclosure window.
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
new file mode 100644
index 0000000..a5c1cb8
--- /dev/null
+++ b/docs/ffi-boundary.md
@@ -0,0 +1,62 @@
+# jerboa-pcre2 FFI Boundary
+
+`jerboa-pcre2` exposes a Jerboa API over the system `libpcre2-8` library through
+`jerboa_pcre2_shim.c`.
+
+## Trust Boundaries
+
+- Patterns, subjects, replacements, and named-group inputs are untrusted data.
+- Scheme owns all input bytevectors passed to C. C must not retain them after
+  returning.
+- Native loading is lazy through `(jerboa ffi)` so importing the module does not
+  crash static binaries or feature probes.
+- Matching, JIT matching, compilation, and substitution use collect-safe FFI
+  declarations because they can consume CPU or wait in native code.
+
+## Memory And Lifetime Rules
+
+- `pcre2-compile` owns a `pcre2_code` pointer and one reusable match-data handle.
+- Per-call match-data allocated by search/match/fold/split helpers is freed on
+  success, no-match, and exception paths.
+- `pcre2-release!` frees native handles, zeros the record fields, removes the
+  regex from the cache, and makes later reuse raise before C is called.
+- Result BLOB-like arbitrary bytes are not a goal of the string substitution
+  API; outputs are treated as UTF-8 strings.
+- C entry points reject null pointers, invalid offsets, and unsafe allocation
+  sizes as a second line of defense.
+
+## ReDoS And Resource Limits
+
+- PCRE2 is a backtracking engine. Untrusted patterns can cause high CPU usage
+  even when subjects are small.
+- The C shim applies default per-thread match and depth limits:
+  `match-limit=10000000` and `depth-limit=1000000`.
+- Callers can lower or raise limits with `(pcre2-set-match-limits! match depth)`.
+- Returning `#f` for match-limit failures is intentional at this layer; callers
+  that need diagnostics should compile and test patterns before admitting them.
+- Production consumers should prefer trusted pattern sets or a review/admission
+  step before accepting user-authored regular expressions.
+- `hostile_regex_status: expanded`
+- `hostile_regex_cases_minimum: 192`
+- `hostile_regex_scope_status: nested-alternation-optional-quote-limits`
+- `make hostile-regex-corpus` runs deterministic ReDoS-shaped no-match cases
+  under low match/depth limits and quoted-literal cases for hostile-looking
+  patterns. Release reproducibility compares repeated corpus output.
+
+## Release Gates
+
+Before production release:
+
+- `make test` must pass from a clean checkout.
+- `make audit` must record PCRE2 version and native linkage.
+- `make hostile-regex-corpus` must pass and be archived in release evidence.
+- `make pcre2-advisory-check` must pass. The current production minimum is
+  PCRE2 10.46; 10.47 is the reviewed current release.
+- `make target-evidence` must record local blocked status or a marker-complete
+  target proof through `JPCRE2_TARGET_PROOF_FILE`.
+- `make release-evidence` must record dependency, linkage, advisory, source
+  input, hash, and two-clean-build reproducibility evidence under
+  `dist/release-evidence/`.
+- Jerboa security scanner and the high-confidence secret scan must be clean.
+- PCRE2 CVE status and platform support must be documented in release notes.
+- SBOM/platform gaps must be documented.
diff --git a/docs/pcre2-advisory-review.md b/docs/pcre2-advisory-review.md
new file mode 100644
index 0000000..ecad81c
--- /dev/null
+++ b/docs/pcre2-advisory-review.md
@@ -0,0 +1,38 @@
+# PCRE2 Advisory Review
+
+Review date: 2026-06-21
+
+`jerboa-pcre2` links the system `libpcre2-8` library through
+`jerboa_pcre2_shim.c`. A release candidate is not acceptable if it links PCRE2
+10.45 or any older package without explicit downstream backport evidence.
+
+## Accepted PCRE2 Version
+
+Release evidence must pass `make pcre2-advisory-check`.
+
+- Preferred current release: PCRE2 10.47.
+- Minimum accepted release for production: PCRE2 10.46.
+- PCRE2 10.45 is rejected because it is affected by CVE-2025-58050.
+- PCRE2 10.44 and older require explicit distro/security-backport evidence
+  before any production claim.
+
+Official sources:
+
+- https://www.pcre.org/
+- https://github.com/PCRE2Project/pcre2/releases
+- https://pcre2project.github.io/pcre2/project/security/
+
+## Local Review Result
+
+The local release path uses Homebrew PCRE2 10.47.
+
+## Release Gate
+
+Before publishing:
+
+- Run `make clean`.
+- Run `make release-evidence JERBUILD=/path/to/jerbuild`.
+- Confirm `dist/release-evidence/pcre2-advisory-check.txt` reports
+  `status=pass`.
+- Confirm `dist/release-evidence/reproducibility/result.txt` reports
+  `status=match`.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..4cc31fc
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,76 @@
+# Release Evidence
+
+`jerboa-pcre2` is a native FFI boundary around PCRE2. Production release
+candidates must carry current test, dependency, linkage, advisory, and
+reproducibility evidence.
+
+Run from a clean checkout:
+
+```sh
+make clean
+make release-evidence JERBUILD=/path/to/jerbuild
+```
+
+The `release-evidence` target runs `make test`, `make hostile-regex-corpus`,
+`make audit`, `make security`, `make target-evidence`, a minimal SBOM manifest,
+the PCRE2 advisory gate, and a two-clean-build reproducibility check. It writes
+untracked artifacts under
+`dist/release-evidence/`:
+
+- `git-commit.txt` and `git-status.txt`.
+- `build-environment.txt` with host-neutral OS and Jerboa toolchain identity.
+- `cc-version.txt` and `pcre2-version.txt`.
+- `native-linkage.txt` from `otool -L` or `ldd`.
+- `pcre2-advisory-check.txt` with the accepted PCRE2 version result.
+- `sbom.txt` with the local `libpcre2-8` dependency manifest.
+- `hostile-regex-corpus.txt` with the expanded deterministic adversarial-pattern
+  corpus under low match/depth limits.
+- `reproducibility/result.txt` plus first/second native shim and generated
+  library hashes, including repeated `hostile_regex_status`.
+- `target-evidence/status.txt`, `proof-template.txt`, and optional hashed target
+  proof when `JPCRE2_TARGET_PROOF_FILE` is supplied.
+- `source-inputs.txt` for source, docs, tests, CI, support, and evidence
+  scripts.
+- `native-sha256.txt` for the C shim source and built native shim.
+
+Before publishing, review PCRE2 advisories for the linked package or distro
+build and document any accepted CVE or platform risk in release notes. The
+current accepted PCRE2 version policy is documented in
+`docs/pcre2-advisory-review.md`, and `make release-evidence` rejects
+unreviewed versions automatically.
+
+Reproducibility evidence is claimed only for two clean builds in the same
+toolchain environment. The automated report compares the native shim, the
+generated Jerboa library manifest, and the hostile regex corpus output.
+Reproducibility archives hashes/status/diffs only; raw scratch build logs and
+raw native shims are not retained in release evidence.
+
+## Target Production Proof
+
+`make target-evidence` defaults to local blocked status:
+
+```text
+target_evidence_status: local-release-proof-recorded
+target_pcre2_proof_status: not-run
+production_pcre2_status: blocked-not-run
+```
+
+For a production release host, set `JPCRE2_REQUIRE_TARGET_PROOF=1` and provide a
+small marker-only `JPCRE2_TARGET_PROOF_FILE` containing exactly these reviewed
+markers:
+
+```text
+target_pcre2_advisory_status=reviewed
+target_pcre2_linkage_status=reviewed
+platform_cve_backport_status=reviewed
+hostile_regex_soak_status=reviewed
+downstream_consumer_integration_status=reviewed
+external_regex_ffi_review_status=reviewed
+cross_platform_smoke_status=reviewed
+sensitive_artifact_policy=no-private-pattern-corpora-secrets-private-paths-or-host-data
+```
+
+The proof file must not contain private pattern corpora, secrets, private paths,
+hostnames, SSH clone strings, or raw command output. Missing, incomplete,
+overlarge, host-private, or high-confidence secret proof files fail closed with
+`status=blocked-target-proof`.
diff --git a/jerboa_pcre2_shim.c b/jerboa_pcre2_shim.c
index 764a379..0092256 100644
--- a/jerboa_pcre2_shim.c
+++ b/jerboa_pcre2_shim.c
@@ -7,6 +7,7 @@
 
 #define PCRE2_CODE_UNIT_WIDTH 8
 #include <pcre2.h>
+#include <limits.h>
 #include <stdlib.h>
 #include <string.h>
 #include <stdint.h>
@@ -16,10 +17,37 @@
  * ------------------------------------------------------------------- */
 static __thread int    _ffi_errorcode   = 0;
 static __thread size_t _ffi_erroroffset = 0;
+static __thread uint32_t _ffi_match_limit = 10000000u;
+static __thread uint32_t _ffi_depth_limit = 1000000u;
+
+int jerboa_pcre2_set_match_limits(uint32_t match_limit, uint32_t depth_limit)
+{
+    if (match_limit == 0 || depth_limit == 0) return -1;
+    _ffi_match_limit = match_limit;
+    _ffi_depth_limit = depth_limit;
+    return 0;
+}
+
+static pcre2_match_context_8* jerboa_pcre2_make_match_context(void)
+{
+    pcre2_match_context_8 *ctx = pcre2_match_context_create_8(NULL);
+    if (!ctx) return NULL;
+    if (pcre2_set_match_limit_8(ctx, _ffi_match_limit) < 0 ||
+        pcre2_set_depth_limit_8(ctx, _ffi_depth_limit) < 0) {
+        pcre2_match_context_free_8(ctx);
+        return NULL;
+    }
+    return ctx;
+}
 
 pcre2_code_8* jerboa_pcre2_compile(
     const char* pattern, size_t length, uint32_t options)
 {
+    if (!pattern && length > 0) {
+        _ffi_errorcode = PCRE2_ERROR_NULL;
+        _ffi_erroroffset = 0;
+        return NULL;
+    }
     return pcre2_compile_8(
         (PCRE2_SPTR8)pattern, length, options,
         &_ffi_errorcode, &_ffi_erroroffset, NULL);
@@ -49,8 +77,18 @@ int jerboa_pcre2_match(
     size_t startoffset, uint32_t options,
     pcre2_match_data_8* match_data)
 {
-    return pcre2_match_8(code, (PCRE2_SPTR8)subject, subject_length,
-                         startoffset, options, match_data, NULL);
+    int rc;
+    pcre2_match_context_8 *ctx = NULL;
+
+    if (!code || (!subject && subject_length > 0) || !match_data) return PCRE2_ERROR_NULL;
+    if (startoffset > subject_length) return PCRE2_ERROR_BADOFFSET;
+
+    ctx = jerboa_pcre2_make_match_context();
+    if (!ctx) return PCRE2_ERROR_NOMEMORY;
+    rc = pcre2_match_8(code, (PCRE2_SPTR8)subject, subject_length,
+                       startoffset, options, match_data, ctx);
+    pcre2_match_context_free_8(ctx);
+    return rc;
 }
 
 /* -------------------------------------------------------------------
@@ -59,11 +97,13 @@ int jerboa_pcre2_match(
 pcre2_match_data_8* jerboa_pcre2_match_data_create_from_pattern(