From 8e88dded4be065362fbd2e2b39f7a68b9b0b7d65 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 3 Oct 2026 17:24:08 +0200 Subject: [PATCH] test(api): race the pdf/docmark temp-copy path too PR #156 round 4: TextIn's pdf/docmark branch copies the file through the root to a temp file, and pointing the extractor back at the raw/ path by name stayed green (race tests used only r.md); a stub docmark then got the outside file's bytes 34k times in 4s. TestDocmarkExtractionLeaksNothingUnderASymlinkSwapRace races a symlink into raw/r.docx through ReadIngestFile while a fake docmark counts the sentinel in every document it receives. Flipped: the by-name revert is red. Refs #153 Co-Authored-By: Claude Opus 5.5 --- ingestion/internal/api/raw_race_test.go | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/ingestion/internal/api/raw_race_test.go b/ingestion/internal/api/raw_race_test.go index 580c457..1c7b2bd 100644 --- a/ingestion/internal/api/raw_race_test.go +++ b/ingestion/internal/api/raw_race_test.go @@ -1,6 +1,10 @@ package api_test import ( + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -110,3 +114,59 @@ func TestRawReadersDoNotHangUnderAFIFOSwapRace(t *testing.T) { t.Fatalf("a raw/ reader hung on a swapped-in FIFO after %d iterations", iters.Load()) } } + +// PR #156 round 4: the pdf/docmark branch of TextIn copies the file through +// the root to a temp file; pointing the extractor back at the raw/ path by +// name stayed green because the race tests only used r.md (a stub docmark +// then received the outside file's bytes 34k times in 4s). This races a +// symlink into raw/r.docx and has a fake docmark count the sentinel in every +// document it is sent. +func TestDocmarkExtractionLeaksNothingUnderASymlinkSwapRace(t *testing.T) { + var seen atomic.Int64 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Params struct { + Arguments struct { + ContentBase64 string `json:"content_base64"` + } `json:"arguments"` + } `json:"params"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + if b, err := base64.StdEncoding.DecodeString(req.Params.Arguments.ContentBase64); err == nil && + strings.Contains(string(b), raceSentinel) { + seen.Add(1) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"converted"}]}}`)) + })) + defer srv.Close() + t.Setenv("DOCMARK_URL", srv.URL) + + brain, raw, outside := raceBrain(t) + tgt := filepath.Join(raw, "r.docx") + if err := os.WriteFile(tgt, []byte("inside docx bytes"), 0o644); err != nil { + t.Fatal(err) + } + var stop atomic.Bool + go func() { + for i := 0; !stop.Load(); i++ { + tmp := filepath.Join(raw, ".swapdocx") + _ = os.Remove(tmp) + if i%2 == 0 { + _ = os.WriteFile(tmp, []byte("inside docx bytes"), 0o644) + } else { + _ = os.Symlink(outside, tmp) + } + _ = os.Rename(tmp, tgt) + } + }() + defer stop.Store(true) + iters := 0 + for deadline := time.Now().Add(1500 * time.Millisecond); time.Now().Before(deadline); iters++ { + _, _ = api.ReadIngestFile(brain, tgt) + } + if n := seen.Load(); n > 0 { + t.Fatalf("docmark received the outside file %d times in %d iterations", n, iters) + } + t.Logf("%d iterations, 0 leaks to docmark", iters) +}