126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package main
|
|
|
|
// The digest cache exists for one reason — a pass that hashes every executable
|
|
// on the box costs 2.9 s on this machine and the delta runs every minute — and
|
|
// it is allowed to exist only if it cannot be talked into a stale answer. These
|
|
// tests are that condition, not the speed.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// THE ATTACK THE KEY IS SHAPED AROUND. An in-place overwrite that restores the
|
|
// old size and the old modification time defeats every field a cache normally
|
|
// keys on. It does not defeat ctime, which the kernel moves on any write and no
|
|
// syscall sets — and that is the entire argument for caching a digest in a
|
|
// program whose job is to notice when a binary changes.
|
|
func TestABinaryRewrittenWithItsOldTimestampIsNotServedFromTheCache(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows has no change time through Go's FileInfo; deltaCacheWeakTTL " +
|
|
"covers it there, and deltacache_windows.go says so")
|
|
}
|
|
p := filepath.Join(t.TempDir(), "victim")
|
|
if err := os.WriteFile(p, []byte("AAAAAAAA"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fi, err := os.Stat(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := &digestCache{}
|
|
before, _ := c.digest(p)
|
|
if before == "" {
|
|
t.Fatal("the fixture was not hashed at all")
|
|
}
|
|
// Same length, same modification time, different content.
|
|
if err := os.WriteFile(p, []byte("BBBBBBBB"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(p, fi.ModTime(), fi.ModTime()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, _ := c.digest(p)
|
|
if after == before {
|
|
t.Fatalf("the cache returned the OLD digest for a file whose contents had been "+
|
|
"replaced under its original size and timestamp (%s). A digest cache that "+
|
|
"can be defeated by `touch -r` hides exactly the event this plane exists "+
|
|
"to report.", before[:12])
|
|
}
|
|
}
|
|
|
|
// And it does cache — otherwise the property above is held by a cache that
|
|
// never hits, which is a different program.
|
|
func TestTheCacheHitsWhenNothingMoved(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "steady")
|
|
if err := os.WriteFile(p, []byte("hello"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := &digestCache{}
|
|
a, _ := c.digest(p)
|
|
b, _ := c.digest(p)
|
|
if a != b || a == "" {
|
|
t.Fatalf("two reads of one unchanged file gave %q and %q", a, b)
|
|
}
|
|
hits, misses, _ := c.stats()
|
|
if hits != 1 || misses != 1 {
|
|
t.Errorf("hits=%d misses=%d, want 1 and 1 — the second read went the long way "+
|
|
"round, so the 60-second cadence costs a full hash of the box every time", hits, misses)
|
|
}
|
|
}
|
|
|
|
// A FAILURE IS A FACT ABOUT THE MOMENT, NOT ABOUT THE FILE. Caching "could not
|
|
// be read" would mean a process whose executable becomes readable — the bastion
|
|
// gaining rights, a mount appearing — is told for ever that it could not be
|
|
// looked at.
|
|
func TestAFileThatBecomesReadableIsDigestedRatherThanRemembered(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "later")
|
|
c := &digestCache{}
|
|
if d, note := c.digest(p); d != "" || note == "" {
|
|
t.Fatalf("a path with no file gave digest %q note %q", d, note)
|
|
}
|
|
if err := os.WriteFile(p, []byte("now it exists"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d, _ := c.digest(p); d == "" {
|
|
t.Fatal("the file exists now and the cache still says it cannot be read")
|
|
}
|
|
}
|
|
|
|
// The cache answers the same thing the uncached path does. It is a speed
|
|
// device; the moment it is also a second opinion, the accounting and the delta
|
|
// can disagree about what is running.
|
|
func TestTheCachedAnswerIsTheUncachedAnswer(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "same")
|
|
if err := os.WriteFile(p, []byte("content"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want, wantNote := baselineExeDigest(p)
|
|
c := &digestCache{}
|
|
got, gotNote := c.digest(p)
|
|
if got != want || gotNote != wantNote {
|
|
t.Fatalf("cached (%q,%q) != direct (%q,%q)", got, gotNote, want, wantNote)
|
|
}
|
|
// Including the refusals: a directory is not an executable either way.
|
|
d1, n1 := baselineExeDigest(filepath.Dir(p))
|
|
d2, n2 := c.digest(filepath.Dir(p))
|
|
if d1 != d2 || n1 != n2 {
|
|
t.Fatalf("cached (%q,%q) != direct (%q,%q) for a directory", d2, n2, d1, n1)
|
|
}
|
|
}
|
|
|
|
// A weakly-keyed entry is re-hashed on a timer. Held here as a fact about the
|
|
// constant rather than by waiting fifteen minutes: if someone lengthens it to
|
|
// hours, the Windows hole widens with no test saying so.
|
|
func TestTheWeakKeyTTLStaysShortEnoughToMatter(t *testing.T) {
|
|
if deltaCacheWeakTTL > 30*time.Minute {
|
|
t.Fatalf("deltaCacheWeakTTL is %v — on a platform with no change time this is "+
|
|
"how long a size-and-timestamp-preserving overwrite reads as known",
|
|
deltaCacheWeakTTL)
|
|
}
|
|
}
|