Security hardening and release readiness
ober
94c8d679c920c65591dd39f36246b00bdef01c92
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +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: Test + run: make test-linux + + - name: Audit native linkage + run: make audit + + - name: Release evidence + run: make release-evidence new file mode 100644 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,35 @@ +name: Security Baseline + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + baseline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Required release files + run: | + set -eu + test -f LICENSE + test -f SECURITY.md + test -f .gitignore + find . -maxdepth 1 -iname "README*" -type f | grep -q . + + - name: High-confidence secret scan + run: | + set -eu + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})" + matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)" + if [ -n "$matches" ]; then + echo "$matches" + echo "High-confidence secret pattern found." + exit 1 + fi --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ lib/ *.wpo *.o .jerbuild-hashes +.jerboa/* +!.jerboa/ +!.jerboa/security.json +dist/ +tests/tmp/ new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,44 @@ +{ + "repository": "jerboa-inotify", + "riskClass": "linux-native-ffi-filesystem-monitor", + "status": "experimental-linux-only", + "releaseEvidence": { + "requiredTargets": [ + "security", + "test", + "audit", + "platform-evidence", + "sbom", + "reproducibility-report", + "release-evidence" + ], + "sensitiveArtifactPolicy": "no-private-paths-host-data-secrets-or-raw-production-watch-output" + }, + "targetEvidence": { + "linuxProofFileEnv": "JERBOA_INOTIFY_TARGET_LINUX_PROOF_FILE", + "linuxRequireProofEnv": "JERBOA_INOTIFY_REQUIRE_TARGET_LINUX_PROOF", + "overflowProofFileEnv": "JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE", + "overflowRequireProofEnv": "JERBOA_INOTIFY_REQUIRE_TARGET_OVERFLOW_PROOF", + "statusFile": "dist/platform-evidence/status.txt", + "linuxRequiredMarkers": [ + "target_platform_status=linux-recorded", + "linux_inotify_status=target-evidence-recorded", + "test_linux_status=target-evidence-recorded", + "native_linkage_status=target-evidence-recorded", + "native_shim_reproducibility_status=target-evidence-recorded" + ], + "overflowRequiredMarkers": [ + "overflow_runtime_status=target-evidence-recorded", + "overflow_event_status=rescan-required-tested", + "backpressure_status=target-evidence-recorded", + "recursive_watch_status=target-evidence-recorded", + "path_logging_status=redaction-tested" + ] + }, + "ffiBoundary": { + "nativeLoading": "lazy-via-jerboa-ffi", + "collectSafeCalls": true, + "cShim": "jerboa_inotify_shim.c", + "linuxRuntimeRequired": true + } +} 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,43 +1,131 @@ -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 - -CC ?= cc -CFLAGS ?= -shared -fPIC -O2 -LIBS ?= -SHIM = jerboa_inotify_shim.so +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 +RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence +PLATFORM_EVIDENCE_DIR ?= $(DIST_DIR)/platform-evidence +CC ?= cc +WARN_CFLAGS ?= -Wall -Wextra -Werror -Wformat=2 -Wshadow -Wpointer-arith -Wcast-align -Wwrite-strings +HARDEN_CFLAGS ?= -fPIC -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 +CFLAGS ?= $(HARDEN_CFLAGS) $(WARN_CFLAGS) +LDFLAGS ?= -shared -Wl,-z,relro,-z,now +LIBS ?= +SHIM = jerboa_inotify_shim.so UNAME_S := $(shell uname -s) -.PHONY: all build transpile test clean shim platform-check +.PHONY: all build transpile test test-linux clean shim ensure-jerboa-tools audit \ + platform-check platform-evidence security sbom reproducibility-report 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 + platform-check: @if [ "$(UNAME_S)" != "Linux" ]; then \ - echo "inotify is a Linux kernel API; shim build + tests require Linux ($(UNAME_S) detected)."; \ - exit 1; \ + echo "inotify is a Linux kernel API; runtime shim build/tests require Linux ($(UNAME_S) detected)."; \ + exit 1; \ fi shim: platform-check $(SHIM) -$(SHIM): jerboa_inotify_shim.c - $(CC) $(CFLAGS) -o $@ $< $(LIBS) +$(SHIM): jerboa_inotify_shim.c Makefile + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS) -transpile: - $(JERBUILD) transpile src lib --force +transpile: ensure-jerboa-tools + @$(JERBUILD) transpile src lib --force -build: transpile shim +build: transpile + @if [ "$(UNAME_S)" = "Linux" ]; then $(MAKE) shim; else echo "Skipping inotify shim build on $(UNAME_S)."; fi -test: build +test: transpile + @if [ "$(UNAME_S)" = "Linux" ]; then \ + $(MAKE) test-linux; \ + else \ + echo "Skipping inotify runtime tests on $(UNAME_S); CI runs them on Linux."; \ + fi + +test-linux: build JERBOA_INOTIFY_LIB=$(CURDIR) \ LD_LIBRARY_PATH=$(CURDIR) \ $(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/inotify-test.ss +audit: build + @echo "==> Platform" + @uname -srm + @echo "==> Native linkage for $(SHIM)" + @if [ "$(UNAME_S)" = "Linux" ] && command -v ldd >/dev/null 2>&1; then \ + ldd "$(SHIM)"; \ + else \ + echo "No Linux shim linkage audit available on $(UNAME_S)."; \ + fi + +platform-evidence: + @REPO_ROOT="$(CURDIR)" SHIM="$(SHIM)" PLATFORM_EVIDENCE_DIR="$(PLATFORM_EVIDENCE_DIR)" sh scripts/platform-evidence.sh + +security: scripts/security-check.sh + @sh scripts/security-check.sh + +sbom: transpile + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + CC="$(CC)" \ + SHIM="$(SHIM)" \ + sh scripts/sbom.sh + +reproducibility-report: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + MAKE="$(MAKE)" \ + SHIM="$(SHIM)" \ + sh scripts/reproducibility-report.sh + +verify: security test audit platform-evidence sbom reproducibility-report + +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 tests" + @$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1 + @echo "==> Running native audit" + @$(MAKE) audit > "$(RELEASE_EVIDENCE_DIR)/audit.log" 2>&1 + @echo "==> Recording platform evidence" + @$(MAKE) platform-evidence > "$(RELEASE_EVIDENCE_DIR)/platform-evidence.log" 2>&1 + @rm -rf "$(RELEASE_EVIDENCE_DIR)/platform-evidence" + @cp -R "$(PLATFORM_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/platform-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" + @{ \ + echo "overflow_policy_status=present"; \ + grep -E 'overflow_policy_status: documented|overflow_event_status: rescan-required|consumer_cache_status: dirty-until-rescan|backpressure_status: caller-required|recursive_watch_status: caller-managed|path_logging_status: redaction-required-for-sensitive-roots|target_overflow_proof_status: documented|proof_failure_status: blocked-target-proof' docs/overflow-handling.md; \ + } > "$(RELEASE_EVIDENCE_DIR)/overflow-policy.txt" + @grep -q 'overflow_policy_status: documented' "$(RELEASE_EVIDENCE_DIR)/overflow-policy.txt" + @grep -q 'overflow_event_status: rescan-required' "$(RELEASE_EVIDENCE_DIR)/overflow-policy.txt" + @grep -q 'backpressure_status: caller-required' "$(RELEASE_EVIDENCE_DIR)/overflow-policy.txt" + @find src lib docs scripts -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256" + @sh scripts/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)" + @sh scripts/sanitize-evidence.sh "$(PLATFORM_EVIDENCE_DIR)" + @sh scripts/sanitize-evidence.sh "$(DIST_DIR)/sbom" + @sh scripts/sanitize-evidence.sh "$(DIST_DIR)/reproducibility" + @echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)" + clean: rm -f $(SHIM) chez_inotify_shim.so - rm -rf lib + rm -rf lib .jerboa/bin tests/tmp dist --- a/README.md +++ b/README.md @@ -1,29 +1,58 @@ -# chez-inotify +# jerboa-inotify -Linux inotify filesystem event monitoring for Chez Scheme. +Linux inotify filesystem event monitoring for Jerboa. + +Status: experimental and Linux-only. Treat this repository as a native FFI +filesystem component until the release gates in `SECURITY.md` and +`docs/ffi-boundary.md` are complete. ## Requirements -- Chez Scheme 10.x -- Linux (inotify is Linux-only) -- GCC +- Linux with inotify support +- Jerboa `jerbuild` +- C compiler -## Build +## Build And Test -```bash -make +```sh +make test-linux +make audit +make security +make platform-evidence +make sbom +make reproducibility-report +make release-evidence ``` +On non-Linux hosts, `make test` transpiles the Jerboa source and skips runtime +tests. CI runs the real kernel tests on Ubuntu. + +If `jerbuild` is not installed, the Makefile bootstraps the pinned Jerboa +release into `.jerboa/bin`. + +`make release-evidence` records test output, platform evidence, native linkage +when Linux is available, SBOM manifests, and reproducibility output under +`dist/release-evidence/`. On non-Linux hosts it is expected to report partial +evidence because inotify runtime checks require the Linux kernel. + +Production Linux support is blocked until `make platform-evidence` records +Linux runtime/linkage proof and overflow/backpressure proof through +`JERBOA_INOTIFY_TARGET_LINUX_PROOF_FILE` and +`JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE`, or a Linux release host runs the +full evidence target directly. Production release hosts should set the matching +`JERBOA_INOTIFY_REQUIRE_TARGET_*_PROOF=1` variables when proof files are +required. Proof files must not contain private paths, hostnames, secrets, raw +production watch output, or raw command output. + ## Usage ```scheme -(import (chez-inotify)) +(import (jerboa-inotify)) (define fd (inotify-init)) (define wd (inotify-add-watch fd "/path/to/watch" (bitwise-ior IN_CREATE IN_DELETE IN_MODIFY))) -;; Poll with 5 second timeout (when (inotify-poll fd 5000) (for-each (lambda (ev) @@ -37,31 +66,32 @@ make (inotify-close fd) ``` +Consumers must handle `IN_Q_OVERFLOW` by rescanning watched state. Inotify does +not recursively watch directory trees. Production callers should follow +`docs/overflow-handling.md` for cache invalidation, backpressure, and path-log +redaction. + ## API | Function | Description | -|----------|-------------| +| --- | --- | | `(inotify-init)` | Create inotify instance, returns fd | | `(inotify-close fd)` | Close inotify fd | | `(inotify-add-watch fd path mask)` | Add watch, returns wd | | `(inotify-rm-watch fd wd)` | Remove watch | -| `(inotify-poll fd timeout-ms)` | Poll for events, returns #t/#f | -| `(inotify-read-events fd)` | Read all pending events | +| `(inotify-poll fd timeout-ms)` | Poll for events | +| `(inotify-read-events fd)` | Read pending events | ## Event Record | Accessor | Description | -|----------|-------------| +| --- | --- | | `(inotify-event-wd ev)` | Watch descriptor | | `(inotify-event-mask ev)` | Event mask | | `(inotify-event-cookie ev)` | Cookie for rename tracking | -| `(inotify-event-name ev)` | Filename or #f | - -## Constants - -Watch masks: `IN_ACCESS`, `IN_ATTRIB`, `IN_CLOSE_WRITE`, `IN_CLOSE_NOWRITE`, `IN_CREATE`, `IN_DELETE`, `IN_DELETE_SELF`, `IN_MODIFY`, `IN_MOVE_SELF`, `IN_MOVED_FROM`, `IN_MOVED_TO`, `IN_OPEN`, `IN_ALL_EVENTS`, `IN_MOVE`, `IN_CLOSE` +| `(inotify-event-name ev)` | Filename or `#f` | -Watch flags: `IN_DONT_FOLLOW`, `IN_EXCL_UNLINK`, `IN_MASK_ADD`, `IN_ONESHOT`, `IN_ONLYDIR` +## Security -Event flags: `IN_IGNORED`, `IN_ISDIR`, `IN_Q_OVERFLOW`, `IN_UNMOUNT` -# chez-inotify +See `SECURITY.md`, `docs/ffi-boundary.md`, `docs/overflow-handling.md`, and +`docs/release-evidence.md`. new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,67 @@ +# Security Policy + +`jerboa-inotify` is experimental and Linux-only. Do not make production claims +for this repository until the release gates below are complete from a clean +Linux 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-linux` and `make audit` pass locally on Linux and in CI. +- The high-confidence secret scan is clean for the working tree, and repository + history has been reviewed or intentionally reset before first public release. +- Linux kernel/platform support, native linkage, event-overflow behavior, and + unsupported-platform behavior are documented in release notes. +- Event-overflow/backpressure consumer policy is documented in + `docs/overflow-handling.md`. +- `make security` passes, including release-file, native-boundary, generated + artifact, private-path, and target-proof marker checks. +- `make release-evidence` has been archived from Linux, including SBOM, + platform, linkage, test, and reproducibility evidence. +- Target Linux runtime/linkage and overflow evidence is captured through the + proof controls documented in `docs/release-evidence.md`; production release + hosts set `JERBOA_INOTIFY_REQUIRE_TARGET_LINUX_PROOF=1` or + `JERBOA_INOTIFY_REQUIRE_TARGET_OVERFLOW_PROOF=1` with + `JERBOA_INOTIFY_TARGET_LINUX_PROOF_FILE` or + `JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE`. Required missing, incomplete, or + sensitive proof material fails closed with `status=blocked-target-proof`. + +## Filesystem Monitoring Risks + +- Inotify is notification, not authorization. It must not be treated as a + sandbox, policy engine, or complete audit log. +- Event queues can overflow. Consumers must handle `IN_Q_OVERFLOW` and rescan + watched state. Required consumer cache and backpressure behavior is documented + in `docs/overflow-handling.md`. +- Recursive watching is not automatic. Consumers watching directory trees must + add watches for new subdirectories deliberately. +- Paths and filenames can reveal sensitive local filesystem layout. Avoid + logging watched paths from private or production environments unless intended. + +Target proof files are marker-only release attestations. Do not include private +paths, hostnames, secrets, production watch output, SSH clone strings, or raw +command output. The platform-evidence script rejects overlarge, marker- +incomplete, host-private, or high-confidence secret proof material. + +## 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. +- Watched paths are length-checked and reject embedded NUL bytes before C calls. +- Event parsing validates buffer bounds before reading kernel event fields. +- `read`, `poll`, and add-watch operations are collect-safe FFI calls. +- File descriptors and watch descriptors are caller-owned resources. +- `make release-evidence` records non-Linux partial status honestly and must be + rerun on Linux before production claims. + +## 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,53 @@ +# jerboa-inotify FFI Boundary + +`jerboa-inotify` exposes Linux inotify through `jerboa_inotify_shim.c`. + +## Trust Boundaries + +- Watched paths are untrusted strings. They are converted to UTF-8 bytevectors, + length-checked, rejected if empty or containing NUL, and copied into a C string + before `inotify_add_watch`. +- Event buffers are kernel-provided data. The C shim validates offsets and event + lengths before reading fields or names. +- Native loading is lazy through `(jerboa ffi)` so importing the module does not + crash non-Linux feature probes or static binaries. +- `read`, `poll`, and `inotify_add_watch` are collect-safe FFI calls. + +## Memory And Lifetime Rules + +- Scheme owns read buffers and path bytevectors. +- C does not retain Scheme-owned pointers after returning. +- The caller owns the inotify file descriptor returned by `inotify-init` and must + close it with `inotify-close`. +- Watch descriptors returned by `inotify-add-watch` should be removed with + `inotify-rm-watch` when no longer needed. + +## Operational Caveats + +- This repository is Linux-only. macOS and other platforms can transpile the + Jerboa source but cannot run the kernel runtime tests. +- Inotify observes local filesystem events only. It is not an authorization or + sandbox boundary. +- Event queues can overflow. Consumers must handle `IN_Q_OVERFLOW` and rescan + watched state when necessary. See `docs/overflow-handling.md` for the required + consumer cache and backpressure policy. +- Recursive watching is a caller responsibility; inotify does not recursively + watch directory trees. + +## Release Gates + +Before production release: + +- `make test-linux` must pass on Linux from a clean checkout. +- `make audit` must record platform and native linkage. +- `make release-evidence` must be archived from Linux, including SBOM, + generated-library reproducibility, and native-shim reproducibility output. +- Jerboa security scanner and the high-confidence secret scan must be clean. +- Linux kernel/platform support and event-overflow behavior must be documented + in release notes. +- Overflow/backpressure policy evidence must be present in `make + release-evidence` output. +- Target Linux runtime/linkage and overflow evidence can be attached through + `JERBOA_INOTIFY_TARGET_LINUX_PROOF_FILE` and + `JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE`; required proof flags fail closed + with `status=blocked-target-proof`. new file mode 100644 --- /dev/null +++ b/docs/overflow-handling.md @@ -0,0 +1,76 @@ +# Queue Overflow And Backpressure + +`jerboa-inotify` exposes Linux inotify events. It does not make the kernel event +queue lossless. Consumers must treat queue overflow as a correctness boundary +and repair their own view of watched state. + +Required release markers: + +- `overflow_policy_status: documented` +- `overflow_event_status: rescan-required` +- `consumer_cache_status: dirty-until-rescan` +- `backpressure_status: caller-required` +- `recursive_watch_status: caller-managed` +- `path_logging_status: redaction-required-for-sensitive-roots` +- `target_overflow_proof_status: documented` +- `proof_failure_status: blocked-target-proof` + +## Required Consumer Shape + +When a consumer sees `IN_Q_OVERFLOW`, it must: + +- mark all cached state for the affected watcher as dirty; +- stop trusting incremental event ordering; +- rescan watched directories from a known root; +- rebuild any derived index before reporting the system as caught up; and +- emit a bounded diagnostic that does not expose sensitive watched paths unless + explicitly configured. + +Applications that cannot tolerate missed changes must combine inotify with a +durable source of truth, such as periodic full scans, content hashes, mtime/size +checks, or a database reconciliation step. + +## Backpressure + +Consumers should avoid unbounded per-event work. A production watcher should use +one of these patterns: + +- bounded work queues that coalesce repeated changes for the same path; +- a debounce window for high-churn directories; +- per-root concurrency limits for expensive indexing or upload work; +- explicit overflow counters and health status; and +- a manual or scheduled full reconciliation path. + +If the work queue fills, the caller should mark the relevant watched root dirty +and schedule a rescan instead of dropping events silently. + +## Recursive Watchers + +Inotify does not watch directory trees recursively. A recursive watcher must add +watches for existing subdirectories during initial scan, add watches for newly +created directories, remove watches for deleted directories, and rescan after +overflow before trusting its watch set. + +## Path Logging + +Watched paths may reveal private project names, user names, host layouts, and +secret-bearing directories. Production diagnostics should log root IDs or +redacted path labels by default, with full path logging as an explicit local +debug setting. + +## Target Evidence + +`JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE` records Linux release-host overflow +and consumer backpressure evidence. When +`JERBOA_INOTIFY_REQUIRE_TARGET_OVERFLOW_PROOF=1`, missing or incomplete proof +fails closed with `status=blocked-target-proof`. +Proof files must remain marker-only and must not include private watched paths, +hostnames, secrets, raw watch output, or raw command output. + +Required proof markers are: + +- `overflow_runtime_status=target-evidence-recorded` +- `overflow_event_status=rescan-required-tested` +- `backpressure_status=target-evidence-recorded` +- `recursive_watch_status=target-evidence-recorded` +- `path_logging_status=redaction-tested` new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,66 @@ +# Release Evidence + +`make release-evidence` writes ignored local evidence under +`dist/release-evidence/`. + +The evidence bundle contains: + +- build/test output; +- native linkage audit output when running on Linux; +- platform evidence showing whether Linux inotify runtime checks were possible; +- `platform-evidence/status.txt` plus optional copied target proof files and + `.sha256` sidecars; +- overflow/backpressure policy evidence in `overflow-policy.txt`; +- SBOM-style source, generated-library, build-environment, and native-shim + manifests; +- repeated transpile reproducibility output; +- repeated native-shim reproducibility output on Linux. + +Release evidence uses host-neutral platform fields and is sanitized before +publication. Proof files and release artifacts must not preserve private paths, +hostnames, secrets, SSH clone strings, raw production watch output, or raw +command output. Reproducibility evidence archives hashes/status only; scratch +build logs are not retained. + +On non-Linux hosts, the evidence is intentionally partial: transpilation and +generated-library reproducibility can pass, but runtime, linkage, and native +shim reproducibility report `status=blocked` because inotify is a Linux kernel +API. Production release evidence must include a Linux run of `make +release-evidence`. + +The overflow policy evidence records that `IN_Q_OVERFLOW` requires rescan, +consumer cache state remains dirty until rescan completes, backpressure is a +caller responsibility, recursive watches are caller-managed, and sensitive path +logging needs redaction. + +## Target Proof Files + +`scripts/platform-evidence.sh` accepts two target proof files. Setting a file +path records the proof; setting the matching `REQUIRE` variable to `1` makes a +missing, empty, or marker-incomplete proof block with +`status=blocked-target-proof`. + +- `JERBOA_INOTIFY_TARGET_LINUX_PROOF_FILE` with + `JERBOA_INOTIFY_REQUIRE_TARGET_LINUX_PROOF=1` records Linux runtime, test, + linkage, and native-shim reproducibility evidence. Required markers: + `target_platform_status=linux-recorded`, + `linux_inotify_status=target-evidence-recorded`, + `test_linux_status=target-evidence-recorded`, + `native_linkage_status=target-evidence-recorded`, and + `native_shim_reproducibility_status=target-evidence-recorded`. +- `JERBOA_INOTIFY_TARGET_OVERFLOW_PROOF_FILE` with + `JERBOA_INOTIFY_REQUIRE_TARGET_OVERFLOW_PROOF=1` records overflow and + consumer backpressure evidence. Required markers: + `overflow_runtime_status=target-evidence-recorded`, + `overflow_event_status=rescan-required-tested`, + `backpressure_status=target-evidence-recorded`, + `recursive_watch_status=target-evidence-recorded`, and + `path_logging_status=redaction-tested`. + +Proof files must be 64 KiB or smaller and marker-only. Files containing private +paths, host details, SSH clone strings, or high-confidence secret material fail +closed before they are copied into release evidence. + +target_linux_proof_status: documented +target_overflow_proof_status: documented +proof_failure_status: blocked-target-proof --- a/jerboa_inotify_shim.c +++ b/jerboa_inotify_shim.c @@ -1,122 +1,143 @@ -/* jerboa_inotify_shim.c — Linux inotify wrapper for Chez Scheme FFI */ +/* jerboa_inotify_shim.c - Linux inotify wrapper for Jerboa FFI */ -#include <sys/inotify.h> -#include <unistd.h> #include <errno.h> +#include <limits.h> +#include <poll.h> #include <stdlib.h> #include <string.h> -#include <poll.h> +#include <sys/inotify.h> +#include <unistd.h> + +static int bytes_have_nul(const unsigned char *src, int len) { + if (!src || len < 0) return 1; + for (int i = 0; i < len; i++) { + if (src[i] == 0) return 1; + } + return 0; +} + +static char *copy_cstring(const unsigned char *src, int len) { + char *dst = NULL; + if (!src || len <= 0 || len == INT_MAX || bytes_have_nul(src, len)) return NULL; + dst = (char *)malloc((size_t)len + 1); + if (!dst) return NULL; + memcpy(dst, src, (size_t)len); + dst[len] = '\0'; + return dst; +} + +static ssize_t (*inotify_sys_read)(int, void *, size_t) = read; + +static const struct inotify_event *event_at(const unsigned char *buf, int buflen, int offset) { + const struct inotify_event *ev = NULL; + size_t base = sizeof(struct inotify_event); + size_t total; + + if (!buf || buflen < 0 || offset < 0) return NULL; + if ((size_t)offset > (size_t)buflen) return NULL; + if ((size_t)buflen - (size_t)offset < base) return NULL; + + ev = (const struct inotify_event *)(const void *)(buf + offset); + total = base + (size_t)ev->len; + if (total < base) return NULL; + if (total > (size_t)buflen - (size_t)offset) return NULL; + return ev; +} -/* Create an inotify instance with NONBLOCK and CLOEXEC flags. - Returns fd or -errno. */ int jerboa_inotify_init(void) { int fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (fd < 0) return -errno; return fd; } -/* Add a watch. Returns watch descriptor or -errno. */ -int jerboa_inotify_add_watch(int fd, const char *path, unsigned int mask) { - int wd = inotify_add_watch(fd, path, mask); +int jerboa_inotify_add_watch_bv(int fd, const unsigned char *path, int path_len, unsigned int mask) { + char *path_c = copy_cstring(path, path_len); + int wd; + + if (fd < 0 || !path_c) return -EINVAL; + wd = inotify_add_watch(fd, path_c, mask); + free(path_c); if (wd < 0) return -errno; return wd; } -/* Remove a watch. Returns 0 or -errno. */ int jerboa_inotify_rm_watch(int fd, int wd) { + if (fd < 0 || wd < 0) return -EINVAL; if (inotify_rm_watch(fd, wd) < 0) return -errno; return 0; } -/* Read events into buffer. Returns bytes read, 0 if nothing available, - or -errno on error. */ int jerboa_inotify_read(int fd, unsigned char *buf, int buflen) { - int n = read(fd, buf, buflen); + ssize_t n; + + if (fd < 0 || !buf || buflen <= 0) return -EINVAL; + do { + n = inotify_sys_read(fd, buf, (size_t)buflen); + } while (n < 0 && errno == EINTR); + if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; return -errno; } - return n; + if (n > INT_MAX) return -EOVERFLOW; + return (int)n; } -/* Poll for readability with timeout_ms (-1 = block, 0 = poll). - Returns 1 if readable, 0 if timeout, -errno on error. */ int jerboa_inotify_poll(int fd, int timeout_ms) { struct pollfd pfd; + int rc; + + if (fd < 0 || timeout_ms < -1) return -EINVAL; pfd.fd = fd; pfd.events = POLLIN; - int rc = poll(&pfd, 1, timeout_ms); + pfd.revents = 0; + + do { + rc = poll(&pfd, 1, timeout_ms); + } while (rc < 0 && errno == EINTR); + if (rc < 0) return -errno; - return rc; + if (rc == 0) return 0; + return (pfd.revents & POLLIN) ? 1 : 0; }