Security hardening and release readiness
ober
0d3109a90cecb723f1ff21a648298b48d74b0f61
new file mode 100644 --- /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 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 @@ -4,3 +4,7 @@ lib/ *.wpo *.o .jerbuild-hashes +.jerboa/* +!.jerboa/ +!.jerboa/security.json +dist/ new file mode 100644 --- /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" + } +} 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,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 --- 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`. new file mode 100644 --- /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. new file mode 100644 --- /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. new file mode 100644 --- /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`. new file mode 100644 --- /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`. --- 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(