diff --git a/PUBLIC-SNAPSHOT.json b/PUBLIC-SNAPSHOT.json index 9458878..e9a7fa9 100644 --- a/PUBLIC-SNAPSHOT.json +++ b/PUBLIC-SNAPSHOT.json @@ -1 +1 @@ -{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":77,"allowlist_sha256":"db978285858ba5a1fefeb732d716338d8c652c5fc583f465d08f80b9ec74f0a9","manifest_sha256":"5abb8eaf376ec390da5b1b5d811ec9aa685bf9e47e54086a51d049193738a49b"} +{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":80,"allowlist_sha256":"e40a56b3431efdd99b3a78c38c387722de347216640383fc849569a817edf5c6","manifest_sha256":"0b27ea55bc1f2083ac07ae777d20735e651c5026888b973bfe02ee764b9487dc"} diff --git a/PUBLIC-SNAPSHOT.sha256 b/PUBLIC-SNAPSHOT.sha256 index 8fcec3d..2715b9d 100644 --- a/PUBLIC-SNAPSHOT.sha256 +++ b/PUBLIC-SNAPSHOT.sha256 @@ -15,8 +15,8 @@ b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS. 6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md 327386b40ee9fb92a8568b6a51722578890393fa23051af632f3180385a4e739 ./README.md ce32945cf5f16ab1a0202615bcf2053f46d421dcdd8ff2f1292f95bb5cf4493d ./RELEASE.md -29eebbdfcff05d4ba709bf13d45052c6994767303ddb470031620c9987bca53a ./ROADMAP.md -2c86f5b983dfeb97a02d46850fa42e18cab1ed23201822aa3c344b9d2e1b0c3f ./SECURITY.md +419c334aeb20dc22ceba8d031aaa95314b77125bf3267fd9fdc003e3865f0327 ./ROADMAP.md +d70d89db6bf0142a42a95f45537be5a4562258646d36d56bd9a70096ec78ed91 ./SECURITY.md 53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md 842beff8afa72d120fcad0ac73afb2049d580ff3000975f3b1786c4ade6a14d4 ./TRADEMARKS.md 136a6d82db842547b342f8b0c9ffdc7c04f7c9b473b4ef6dca9dbc940cb24b54 ./cmd/himesan/main.go @@ -25,24 +25,27 @@ ce32945cf5f16ab1a0202615bcf2053f46d421dcdd8ff2f1292f95bb5cf4493d ./RELEASE.md 9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md 5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md 35f5b4b7d195a7b5c071d4665505afef189c7b386d4e3079e9ce8a96ace07f3a ./docs/COMPATIBILITY.md -e4021b554ebc479954321586012add57a5fbfb58a1f7fce001d5638880912fc6 ./docs/DEVELOPMENT_SERVER.md +5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md 51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md -f1a8e78c5aa521324ad2fcb386512158d0c0f9956e97f9a1bc8f97aa5d5e9844 ./docs/THREAT_MODEL.md +04c6b3f93588177a87edbca8f56af0e0f2a7c5ba31936f3174570c8282a1a7c2 ./docs/SECURITY_EVIDENCE.md +d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md +76ac444771ac0a4f584ee0cf86ebfd34233412e6e511485f6cc90131a9a50387 ./docs/V1_RELEASE_PLAN.md f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod -07d161772e9c6eec0dcc12179286e5c686dcabdc4e56a7cb8d112f640b072563 ./internal/compiler/backend.go -28123757d27298dd81cf13ebd9242b24556734a35e36c2ac731f2a8475d70d28 ./internal/compiler/compiler_test.go -d99ba263bf501ca81ed38ba88216c063d2fc22f4b45a3f28d5105957f449c4de ./internal/compiler/context.go +ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go +0a624f76214afbed561a6f6490405c5083e49a53ae301c6ede763b78407ec0c7 ./internal/compiler/backend.go +7c96a4b31a34201cb9c48a7f0bac1c201865e4ecc7c080691af5ce68b3d7c207 ./internal/compiler/compiler_test.go +dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go -42ccf512381e130bf593b065dccd7697560fb00240818efde6321c9095f6b4a6 ./internal/compiler/discover.go +45562a41ef9ab1116746e4962ce8f93d4d8651e1e468a38122c626f8a34a2874 ./internal/compiler/discover.go f5a6b31416027cb69a61d1a1421cba779ec3accae59c9ba9dd45d2c31b72149e ./internal/compiler/e2e_test.go eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go -4c1625114f92f9cc097c2fb1394fa0e4d43a03156f3be0aa537a485ec8243a57 ./internal/compiler/model.go -00180df94e3c73e1614eeae387ef00c3cec90e9f64b45a5a148c79408e2d10c2 ./internal/compiler/operation.go +d166096f185d76b2698aa3ab3251f00e58f84cdedc3af667e88ddd528ca0cb76 ./internal/compiler/model.go +0c7a7a4d6a51a8b58dfe7c12ecd8c608aff639fd6157a9657a0663ceb58c3c8c ./internal/compiler/operation.go d7d8181455d5f37ef9bcc6bdbf86e0630f20e8a5b3b81688d12742687b434c99 ./internal/compiler/parse.go 80cf170514a3b955d24440cb086d34e19f3a305510e3c5db95cb897be91f922a ./internal/compiler/replace_unix.go 0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando -b190a6a8aed288378ea13d12ec06bac68890c473c03c60016f7fa7534f142008 ./internal/compiler/testdata/golden/basic.sando.go +63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go 99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go @@ -52,10 +55,10 @@ eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/dev c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go 6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go 1dae73304532faac4aa8cedda5df65c6d199aaafc001708cc96529c07dee0588 ./internal/devserver/process_windows_64.go -a9b7649562f39a707214dc4a67c2353bd29b2df7fce7a05cac9a6451d1a0fcca ./internal/devserver/proxy.go -54f0fea40c0a19d268dcc33cab35d2c7d12f50134cd73ce9a609288e356f9fa8 ./internal/devserver/proxy_test.go -3d85066927da7e88ccb0a05afa261e06568c007711dbd9fd0219569aff59f568 ./internal/devserver/supervisor.go -6c53d6d10eaa36d9286471e21c61a9e80d858fa0ccfd47be0e72d1838ee440ea ./internal/devserver/supervisor_test.go +7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go +aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go +e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go +b94103cd4b582968cdb0b61b0164f57ade006fa4e5187fcaa05944274192526a ./internal/devserver/supervisor_test.go e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go @@ -63,8 +66,8 @@ d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/ver e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE b4a7bffe678a97209881e07989563a5085aa0ead9e1b67306087dac6b97bad70 ./sando/README.md -a8131a53016401fe8cc3f1be660983c79bb8d267fabef262aca8c55667908279 ./sando/component.go -ff905eacdef265e8ea04a8e462656f37b5f8ccd579b604b917143b0b1a7e5d6e ./sando/component_test.go +7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go +a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod 80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go 85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go diff --git a/ROADMAP.md b/ROADMAP.md index 661f82c..adabf40 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,6 +4,10 @@ Unchecked items are release blockers, not aspirational marketing. +The ordered initiative, repository topology, release-candidate sequence, and +definition of confidence are maintained in +[docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md). + ## Compiler and runtime - [ ] Compiler-owned deterministic golden output repeated across Linux, macOS, and Windows. diff --git a/SECURITY.md b/SECURITY.md index ad68bfc..ae146af 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,12 +2,124 @@ # Security policy -Sandwich Hime is an unsupported public pre-1.0 source preview. No version is yet supported for production use, and the project makes no vulnerability-response SLA or bug-bounty promise. +Sandwich Hime is a public pre-1.0 source preview. Security reports are welcome +now, even though no version is currently designated as supported for production +use. The project would rather receive a careful early report than project +confidence it has not earned. -Do not put undisclosed vulnerability details, credentials, personal data, or a working exploit in a public issue. Until a dedicated confidential address is published, use the repository owner's published Gitea contact method to ask for a private channel without disclosing the issue. If no private contact method is available, retain the details rather than publishing them. A tested confidential contact and documented response targets remain blockers for a supported release. +## Supported versions -The compiler treats templates and embedded Go as trusted source and rendered values as untrusted data. It does not sandbox template authors. The security boundary and known non-goals are specified in [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md). +| Version | Security status | +| --- | --- | +| Public `main` source preview | Best-effort assessment and fixes; interfaces may change | +| Versioned releases | None published yet | -For a future supported release, the intended process includes a private reproducer, regression test, coordinated disclosure when appropriate, checksums, and an advisory. Release artifacts and tags must be signed. Dependencies are minimized and scanned; generation/checking never fetch dependencies or execute project code. +This table will name supported release lines once immutable compiler and runtime +versions are published. A pre-1.0 release is not a promise of API stability or +fitness for a particular application. -This policy describes the project's current process and limitations; it is not legal advice and does not promise that every report can be accepted, embargoed, or fixed on a particular schedule. +## Report a vulnerability privately + +Email **security@sandwichhime.com**. Please do not put an undisclosed +vulnerability, working exploit, credential, secret, or personal data in a +public issue. + +Helpful reports include: + +- the affected compiler/runtime version or exact commit; +- the relevant `.sando` source, generated Go, or development configuration; +- a minimal reproducer and the observed security impact; +- operating system, architecture, Go version, and browser when relevant; +- whether the issue is already public or has a disclosure deadline; and +- a safe way to credit the reporter, or a request to remain anonymous. + +Minimize sensitive data. The mailbox is the private reporting route, but +ordinary email is not end-to-end encrypted. Do not send production secrets or +unnecessary personal data. An encryption key will be published only after its +ownership, backup, and recovery procedure have been tested. + +## What to expect + +These are best-effort targets for a founder-maintained project, not an SLA: + +- acknowledge a report within 7 calendar days; +- provide an initial severity/scope assessment within 14 calendar days when a + reproducible issue is available; and +- provide an update at least every 30 calendar days while an accepted report + remains unresolved. + +Health, disability, family responsibility, incomplete evidence, or incident +complexity may make those targets impossible. If that happens, the maintainer +will communicate the delay when safely able rather than inventing certainty. + +For a reproducible accepted vulnerability, the project aims to retain a private +reproducer where safe, add a regression test where practical, document affected +versions, and agree on a coordinated disclosure plan when appropriate. A fix +may be delivered through a new immutable version, a retraction, an advisory, or +documentation that narrows an incorrect guarantee. Published tags will not be +moved or silently replaced. + +## Scope and trust boundary + +The most useful reports concern: + +- contextual escaping or browser-parser disagreements; +- unsafe URL acceptance or trusted-value boundary confusion; +- parser, generator, path, symlink, ownership, or atomic-write failures; +- generated-code/runtime ABI mismatches; +- deterministic-output or source-provenance failures; +- development proxy exposure, request-origin controls, process cleanup, or + unintended execution; and +- dependency, release, signing, checksum, or artifact-integrity problems. + +Templates and embedded Go are trusted application source. Sandwich Hime is not +a sandbox for an untrusted template author. Handwritten Go implementations of +`sando.Component` and explicit `sando.Trust*` calls are trusted output +capabilities. Application routing, authorization, HTTP headers, database +security, deployment, and production process isolation remain application +responsibilities unless a defect originates in Sandwich Hime itself. + +The complete boundary and known non-goals are maintained in +[the threat model](docs/THREAT_MODEL.md). Reproducible assessment results and +open gaps are recorded separately in +[the security evidence ledger](docs/SECURITY_EVIDENCE.md). + +## Good-faith research + +Good-faith research means making a reasonable effort to: + +- test only systems, repositories, and data you own or are authorized to test; +- prefer local reproductions and the smallest proof necessary; +- stop if testing risks availability, privacy, data integrity, or another + person's account; +- avoid persistence, destructive changes, social engineering, spam, denial of + service, credential collection, and unnecessary data access; +- retain and transmit the minimum sensitive information required; and +- allow reasonable time for investigation before public disclosure. + +This policy permits research on local copies of the source. It does not +authorize active testing of project-operated websites, Gitea infrastructure, +or third-party deployments without separate written permission. Passively +observed issues are welcome. It does not create a bug bounty, safe-harbor +contract, embargo obligation, or promise that every report can be accepted. +The project will not pursue action against research that the maintainer +reasonably believes followed this policy in good faith, but cannot bind third +parties or override applicable law. When uncertain, contact the security +mailbox before testing. + +## Current assurance level + +The code has maintainer-led threat modeling, adversarial unit and integration +tests, race testing, bounded fuzz smoke tests, static analysis, dependency +inventory, and known-vulnerability scanning. The evidence ledger records those +maintainer-run checks against named commits and dates; its results are +point-in-time evidence, not continuous assurance. The project has not received +an independent security audit, certification, or formal verification. Coverage +percentages, passing scanners, and a clean vulnerability database result are +evidence of specific checks—not proof that no vulnerability exists. + +Release artifacts and tags are intended to carry signatures, checksums, an +SBOM, and exact source/build provenance. Those controls are publication gates +until the first versioned release is actually available. + +This policy is practical project guidance, not legal advice. diff --git a/docs/DEVELOPMENT_SERVER.md b/docs/DEVELOPMENT_SERVER.md index 25ebc48..3e6e99b 100644 --- a/docs/DEVELOPMENT_SERVER.md +++ b/docs/DEVELOPMENT_SERVER.md @@ -31,4 +31,8 @@ The stable proxy reserves `/__himesan/events` for SSE. It injects a fixed reload When an existing CSP is present, the proxy adds the fixed script's SHA-256 source and same-origin SSE connection permission; it does not add `unsafe-inline` or `unsafe-eval`. The proxy and every candidate upstream are literal loopback addresses. Replaced process groups are terminated and waited for on Unix and Windows. +If the active application exits, the proxy immediately forgets that exact upstream and closes its idle connections. Requests receive the waiting page until another candidate passes its health check; a different process that later acquires the old loopback port is not selected implicitly. + +Loopback is host-local, not user-local. Host, Origin, and Fetch Metadata checks defend against browser cross-site and DNS-rebinding requests, but they are not authentication against another process or account on the same workstation. Run `himesan dev` only on a trusted, single-user development machine and do not place secrets in its diagnostics. It invokes the configured Go toolchain, may fetch dependencies according to the user's Go environment, executes the project binary with the user's inherited environment, and forwards the application's requests and responses. Eligible HTML responses may be buffered up to 16 MiB for reload injection; application request, response, and SSE concurrency limits remain the application's and operating system's responsibility. + This is not a production proxy, TLS terminator, public preview server, process orchestrator, or deployment system. V1 refuses non-loopback binding. diff --git a/docs/SECURITY_EVIDENCE.md b/docs/SECURITY_EVIDENCE.md new file mode 100644 index 0000000..1833ece --- /dev/null +++ b/docs/SECURITY_EVIDENCE.md @@ -0,0 +1,163 @@ + + +# Security evidence ledger + +This ledger records what was actually inspected and executed. It is a +maintainer-led self-assessment, not an independent audit, certification, formal +verification, or guarantee that no vulnerability exists. + +## Assessment identity + +| Field | Value | +| --- | --- | +| Assessment date | 2026-08-12 | +| Public evidence identity | Exact file checksums in the co-published `PUBLIC-SNAPSHOT.sha256`; private/public commit mapping is retained only in the non-exported operational ledger | +| Assessment phases | Clean pre-remediation source followed by clean remediated source | +| Primary environment | Linux amd64 under WSL, Go 1.26.5 | +| Declared minimum Go | Go 1.25 | +| Assessor | Project maintainer with AI-assisted code review; human responsibility retained | + +Security remediation discovered during this assessment was committed and the +named checks were rerun from a clean source state. Before this ledger can support +a versioned release, the complete campaign must be rerun from the exact +sanitized public release commit. Private-to-public commit mappings are retained +outside the exported source rather than being disclosed here. + +## Observed evidence + +| Property examined | Enforcement or test surface | Result observed on 2026-08-12 | +| --- | --- | --- | +| Root correctness | `go test -count=1 ./...` | Pass | +| Concurrent access | `go test -race -count=1 ./...` | Pass | +| Runtime concurrency | `(cd sando && go test -race -count=1 ./...)` | Pass | +| Standard static analysis | `go vet ./...` and runtime equivalent | Pass | +| Reachable known vulnerabilities | `govulncheck@v1.6.0` on both modules | No vulnerabilities found on 2026-08-12 | +| Dependency surface | `go list -m -json all` in both modules | Zero third-party module requirements | +| Statement coverage | Go cover profiles on remediated source | compiler 75.2%; devserver 80.2%; runtime 96.6% | +| Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found | +| Deterministic generation | repeated generate/check/hash/mtime gates | Pass | +| Writer failures | runtime error/short-write/nil-writer tests | Pass | +| HTML text/attribute/RCDATA escaping | compiler and runtime adversarial cases | Pass for enumerated cases | +| URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases | +| Filesystem boundaries | symlink, nested-module, VCS, ownership, stale-output tests | Pass for tested cases; see open findings | +| Development proxy browser boundary | Host, Origin, Fetch Metadata, CSP, fragment and response tests | Pass for tested cases | +| Native platform behavior | Linux execution; Windows/macOS cross-compilation | Native Windows/macOS execution not yet evidenced | + +Coverage measures statements executed by tests. It is not branch completeness +and is not evidence that the executed behavior is secure. + +`govulncheck` reports vulnerabilities known to the Go vulnerability database +and reachable through its analysis. A clean result cannot detect unknown flaws, +design errors, or vulnerabilities outside its model. + +## Security-relevant design evidence + +### Production boundary + +The production `sando` module contains rendering contracts and contextual write +helpers. It contains no HTTP server, router, middleware, template discovery, +development proxy, plugin loader, or production process manager. Both compiler +and runtime modules currently have no third-party Go module requirements. + +### Compiler behavior + +Compilation builds and formats outputs in memory before generation writes. +Recursive discovery rejects or skips observed symlinks, nested modules, VCS +trees, vendor trees, and detected filesystem crossings. Existing non-owned, +symlink, and non-regular output files are rejected. Each changed file uses an +atomic replacement primitive; the whole set is not a filesystem transaction if +a later replacement fails. + +`generate` and `check` do not run project code, invoke the Go toolchain, fetch +dependencies, or edit module metadata. `himesan dev` is intentionally separate: +it builds and executes trusted project code and may fetch modules under the +user's normal Go configuration. + +### Output contexts + +The compiler accepts dynamic values only in its enumerated contexts. It rejects +dynamic markup construction, unquoted attributes, event-handler values, +dynamic style attributes, unsupported URL lists, foreign content, meta refresh, +malformed tags, and unbalanced generated components. Runtime helpers escape +ordinary text/attributes, validate ordinary whole-URL schemes before writing, +and escape every trusted wrapper in RCDATA. + +Script and style output require opaque trusted types. Those types deliberately +move responsibility to trusted application code; they are not sanitizers. + +## Reproduction commands + +Run from a clean canonical checkout. Networked scans contact the Go module proxy +and vulnerability database. + +```sh +go version +git status --short +git rev-parse HEAD^{commit} HEAD^{tree} + +./scripts/check-licenses.sh +HIMESAN_RACE=1 ./scripts/verify.sh + +go test -count=1 -cover ./... +(cd sando && go test -count=1 -cover ./...) + +go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... +(cd sando && go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...) + +go test ./internal/compiler -run '^$' \ + -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s +go test ./internal/compiler -run '^$' \ + -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s +``` + +The fuzz targets currently assert process robustness and result bounds. They do +not yet prove semantic HTML safety. + +## Assessment findings and remediation status + +The 2026-08-12 assessment identified six concrete gaps. Their current +working-tree status is recorded here without rewriting the original baseline: + +| Finding | Current remediation | Executable evidence | +| --- | --- | --- | +| Generated code named an ABI but did not enforce the exact contract | Generated code now requires the version-specific `sando.ABISandoV1` symbol | `TestGeneratedCodeRequiresVersionedRuntimeABIMarker`; `TestRuntimeABIMarker` | +| Deleted or renamed sources could leave owned `.sando.go` orphans invisible to directory-level `check` | Directory discovery now reports owned outputs whose adjacent source is absent or non-regular and blocks the operation before writes | `TestDirectoryOperationsRejectOrphanedOwnedOutputBeforeWrites` | +| Component-context prose included arbitrary handwritten implementations in the generated-component guarantee | Runtime documentation, policy, and threat model now classify handwritten components as trusted output capabilities | API documentation plus policy review; generated-balance tests retain their narrower scope | +| An exited development child left its former upstream selected | Exit notification now clears only the matching active target immediately, independent of the watcher poll interval | `TestClearTargetOnlyClearsSelectedUpstream`; `TestSupervisorClearsTargetWhenCurrentApplicationExits` | +| Trusted-value warnings were described more broadly than their analysis supports | Policy and threat-model copy now call them best-effort lexical audit hints rather than type or taint analysis | Documentation assertion and review | +| Public copy implied a completed systematic `html/template` differential campaign | Policy and public security copy now describe fixed adversarial cases and list systematic differential work as open | Documentation assertion and review | + +The remediated clean source passed the race-enabled repository verifier, +sanitized-snapshot tests, both bounded fuzz-smoke targets, compiler/runtime +known-vulnerability scans, and Windows/macOS cross-compilation on 2026-08-12. +Those results do not become release evidence until the changes are committed, +exported to the sanitized canonical public tree, and re-run from that exact +public commit. Native Windows/macOS execution and the other gaps below remain +separate release decisions. + +## Open assurance gaps + +- confidential mailbox delivery and response/recovery procedure must be tested; +- release signing, checksum, SBOM, and provenance rehearsal is incomplete; +- native Windows/macOS execution remains outstanding; +- browser-parser differential and semantic property testing need expansion; +- compiler input size, CPU, and memory have no built-in hard budget; +- filesystem checks do not defend against a hostile local actor racing path + components between inspection and use; +- the watcher is a convenience mechanism, not a filesystem-integrity monitor; +- human-readable diagnostics can include hostile local filenames or child-tool + output and should not be treated as a sanitized log protocol; +- development CSP rewriting is convenience, not production CSP validation; +- deliberately detached child descendants may evade process-tree cleanup; +- rendering has no built-in recursion, output-size, allocation, CPU, panic, or + deadline enforcement; +- static cycle detection and trust-use warnings are best-effort analyses; and +- the project has no independent security audit or bug-bounty program. + +## Interpreting this ledger + +“Pass” means the named command or case produced its expected result in the named +environment on the assessment date. It does not mean “secure.” Confidence comes +from keeping the boundary small, making risky capabilities explicit, preserving +ordinary generated Go for review, publishing reproducible tests, recording +failures, and correcting claims when evidence is weaker than the prose. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 352c092..18f1889 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -2,38 +2,183 @@ # Threat model -## Trusted +This document separates demonstrated behavior from intended release work. It +defines the boundary Sandwich Hime can reasonably defend; it is not a claim +that the project or an application using it is universally secure. -- `.sando` files and embedded Go statements; -- handwritten application Go; -- explicit calls to `sando.TrustHTML`, `TrustURL`, `TrustJS`, and `TrustCSS`; -- the selected compiler binary and runtime module version. +## Security objective -## Untrusted +For supported HTML contexts, data supplied to a compiler-generated component +should remain data. It must not change HTML structure, create executable code, +escape a quoted attribute, or introduce a disallowed URL scheme unless trusted +application source makes an explicit security-sensitive decision. -- values supplied to components unless deliberately wrapped in a trusted type; +That objective follows the same high-level model documented by Go's +`html/template`: template authors are trusted while rendered data is not. The +implementations and accepted languages differ. A systematic differential test +campaign against `html/template` remains open work; current tests cover fixed +adversarial cases and do not establish equivalence. + +## Trusted capabilities + +- `.sando` source, including its static markup and embedded Go statements; +- handwritten application Go and values whose formatting methods execute Go; +- handwritten implementations of `sando.Component`; +- explicit `sando.TrustHTML`, `TrustURL`, `TrustJS`, and `TrustCSS` calls; +- the selected compiler binary, Go toolchain, runtime module, and generated Go; +- local project code built and executed by `himesan dev`; and +- the user account, filesystem, environment, and other processes on the + development workstation. + +Template semantics become trusted source when built into an application; +templates are not an untrusted-content sandbox. Someone allowed to edit one can +execute ordinary Go through the application build and must receive the same +trust as any other code contributor. Arbitrary or malformed template bytes +remain adversarial input to compiler robustness while they are being inspected. + +A handwritten `sando.Component` is a trusted output capability. It may write +arbitrary bytes, change HTML parser context, recurse, block, panic, or perform +side effects. Hime-generated components are independently checked for balanced +HTML and may be inserted with ` + +# v1.0.0 launch initiative + +Sandwich Hime v1 is a compatibility and evidence milestone, not a reason to +accumulate features. The intended product is already visible: an HTML-first, +ahead-of-time template engine for Go, typed generated components, a small +HTTP-independent runtime, and an optional opinionated local development loop. + +The private development initiative lives on `codex/v1-launch` in +`sandwich-hime-dev`. Public releases never originate from that private history. + +## Repository and publication topology + +| Surface | Purpose | History and tags | +| --- | --- | --- | +| Private Gitea `sandwich-hime-dev` | development, working branches, private review records, and historical context | normal private history; no public release tags | +| Public Gitea `sandwich-hime` | canonical sanitized source, contribution venue, module origin, releases, and signed tags | fresh reviewed publication history; authoritative `sando/vX.Y.Z` and `vX.Y.Z` tags | +| GitHub `gamertan/sandwich-hime` | discoverability and a convenient sanitized source snapshot | no private refs, force-mirrors, workflows, contribution authority, release artifacts, or semver tags | + +Each public update is exported through the exact committed allowlist, inspected, +committed as a fresh public snapshot, and compared byte-for-byte with the +reviewed export. GitHub receives that public tree only. It never receives the +private repository or an indiscriminate Git mirror. + +## Current readiness + +At the 2026-08-12 v1 initiative baseline, the project is a strong engineering +preview, but not yet a release candidate. Exact private/public commit mappings +remain in the non-exported operational ledger. + +### Demonstrated now + +- race-enabled tests, vet, builds, deterministic generation, license checks, + and sanitized-export tests pass on Linux; +- the compiler module and nested `sando` runtime declare zero third-party Go + module requirements; +- generated/runtime compatibility uses a version-specific compile-time ABI + marker with an incompatible-runtime regression; +- owned-output, orphan, stale, symlink, nested-module, restrictive-permission, + last-good, writer-error, and enumerated contextual-output cases are tested; +- the development proxy is loopback-only, browser-origin hardened, and clears + a dead selected upstream immediately; and +- the security policy, threat model, and dated evidence ledger state both the + demonstrated controls and the unresolved limits. + +### Not demonstrated yet + +- native Windows and macOS execution of the complete supported matrix; +- a stable public API/CLI/schema snapshot and compatibility test; +- systematic browser-parser and `html/template` differential testing; +- a long semantic fuzz campaign beyond bounded no-panic smoke; +- committed, reproducible comparative benchmarks and a predefined regression + threshold; +- real-browser SSE/reload/overlay evidence for `himesan dev`; +- deterministic release artifacts, checksums, SBOMs, signatures, and tested + signing/recovery procedures; or +- clean direct and public-proxy installation of signed release tags. + +## Milestone 1: contract freeze + +Required before security/platform release-candidate work is declared complete: + +- [ ] Decide and specify whether generic component function signatures are v1. +- [ ] Inventory and freeze every exported `sando` symbol, trusted type, + sentinel error, concrete error field, helper, and ABI marker. +- [ ] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas, + `himesan.json` schema, and generated provenance fields. +- [ ] Specify nil/stringification behavior, supported HTML-context matrix, + component trust boundary, URL semantics, and explicit unsupported cases. +- [ ] Add machine-checked public API, CLI, diagnostic, schema, and generated + output compatibility snapshots. +- [ ] Define the v1 deprecation and security-support policy. + +## Milestone 2: security and native-platform evidence + +- [ ] Run the minimum supported Go line and the latest two stable Go lines on + native Linux, macOS, and Windows hosts. +- [ ] Prove identical generated bytes across those hosts and exercise native + path, replacement, permission, race, process-tree, and watcher behavior. +- [ ] Build a systematic differential corpus against Go's documented + `html/template` safety baseline for overlapping supported contexts. +- [ ] Parse representative outputs in real browsers and test structure/code + invariants rather than only byte equality. +- [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports, + paths, source maps, URL normalization, and filesystem operations. +- [ ] Resolve or explicitly accept every open item in + `SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee. +- [ ] Test delivery and reply through `security@sandwichhime.com`. +- [ ] Define severity, advisory, retraction, and CVE-request handling. + +## Milestone 3: measured performance and development UX + +- [ ] Commit a synthetic, repository-owned benchmark corpus comparing + equivalent typed views and output with `html/template`. +- [ ] Define “no material regression” before measuring the release candidate; + publish hardware, OS, Go version, commands, samples, allocations, and output + equivalence with every result. +- [ ] Test SSE reconnect, reload, diagnostic overlays, CSP changes, fragment/API + exclusions, caching, and child cleanup in a real browser on supported hosts. +- [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced + reliably instead of substituting prose for a test. + +## Milestone 4: release rehearsal + +- [ ] Make version validation identical in the CLI, generated headers, scripts, + and release artifacts; reject ambiguous build metadata. +- [ ] Build the candidate compiler at its candidate version and prove its + committed outputs are current under that exact binary. +- [ ] Produce deterministic archives/binaries, checksums, SBOMs, signatures, + and source/build provenance from a clean sanitized canonical checkout. +- [ ] Test release-key backup and two-person recovery for Gitea, domains, + signing material, and publication instructions. +- [ ] Make evidence gates validate content and commit identity rather than only + the presence of non-empty files. +- [ ] Rehearse runtime-first publication and rollback without creating public + semver tags. + +## Milestone 5: release candidates and final launch + +1. Export and review the sanitized canonical release tree. +2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same + reviewed public Gitea commit. +3. Verify documented installs through fresh `GOPROXY=direct` and + `proxy.golang.org` caches on supported Go versions and native platforms. +4. Run the complete evidence suite again from the exact public commit. +5. Operate the official Sandwich Hime website on the RC runtime for a 14-day + observation period with no unresolved Hime render, security, accessibility, + or rollback regression. This is product dogfooding, not a dependency on EQL + or another application's private repository. +6. Fix findings in a new RC; restart the observation period when the affected + boundary warrants it. +7. Finalize the changelog, supported-version table, migration notes, release + notes, legal/trademark review, checksums, SBOMs, and signatures. +8. Publish `sando/v1.0.0` first and `v1.0.0` second. Never move a tag. +9. Refresh the untagged GitHub discovery snapshot and point it to canonical + Gitea releases and contribution channels. + +## Explicitly deferrable after v1 + +Unless testing finds a release-blocking consequence, v1 need not include every +possible context, hostile-local filesystem hardening, typed trust-flow analysis, +complete dynamic cycle detection, an encrypted reporting key, or an external +audit. Those limits must remain visible and must not be contradicted by +marketing. New features do not outrank a small stable contract. + +## Definition of confidence + +“Ready for v1” means a reviewer can trace each promise to a stable public +contract, executable evidence from supported native environments, and a signed +artifact built from the exact canonical source. It does not mean perfect, +invulnerable, or finished forever. diff --git a/internal/compiler/abi_test.go b/internal/compiler/abi_test.go new file mode 100644 index 0000000..6aa3818 --- /dev/null +++ b/internal/compiler/abi_test.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package compiler + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestGeneratedCodeRequiresVersionedRuntimeABIMarker(t *testing.T) { + if testing.Short() { + t.Skip("skipping temporary-module ABI compilation in short mode") + } + t.Parallel() + + directory := resolvedTempDir(t) + templatePath := filepath.Join(directory, "page.sando") + mustWrite(t, templatePath, "") + result, err := Generate(context.Background(), []string{templatePath}) + if err != nil { + t.Fatalf("Generate: %v (%v)", err, result.Diagnostics) + } + generated := string(mustRead(t, templatePath+".go")) + if !strings.Contains(generated, ".ABISandoV1") { + t.Fatalf("generated code does not require the sando.v1 marker:\n%s", generated) + } + + mustWrite(t, filepath.Join(directory, "go.mod"), `module example.test/abi + +go 1.25 + +require gamertan.com/sandwich-hime/sando v0.0.0 + +replace gamertan.com/sandwich-hime/sando => ./fake-sando +`) + mustWrite(t, filepath.Join(directory, "fake-sando", "go.mod"), `module gamertan.com/sandwich-hime/sando + +go 1.25 +`) + mustWrite(t, filepath.Join(directory, "fake-sando", "component.go"), `package sando + +import ( + "context" + "io" +) + +const ABI = "sando.incompatible" + +type Component interface { Render(context.Context, io.Writer) error } +type ComponentFunc func(context.Context, io.Writer) error +func (f ComponentFunc) Render(ctx context.Context, w io.Writer) error { return f(ctx, w) } +`) + + command := exec.Command("go", "test", "./...") + command.Dir = directory + command.Env = append(os.Environ(), "GOWORK=off") + output, buildErr := command.CombinedOutput() + if buildErr == nil { + t.Fatalf("generated code compiled against an incompatible runtime:\n%s", output) + } + if !strings.Contains(string(output), "undefined: __himesan_sando.ABISandoV1") { + t.Fatalf("incompatible runtime failed for an unexpected reason: %v\n%s", buildErr, output) + } +} diff --git a/internal/compiler/backend.go b/internal/compiler/backend.go index eb669d1..3c27e44 100644 --- a/internal/compiler/backend.go +++ b/internal/compiler/backend.go @@ -92,7 +92,10 @@ func generateGo(file *sourceFile) ([]byte, []Diagnostic) { } } output.WriteString(")\n\n") - fmt.Fprintf(&output, "var _ = %s.ABI\n\n", imports.Sando) + // A version-specific exported marker makes the generated/runtime ABI a Go + // build-time contract. The descriptive ABI string alone cannot enforce + // compatibility because constant values are not part of symbol resolution. + fmt.Fprintf(&output, "var _ = %s.ABISandoV1\n\n", imports.Sando) fmt.Fprintf(&output, "func %s%s%s %s.Component {\n", file.Name, file.TypeParams, file.Params, imports.Sando) fmt.Fprintf(&output, "\treturn %s.ComponentFunc(func(%s %s.Context, %s %s.Writer) error {\n", imports.Sando, contextName, imports.Context, writerName, imports.IO) fmt.Fprintf(&output, "\t\t_ = %s\n", contextName) diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index 3339f2b..868d1f6 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -340,6 +340,68 @@ func TestGenerateRefusesUnownedOutput(t *testing.T) { } } +func TestDirectoryOperationsRejectOrphanedOwnedOutputBeforeWrites(t *testing.T) { + t.Parallel() + directory := resolvedTempDir(t) + orphanSource := filepath.Join(directory, "orphan.sando") + mustWrite(t, orphanSource, simpleSource("Orphan", "last good")) + if _, err := Generate(context.Background(), []string{directory}); err != nil { + t.Fatal(err) + } + orphanOutput := orphanSource + ".go" + lastGood := mustRead(t, orphanOutput) + if err := os.Remove(orphanSource); err != nil { + t.Fatal(err) + } + + liveSource := filepath.Join(directory, "live.sando") + mustWrite(t, liveSource, simpleSource("Live", "must not be written")) + // A handwritten file whose name merely resembles an output is not owned by + // Hime-san and must not be treated as an orphan. + mustWrite(t, filepath.Join(directory, "handwritten.sando.go"), "package demo\n") + + checked, err := Check(context.Background(), []string{directory}) + if err == nil { + t.Fatalf("orphaned owned output unexpectedly passed check: %+v", checked) + } + assertDiagnosticCode(t, checked.Diagnostics, "HIM2014") + + generated, err := Generate(context.Background(), []string{directory}) + if err == nil { + t.Fatalf("orphaned owned output unexpectedly allowed generation: %+v", generated) + } + assertDiagnosticCode(t, generated.Diagnostics, "HIM2014") + if !bytes.Equal(lastGood, mustRead(t, orphanOutput)) { + t.Fatal("orphaned last-good output changed") + } + if _, statErr := os.Stat(liveSource + ".go"); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("batch wrote a live output despite the orphan diagnostic: %v", statErr) + } +} + +func TestNewGeneratedOutputInheritsRestrictiveSourceMode(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX file mode test") + } + t.Parallel() + directory := resolvedTempDir(t) + path := filepath.Join(directory, "private.sando") + mustWrite(t, path, simpleSource("Private", "private")) + if err := os.Chmod(path, 0o600); err != nil { + t.Fatal(err) + } + if _, err := Generate(context.Background(), []string{path}); err != nil { + t.Fatal(err) + } + info, err := os.Stat(path + ".go") + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o600 { + t.Fatalf("generated output mode = %04o, want 0600", got) + } +} + func TestDiscoveryBoundariesAndExplicitNestedFile(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink creation commonly requires additional Windows privileges") diff --git a/internal/compiler/context.go b/internal/compiler/context.go index 666e25e..ef52ca7 100644 --- a/internal/compiler/context.go +++ b/internal/compiler/context.go @@ -34,7 +34,8 @@ const ( ) type contextAnalyzer struct { - file *sourceFile + file *sourceFile + positions positionTable state htmlState currentTag string @@ -79,7 +80,12 @@ var unsupportedDynamicAttributes = map[string]string{ } func analyzeContexts(file *sourceFile) []Diagnostic { - analyzer := &contextAnalyzer{file: file, state: htmlData, attrFirstDynamic: -1} + analyzer := &contextAnalyzer{ + file: file, + positions: newPositionTable(file.Source), + state: htmlData, + attrFirstDynamic: -1, + } var diagnostics []Diagnostic for nodeIndex := range file.Nodes { node := &file.Nodes[nodeIndex] @@ -136,20 +142,20 @@ func analyzeContexts(file *sourceFile) []Diagnostic { } } + end := analyzer.positions.at(len(file.Source)) if analyzer.state != htmlData { - diagnostics = append(diagnostics, diagnostic(file.Path, endPosition(file.Source), "HIM1310", "template ends in an incomplete or ambiguous HTML parser context")) + diagnostics = append(diagnostics, diagnostic(file.Path, end, "HIM1310", "template ends in an incomplete or ambiguous HTML parser context")) } if len(analyzer.stack) != 0 { - diagnostics = append(diagnostics, diagnostic(file.Path, endPosition(file.Source), "HIM1311", fmt.Sprintf("component must finish in its starting HTML context; unclosed <%s>", analyzer.stack[len(analyzer.stack)-1]))) + diagnostics = append(diagnostics, diagnostic(file.Path, end, "HIM1311", fmt.Sprintf("component must finish in its starting HTML context; unclosed <%s>", analyzer.stack[len(analyzer.stack)-1]))) } return diagnostics } func (a *contextAnalyzer) consumeText(text string, start sourcePosition) *Diagnostic { - positionTable := newPositionTable(a.file.Source) for index := 0; index < len(text); index++ { b := text[index] - position := positionTable.at(start.Offset + index) + position := a.positions.at(start.Offset + index) if a.rawTag == "script" { a.scriptTail += string(b) if len(a.scriptTail) > len("