From e94e107071223b2504963dfa95ccd1f2293da752 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 19:29:27 +0200 Subject: plan 8: threat model, make fuzz, race and vulncheck --- Makefile | 22 +++++++++++++++++++++- docs/design.md | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 62e2b7e..e09a3be 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ VERSION != git describe --tags --always --dirty 2>/dev/null || echo dev LDFLAGS = -s -w -X main.version=$(VERSION) CROSS_PLATFORMS = linux/amd64 linux/arm64 freebsd/amd64 openbsd/amd64 -.PHONY: all help build install uninstall test vet fmt lint ci bench cross dist release deps install-hooks clean +.PHONY: all help build install uninstall test vet fmt lint ci bench fuzz race vulncheck cross dist release deps install-hooks clean all: build @@ -79,6 +79,26 @@ ci: ## the gate: gofmt, vet, tests, dependencies, no personal data staged, man p bench: ## run the Go benchmarks on generated trees go test -run '^$$' -bench . -benchmem ./... +# FUZZ_TARGETS lists every fuzz target as package-directory:FuzzName. +FUZZTIME ?= 20s +FUZZ_TARGETS = \ + internal/sexp:FuzzParse + +fuzz: ## run every fuzz target for FUZZTIME each (default 20s); a crasher is saved under testdata/fuzz + @for t in $(FUZZ_TARGETS); do \ + pkg=$${t%%:*}; name=$${t##*:}; \ + echo "fuzz $$pkg $$name ($(FUZZTIME))"; \ + go test -run '^$$' -fuzz "^$$name$$" -fuzztime $(FUZZTIME) ./$$pkg || exit 1; \ + done + +race: ## run the tests under the race detector (needs cgo and a C compiler) + CGO_ENABLED=1 go test -race ./... + +# govulncheck v1.1.4 is the newest release that builds with Go 1.24 +# (v1.2.0 and later need Go 1.25); GOTOOLCHAIN=local stops a toolchain download. +vulncheck: ## check the standard library and dependencies against the Go vulnerability database (network) + GOTOOLCHAIN=local go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./... + cross: ## cross-compile linux/amd64, linux/arm64, freebsd/amd64, openbsd/amd64 into dist/krino-$(VERSION)--/ @case '$(VERSION)' in \ ''|*[!A-Za-z0-9._+-]*) \ diff --git a/docs/design.md b/docs/design.md index 331993a..fb8988c 100644 --- a/docs/design.md +++ b/docs/design.md @@ -8,7 +8,8 @@ amended again 2026-09-14 for 0.0.4: `max-size` (§4.4), `(exclude ...)` (§4.2, §4.3, §4.6) and `--min-age` (§11); amended again 2026-09-14 for 0.0.5: the keyword cache (§3, §6.1, §13), and `t`, `d` and `w` in review (§8.3, §10); amended again 2026-09-14 for 0.0.6: `w` applies and quits, and -each choice is echoed in red (§8.2, §8.3). +each choice is echoed in red (§8.2, §8.3); amended again 2026-09-14 for +0.0.7: the threat model and its tests (§15.1). krino (from Greek κρίνω, "to separate, to judge, to decide") sorts files in chosen directories by rules. A rule tests a file's type, name, path, size, @@ -806,6 +807,44 @@ placeholder numbers, screenshots by date, cleaning up old installers). `make ci` is the gate: gofmt, vet, tests, the leak check, man page lint. +### 15.1 Hostile input + +krino runs on folders the internet writes into, so it treats as hostile +every file name, every file's contents, and the output of the tools it runs +on them. The configuration is trusted: it is the user's own. The state +files (the log, the keyword cache, the Trash) are the user's own too: krino +survives their corruption — a truncated line, a damaged cache, a +hand-edited trashinfo — but does not defend against another local process +with write access to them, which could already do anything krino can. + +What that means, and the tests that hold it (plan 8): + +- A name never controls the terminal. Every name, path, reason and warning + is printed through `display`, which shows C0 controls, DEL, C1 controls, + Unicode bidirectional embeddings, overrides and isolates, and invalid + UTF-8 as escapes (`\x1b`, `\u202e`). +- A name never redirects a step. A rename whose placeholders produce "", + "." or "..", and a destination whose placeholders add a ".." segment, are + skipped with a reason. +- A file is acted on only while it is still the file that was planned: + same size and modification time, still a regular file (not a symlink put + in its place), and, at its planned path, the same inode. +- A file reaches an external tool only as an absolute path, so a name + starting with `-` is never read as an option. +- A trash entry name from the log must be a plain name inside the Trash, + and a trashinfo `Path` must be absolute. +- Every decoder of outside data is fuzzed (`make fuzz`): the config reader, + sizes and durations, placeholders, ignore patterns, the log's escaping, + trashinfo paths, the keyword cache, markup and zip extraction, and text + normalisation. +- For generated trees and rules, apply then undo restores every file, and + nothing is lost in between (property test, `KRINO_PROPERTY_RUNS`). +- Every value of every enum the output, the log or planning depends on is + handled: tests read the constants from source. +- `make vulncheck` checks the standard library and dependencies against + the Go vulnerability database; `make race` runs the tests under the race + detector. + ## 16. Lessons from the prototype krino replaces a Python prototype that sorted a downloads folder by -- cgit v1.3