package accounts import ( "context" "errors" "fmt" "slices" "testing" "time" ) // The walk for setuid programs yields to a busy disk by the machine's I/O pressure (novox/hq issue 433), because // the idle I/O class it is put in is ignored by a disk scheduler that does not schedule by class, as the // control-node's mq-deadline does not. // fakePace is a pacer whose pressure readings, sleeps and clock a test controls. type fakePace struct { readings []float64 // said in turn; the last is said again once they run out read int slept []time.Duration clock time.Time said []string fail error } func (f *fakePace) pacer() *pacer { return &pacer{ pressure: func() (float64, error) { if f.fail != nil { return 0, f.fail } i := min(f.read, len(f.readings)-1) f.read++ return f.readings[i], nil }, sleep: func(ctx context.Context, d time.Duration) error { if err := ctx.Err(); err != nil { return err } f.slept = append(f.slept, d) f.clock = f.clock.Add(d) return nil }, now: func() time.Time { return f.clock }, logf: func(format string, args ...any) { f.said = append(f.said, fmt.Sprintf(format, args...)) }, } } func TestTheWalkPausesWhileTheDiskIsUnderPressure(t *testing.T) { f := &fakePace{readings: []float64{42, 18, PaceAbove, 3}, clock: time.Unix(1_000_000, 0)} pc := f.pacer() if err := pc.yield(t.Context()); err != nil { t.Fatal(err) } // 42 and 18 are above the bound; PaceAbove itself is not. if want := []time.Duration{PacePause, PacePause}; !slices.Equal(f.slept, want) { t.Fatalf("slept %v, want %v", f.slept, want) } if f.read != 3 { t.Fatalf("read the pressure %d times, want 3", f.read) } } func TestTheWalkDoesNotPauseOnAQuietDisk(t *testing.T) { f := &fakePace{readings: []float64{0.4}, clock: time.Unix(1_000_000, 0)} if err := f.pacer().yield(t.Context()); err != nil { t.Fatal(err) } if len(f.slept) != 0 { t.Fatalf("slept %v on a quiet disk", f.slept) } } // A machine that is never quiet still sees the walk end: a pause in a row is bounded, then one more directory. func TestTheWalkPausesAtMostPaceAtMostInARow(t *testing.T) { f := &fakePace{readings: []float64{95}, clock: time.Unix(1_000_000, 0)} pc := f.pacer() if err := pc.yield(t.Context()); err != nil { t.Fatal(err) } var total time.Duration for _, d := range f.slept { total += d } if total != PaceAtMost { t.Fatalf("paused %v in a row, want %v", total, PaceAtMost) } } func TestTheWalkLooksAtThePressureAtMostEveryPaceLook(t *testing.T) { f := &fakePace{readings: []float64{1}, clock: time.Unix(1_000_000, 0)} pc := f.pacer() for range 5 { if err := pc.yield(t.Context()); err != nil { t.Fatal(err) } f.clock = f.clock.Add(PaceLook / 10) } if f.read != 1 { t.Fatalf("read the pressure %d times within %v, want once", f.read, PaceLook) } f.clock = f.clock.Add(PaceLook) if err := pc.yield(t.Context()); err != nil { t.Fatal(err) } if f.read != 2 { t.Fatalf("read the pressure %d times after %v, want twice", f.read, PaceLook) } } // Pressure that cannot be read pauses nothing, and is said once rather than at every directory. func TestAPressureThatCannotBeReadIsSaidOnce(t *testing.T) { f := &fakePace{fail: errors.New("no such file"), clock: time.Unix(1_000_000, 0)} pc := f.pacer() for range 3 { if err := pc.yield(t.Context()); err != nil { t.Fatal(err) } f.clock = f.clock.Add(PaceLook) } if len(f.slept) != 0 || len(f.said) != 1 { t.Fatalf("slept %v, said %q; want no pause and one line", f.slept, f.said) } } // A walk ended while it pauses is an incomplete walk, never one that found nothing. func TestAWalkEndedWhileItPausesIsIncomplete(t *testing.T) { top := tree(t, "bin/", "bin/su!") f := &fakePace{readings: []float64{80}, clock: time.Unix(1_000_000, 0)} ctx, cancel := context.WithCancel(t.Context()) defer cancel() pc := f.pacer() // The engine stops while the walk waits for the disk: the context ends inside the pause, not before the walk. pc.sleep = func(ctx context.Context, d time.Duration) error { f.slept = append(f.slept, d) cancel() return ctx.Err() } w := mine() w.pace = pc p := &SetuidProgress{Mounts: []string{top}} err := w.walk(ctx, p, func() {}) if len(f.slept) == 0 { t.Fatal("the walk never paused, so this proves nothing about a walk ended while it pauses") } if !errors.Is(err, context.Canceled) { t.Fatalf("a walk ended while it paused answered %v", err) } } // Paced, the walk finds what it finds unpaced: pausing changes when, never what. func TestAPacedWalkFindsWhatAnUnpacedWalkFinds(t *testing.T) { top := tree(t, "bin/", "bin/su!", "bin/ls", "sbin/", "sbin/mount!", "usr/", "usr/lib/", "usr/lib/helper%") plain := walkAll(t, mine(), top) f := &fakePace{readings: []float64{50, 2}, clock: time.Unix(1_000_000, 0)} w := mine() w.pace = f.pacer() paced := walkAll(t, w, top) if !slices.Equal(plain, paced) { t.Fatalf("paced walk found %v, unpaced %v", paced, plain) } if len(f.slept) == 0 { t.Fatal("the walk read its directories without asking the pacer: it never paused under pressure") } } func TestTheIOPressureIsReadAsTheKernelWritesIt(t *testing.T) { got, err := parseIOPressure([]byte("some avg10=26.24 avg60=20.07 avg300=27.47 total=123456789\n" + "full avg10=21.90 avg60=15.77 avg300=22.47 total=98765432\n")) if err != nil || got != 26.24 { t.Fatalf("read %v, %v; want 26.24", got, err) } for _, bad := range []string{"", "full avg10=1.00 avg60=0 avg300=0 total=0\n", "some avg10=x avg60=0\n"} { if _, err := parseIOPressure([]byte(bad)); err == nil { t.Errorf("%q was read as a pressure", bad) } } }