bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+351
View File
@@ -0,0 +1,351 @@
//go:build !windows
package main
// Gates for ctx_extract (contextextract.go) — the op that turns a user's
// document into text ON THEIR MACHINE.
//
// These drive the REAL mgmt frame path with driveOp, so what is measured is what
// a host would actually get back: the caps gate, the argv, the refusals, and the
// text itself. The PDF below is a real one-page document built by hand (620
// bytes) rather than a fixture nobody can regenerate.
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
)
// A valid one-page PDF whose visible text is exactly probePDFText. 11pt on a
// 612x792 MediaBox — at 24pt the string ran off the page and pdftotext returned
// it TRUNCATED, which would have made a full-string assertion fail for a reason
// that has nothing to do with this code.
const probePDFText = "PRIVATAE EXTRACTION PROBE alpha bravo charlie"
const probePDFB64 = "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5" +
"cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVu" +
"dCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAv" +
"Q29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NiA+PgpzdHJlYW0KQlQgL0YxIDExIFRmIDcy" +
"IDcwMCBUZCAoUFJJVkFUQUUgRVhUUkFDVElPTiBQUk9CRSBhbHBoYSBicmF2byBjaGFybGllKSBUaiBFVAplbmRzdHJlYW0K" +
"ZW5kb2JqCjUgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUxIC9CYXNlRm9udCAvSGVsdmV0aWNhID4+CmVu" +
"ZG9iagp4cmVmCjAgNgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBu" +
"IAowMDAwMDAwMTE1IDAwMDAwIG4gCjAwMDAwMDAyNDEgMDAwMDAgbiAKMDAwMDAwMDM2NyAwMDAwMCBuIAp0cmFpbGVyCjw8" +
"IC9TaXplIDYgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjQzNwolJUVPRgo="
// driveExtract runs one ctx_extract job through the REAL mgmt frame path with a
// CONFIGURED context root.
//
// driveOp (anchor_test.go) passes empty roots, which is right for the ops it was
// written for but makes every rootCtx op answer "context store not configured" —
// and a test that asserts "was refused" would then pass on that refusal while
// proving nothing about this code. Measured: three of these tests went green
// that way before this helper existed.
func driveExtract(t *testing.T, self string, args []string) (string, string, int) {
t.Helper()
out, errOut, code, _ := driveExtractIn(t, self, args)
return out, errOut, code
}
// driveExtractIn additionally hands back the context root it used, so a test can
// look at what the call left on disk.
func driveExtractIn(t *testing.T, self string, args []string) (string, string, int, string) {
t.Helper()
ctxRoot := t.TempDir()
up := websocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ws, err := up.Upgrade(w, r, nil)
if err != nil {
return
}
defer ws.Close()
handleMgmtConnFor(ws, self, ctxRoot, "", "")
}))
defer srv.Close()
c, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil)
if err != nil {
t.Fatalf("dial: %v", err)
}
defer c.Close()
job, _ := json.Marshal(mgmtJob{Op: "ctx_extract", Args: args, Approval: testApproval(t, "ctx_extract", args)})
if err := c.WriteMessage(websocket.BinaryMessage, append([]byte("C"), job...)); err != nil {
t.Fatalf("write: %v", err)
}
var out, errOut string
for {
_, data, err := c.ReadMessage()
if err != nil {
t.Fatalf("read: %v", err)
}
switch data[0] {
case 'O':
out += string(data[1:])
case 'R':
errOut += string(data[1:])
case 'X':
var code int
fmt.Sscanf(string(data[1:]), "%d", &code)
return out, errOut, code, ctxRoot
}
}
}
// THE DOCUMENT MUST NOT SURVIVE THE CALL. It is the user's file, handed over for
// one job; a scratch copy left behind in the pairing's root would be this lane
// quietly accumulating their documents on their own disk, which is the opposite
// of the reason extraction runs here at all. Checked on BOTH paths — a tool that
// fails must clean up exactly as well as one that succeeds.
func TestCtxExtractLeavesNothingOnDisk(t *testing.T) {
setupBox(t)
if ok, _ := probePrepTool(prepTool{Name: "pdf-text", Bin: "pdftotext", Args: []string{"-v"}}); !ok {
t.Skip("pdftotext not installed")
}
for _, tc := range []struct {
name, b64 string
wantCode int
}{
{"a document it can read", probePDFB64, 0},
{"a document it cannot", base64.StdEncoding.EncodeToString([]byte("not a pdf")), 1},
} {
_, _, code, root := driveExtractIn(t, tAnchor, []string{extractReq(t, "pdf-text", tc.b64)})
if code != tc.wantCode {
t.Fatalf("%s: code=%d want %d", tc.name, code, tc.wantCode)
}
left, err := os.ReadDir(root)
if err != nil {
t.Fatal(err)
}
if len(left) != 0 {
names := []string{}
for _, e := range left {
names = append(names, e.Name())
}
t.Fatalf("%s: the user's document was left in the context root: %v", tc.name, names)
}
}
}
// A configured root must NOT be what makes this safe: the same job on a pairing
// without the dcn family is refused before the handler ever sees the root.
func extractReq(t *testing.T, tool, b64 string) string {
t.Helper()
b, err := json.Marshal(ctxExtractReq{Tool: tool, Mime: "application/pdf", B64: b64})
if err != nil {
t.Fatal(err)
}
return string(b)
}
// THE WHOLE FEATURE IN ONE ASSERTION: a real PDF in, its real text out, over the
// real management frame.
func TestCtxExtractReturnsThePDFsText(t *testing.T) {
setupBox(t)
if ok, _ := probePrepTool(prepTool{Name: "pdf-text", Bin: "pdftotext", Args: []string{"-v"}}); !ok {
t.Skip("pdftotext not installed on this box — a skip proves nothing, install poppler-utils")
}
out, errOut, code := driveExtract(t, tAnchor, []string{extractReq(t, "pdf-text", probePDFB64)})
if code != 0 {
t.Fatalf("ctx_extract failed: code=%d err=%s", code, errOut)
}
var resp ctxExtractResp
if err := json.Unmarshal([]byte(out), &resp); err != nil {
t.Fatalf("reply is not JSON: %v (%q)", err, out)
}
if !strings.Contains(resp.Text, probePDFText) {
t.Fatalf("extracted text does not contain the document's words.\n want substring: %q\n got: %q",
probePDFText, resp.Text)
}
if resp.Tool != "pdf-text" || resp.Chars != len(resp.Text) || resp.Truncated {
t.Fatalf("reply metadata disagrees with its own text: %+v", resp)
}
}
// The caps gate, driven through the real frame. A pairing granted another family
// must not be able to make this machine run a program — this is the assertion
// that would fail if ctx_extract were ever renamed out of the ctx_ prefix, since
// opFamily returns "" for an unknown op and capsAllow then ALLOWS it.
func TestCtxExtractRefusedWithoutTheDCNFamily(t *testing.T) {
priv := setupBox(t)
writePairingState(t, tMember, pairingPending)
future := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
terms, sig := signTerms(t, priv, grantTerms(tMember, []string{"observe"}, future))
if _, errOut, code := driveOp(t, tAnchor, "pairing_grant_sync", []string{terms, sig}); code != 0 {
t.Fatalf("could not install the observe-only grant: %d %s", code, errOut)
}
_, errOut, code := driveExtract(t, tMember, []string{extractReq(t, "pdf-text", probePDFB64)})
if code == 0 {
t.Fatal("a pairing granted only 'observe' ran an extractor on this machine")
}
if !contains(errOut, "dcn") && !contains(errOut, "caps") {
t.Fatalf("refused, but not for the caps reason: %q", errOut)
}
}
// Only a tool this build NAMES may run. Advertising a capability must never by
// itself be enough to execute something.
func TestCtxExtractRefusesAnUnknownTool(t *testing.T) {
setupBox(t)
// "transcribe" was the not-yet-a-job example here and is a real job now
// (contextstt.go), so it is replaced rather than left to pass for the wrong
// reason: it was still refused, but as "this is not audio" instead of "this
// build has no such tool", which is not what this test asserts. "translate"
// takes its place and is deliberately a job this lane might plausibly gain —
// a name nobody would ever add is a weak stand-in.
for _, tool := range []string{"", "rm", "pdf-text; rm -rf /", "translate"} {
_, errOut, code := driveExtract(t, tAnchor, []string{extractReq(t, tool, probePDFB64)})
if code == 0 {
t.Fatalf("tool %q was accepted — only names in extractArgv may run", tool)
}
if !contains(errOut, "unknown tool") && !contains(errOut, "tool table") {
t.Fatalf("tool %q refused for the wrong reason: %q", tool, errOut)
}
}
}
// Over the cap is REFUSED WITH ITS SIZE — never silently truncated, never partly
// extracted. The number has to be in the message or a caller cannot tell whether
// to split the document or wait for the pull lane.
func TestCtxExtractRefusesAnOversizeDocumentWithItsSize(t *testing.T) {
setupBox(t)
big := base64.StdEncoding.EncodeToString(make([]byte, ctxExtractMaxBytes+1))
_, errOut, code := driveExtract(t, tAnchor, []string{extractReq(t, "pdf-text", big)})
if code == 0 {
t.Fatal("a document over the cap was accepted")
}
if !contains(errOut, "8388609") || !contains(errOut, "8388608") {
t.Fatalf("the refusal must state the document's size AND the cap, got: %q", errOut)
}
if contains(errOut, "truncat") && !contains(errOut, "not truncated") {
t.Fatalf("refusal must not imply truncation: %q", errOut)
}
}
// A document the tool cannot read fails with the TOOL's own words, not a generic
// error — a password-protected PDF should say so.
func TestCtxExtractSurfacesTheToolsOwnFailure(t *testing.T) {
setupBox(t)
if ok, _ := probePrepTool(prepTool{Name: "pdf-text", Bin: "pdftotext", Args: []string{"-v"}}); !ok {
t.Skip("pdftotext not installed")
}
junk := base64.StdEncoding.EncodeToString([]byte("this is definitely not a PDF"))
_, errOut, code := driveExtract(t, tAnchor, []string{extractReq(t, "pdf-text", junk)})
if code == 0 {
t.Fatal("a non-PDF was accepted as extractable")
}
if !contains(errOut, "could not read this document") {
t.Fatalf("unhelpful failure: %q", errOut)
}
}
// Garbage in the envelope is refused before anything is written to disk or run.
func TestCtxExtractRefusesBadInput(t *testing.T) {
setupBox(t)
cases := map[string]string{
"not json": "{not json",
"not base64": extractReq(t, "pdf-text", "!!!! not base64 !!!!"),
"empty document": extractReq(t, "pdf-text", ""),
}
for name, arg := range cases {
if _, _, code := driveExtract(t, tAnchor, []string{arg}); code == 0 {
t.Fatalf("%s was accepted", name)
}
}
if _, _, code := driveExtract(t, tAnchor, nil); code == 0 {
t.Fatal("a job with no argument at all was accepted")
}
}
// ── the findings, as gates ──────────────────────────────────────────────────
// OUTPUT IS BOUNDED AS IT ARRIVES, not after the process exits. Extracted text
// is not proportional to the input: a 3.2 MB PDF whose 20,000 pages share one
// compressed stream made pdftotext emit ~83 MB and peaked the bastion at 211 MiB
// heap / 306 MiB allocated (measured). Capping the REPLY is too late —
// by then the bytes are already in memory, on the user's machine, times the
// worker count.
func TestRunExtractBoundsOutputAsItArrives(t *testing.T) {
// 64 MiB of 'x' down stdout from a process that exits 0.
out, timedOut, err := runExtract(20*time.Second, "sh", "-c",
"head -c 67108864 /dev/zero | tr '\\0' 'x'")
if err != nil || timedOut {
t.Fatalf("the producer should have succeeded: err=%v timedOut=%v", err, timedOut)
}
if len(out) > ctxExtractMaxText+1 {
t.Fatalf("captured %d bytes; the cap is %d — output is not bounded as it "+
"arrives, so a runaway document costs whatever it feels like",
len(out), ctxExtractMaxText+1)
}
}
// A DEADLINE IS NOT A BAD DOCUMENT. Before this split, a timeout was reported as
// "could not read this document: <the first 400 chars OF THE USER'S OWN FILE>",
// presented to the model as the tool's explanation.
func TestRunExtractSeparatesATimeoutFromAFailure(t *testing.T) {
_, timedOut, _ := runExtract(300*time.Millisecond, "sh", "-c", "sleep 5")
if !timedOut {
t.Fatal("a command that outlived its deadline must report timedOut")
}
_, timedOut2, err := runExtract(5*time.Second, "sh", "-c", "echo nope >&2; exit 3")
if timedOut2 {
t.Fatal("a command that FAILED must not be reported as a timeout")
}
if err == nil {
t.Fatal("a non-zero exit must surface as an error")
}
}
// Scratch left by a process that died mid-job is swept at start — nothing else
// would ever notice one of the user's documents sitting there.
func TestSweepStaleScratch(t *testing.T) {
root := t.TempDir()
for _, n := range []string{"extract-111", "extract-222"} {
if err := os.WriteFile(filepath.Join(root, n), []byte("a user's document"), 0o600); err != nil {
t.Fatal(err)
}
}
keep := filepath.Join(root, "ctx-blob-keep")
if err := os.WriteFile(keep, []byte("not ours"), 0o600); err != nil {
t.Fatal(err)
}
if n := sweepStaleScratch(root); n != 2 {
t.Fatalf("swept %d, want 2", n)
}
if _, err := os.Stat(keep); err != nil {
t.Fatal("the sweep removed a file that was not extraction scratch")
}
if n := sweepStaleScratch(""); n != 0 {
t.Fatal("an unconfigured root must sweep nothing")
}
}
// An oversize document is refused WITHOUT being decoded. The frame already
// exists three times by the time the handler sees it (the websocket message,
// job.Args[0], req.B64); decoding first added a fourth copy of a body about to
// be refused. The estimate is the cheap gate; the exact check still runs after.
func TestCtxExtractRefusesOversizeBeforeDecoding(t *testing.T) {
setupBox(t)
// Deliberately NOT valid base64. If the handler decodes before checking the
// size, it fails with "body is not base64" — which is how this test knows
// the order regressed, rather than merely that something was refused.
big := strings.Repeat("!", (ctxExtractMaxBytes/3)*4+64)
_, errOut, code := driveExtract(t, tAnchor, []string{extractReq(t, "pdf-text", big)})
if code == 0 {
t.Fatal("an oversize body was accepted")
}
if contains(errOut, "not base64") {
t.Fatalf("the body was DECODED before its size was checked — the size "+
"gate must come first: %q", errOut)
}
if !contains(errOut, "over the") {
t.Fatalf("refused, but not for size: %q", errOut)
}
}