diff --git a/scripts/ci/build-guest b/scripts/ci/build-guest index 8d6a463f1..1c7ea64c3 100644 --- a/scripts/ci/build-guest +++ b/scripts/ci/build-guest @@ -39,6 +39,7 @@ ginkgo) # Drivers consume the core's Module.symvers. Keep these builds ordered. make "${opts[@]}" scst make "${opts[@]}" scst_local + make "${opts[@]}" usr make "${opts[@]}" iscsi INFINIBAND_ENABLED=false make "${opts[@]}" scstadm make ginkgo-check diff --git a/tests/ginkgo/README.md b/tests/ginkgo/README.md index 94fec9af5..a6c9a2b16 100644 --- a/tests/ginkgo/README.md +++ b/tests/ginkgo/README.md @@ -131,6 +131,12 @@ Fio verifies CRC32C data using three concurrent [profile](internal/dataio/verify.fio). It checks completed I/O, not performance. Readback checks do not verify data persistence across a crash or power loss. +The `user` specs run `usr/fileio/fileio_tgt` over `scst_local` with a fault +hook preloaded that makes the handler return an invalid READ CAPACITY block +length. READ CAPACITY, reads and writes must stay correct afterwards. They +need a C compiler on the target to build the hook; `ginkgo-test-build` also +builds `usr`, and the local suite loads `scst_user`. + The `unmap` specs exercise temporary thin-provisioned fileio exports over `scst_local` and TCP iSCSI. They require util-linux `fallocate` on the target and a backing filesystem that supports hole punching. diff --git a/tests/ginkgo/internal/dataio/user.go b/tests/ginkgo/internal/dataio/user.go new file mode 100644 index 000000000..3cef71e8c --- /dev/null +++ b/tests/ginkgo/internal/dataio/user.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-only +package dataio + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "regexp" +) + +type modelBlock struct { + Path string `json:"path"` + Type string `json:"type"` + Size uint64 `json:"size"` + Logical uint32 `json:"log-sec"` + Model *string `json:"model"` + HCTL *string `json:"hctl"` +} + +func modelBlocks(output string) ([]modelBlock, error) { + var list struct { + Devices []modelBlock `json:"blockdevices"` + } + if err := json.Unmarshal([]byte(output), &list); err != nil { + return nil, err + } + if list.Devices == nil { + return nil, fmt.Errorf("missing lsblk device list") + } + return list.Devices, nil +} + +// FindModel addresses the LUN 0 disk whose INQUIRY product identification is +// model. The user-space fileio handler reports its device name there. +func (e *Endpoint) FindModel(ctx context.Context, model string) error { + r, err := e.Run(ctx, e.Timeout, "lsblk", "--json", "--bytes", "--nodeps", + "--output", "PATH,TYPE,SIZE,LOG-SEC,MODEL,HCTL") + if err != nil { + return err + } + if r.ExitCode != 0 { + return fmt.Errorf("lsblk: %s", r.String()) + } + devices, err := modelBlocks(r.Stdout) + if err != nil { + return err + } + var matches []modelBlock + for _, d := range devices { + if d.Model != nil && *d.Model == model && d.HCTL != nil && + regexp.MustCompile(`^\d+:\d+:\d+:0$`).MatchString(*d.HCTL) { + matches = append(matches, d) + } + } + if len(matches) != 1 { + return fmt.Errorf("expected one exported disk with model %s, found %d", model, len(matches)) + } + d := matches[0] + if d.Type != "disk" || !regexp.MustCompile(`^/dev/[a-zA-Z0-9/_-]+$`).MatchString(d.Path) || + d.Size != e.Backend.Bytes || d.Logical != e.Backend.BlockSize { + return fmt.Errorf("exported device geometry mismatch: %+v", d) + } + e.Path = d.Path + // Also consumes the unit attention reported for a new LUN. + _, err = e.sg(ctx, "sg_turs") + return err +} + +func (e *Endpoint) readBlock(ctx context.Context, lba uint64, want []byte) error { + data, err := success(e.raw(ctx, false, lba, nil)) + if err != nil { + return err + } + if !bytes.Equal(data, want) { + return fmt.Errorf("READ of LBA %d returned different data", lba) + } + return nil +} + +// READ CAPACITY CDBs whose replies a handler fault can corrupt. +var ( + ReadCapacity10 = []byte{0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0} + ReadCapacity16 = []byte{0x9e, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0} +) + +// UserHandlerFault checks a disk served by the user-space fileio handler +// across a handler fault. Reading LBA 0, which holds lba0, leaves known bytes +// in the handler's reused buffer. arm enables the fault in the handler, which +// replaces the block length of the reply to capacityCDB. Afterwards geometry, +// reads and writes must be unaffected. +func (e *Endpoint) UserHandlerFault(ctx context.Context, lba0 []byte, arm func() error, capacityCDB []byte) error { + if err := e.Capacity(ctx); err != nil { + return fmt.Errorf("before the fault: %w", err) + } + if err := e.readBlock(ctx, 0, lba0); err != nil { + return err + } + if err := arm(); err != nil { + return err + } + request := 8 + if capacityCDB[0] == 0x9e { + request = 32 + } + // scst_sense_internal_failure is 0b/44/00. + r, err := e.rawCDB(ctx, capacityCDB, nil, request) + if err := rejection(r, err, 11, [3]byte{0xb, 0x44, 0}); err != nil { + return fmt.Errorf("invalid block length: %w", err) + } + if err := e.Capacity(ctx); err != nil { + return fmt.Errorf("after the fault: %w", err) + } + if err := e.readBlock(ctx, 0, lba0); err != nil { + return err + } + payload := bytes.Repeat([]byte("SCSTUSER"), int(e.Backend.BlockSize)/8) + if _, err := success(e.raw(ctx, true, 1, payload)); err != nil { + return err + } + return e.readBlock(ctx, 1, payload) +} diff --git a/tests/ginkgo/internal/dataio/user_test.go b/tests/ginkgo/internal/dataio/user_test.go new file mode 100644 index 000000000..fb61e881b --- /dev/null +++ b/tests/ginkgo/internal/dataio/user_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-only +package dataio + +import ( + "context" + "encoding/json" + "testing" + "time" + + "scst-component-tests/internal/admin" +) + +func TestFindModelRejectsAmbiguousOrWrongDiskBeforeSG(t *testing.T) { + model, other, lun0, lun7 := "u0123456789abcde", "other", "1:0:0:0", "1:0:0:7" + good := modelBlock{Path: "/dev/sdz", Type: "disk", Size: 16 << 20, Logical: 4096, Model: &model, HCTL: &lun0} + for name, disks := range map[string][]modelBlock{ + "absent": {}, "duplicate": {good, good}, + "no model": {func() modelBlock { b := good; b.Model = nil; return b }()}, + "other model": {func() modelBlock { b := good; b.Model = &other; return b }()}, + "wrong LUN": {func() modelBlock { b := good; b.HCTL = &lun7; return b }()}, + "no HCTL": {func() modelBlock { b := good; b.HCTL = nil; return b }()}, + "wrong size": {func() modelBlock { b := good; b.Size--; return b }()}, + "wrong sector": {func() modelBlock { b := good; b.Logical = 512; return b }()}, + "not a disk": {func() modelBlock { b := good; b.Type = "rom"; return b }()}, + } { + t.Run(name, func(t *testing.T) { + run := func(_ context.Context, _ time.Duration, argv ...string) (admin.Result, error) { + if argv[0] != "lsblk" { + t.Fatalf("wrong device reached SCSI utility: %v", argv) + } + data, err := json.Marshal(map[string]any{"blockdevices": disks}) + return admin.Result{Stdout: string(data)}, err + } + e := Endpoint{Run: run, Backend: Backend{Bytes: 16 << 20, BlockSize: 4096}} + if e.FindModel(context.Background(), model) == nil { + t.Fatal("accepted wrong disk") + } + }) + } +} + +func TestFindModelSelectsOwnedDisk(t *testing.T) { + model, lun0 := "u0123456789abcde", "1:0:0:0" + var turs []string + run := func(_ context.Context, _ time.Duration, argv ...string) (admin.Result, error) { + if argv[0] == "lsblk" { + data, err := json.Marshal(map[string]any{"blockdevices": []modelBlock{ + {Path: "/dev/sdy", Type: "disk", Size: 1 << 30, Logical: 512}, + {Path: "/dev/sdz", Type: "disk", Size: 16 << 20, Logical: 4096, Model: &model, HCTL: &lun0}, + }}) + return admin.Result{Stdout: string(data)}, err + } + if argv[0] != "python3" { + t.Fatalf("unexpected command: %v", argv) + } + turs = append(turs, argv[len(argv)-1]) + return admin.Result{Stdout: `{"exit_code":0,"data":"","stderr":""}`}, nil + } + e := Endpoint{Run: run, Backend: Backend{Bytes: 16 << 20, BlockSize: 4096}} + if err := e.FindModel(context.Background(), model); err != nil { + t.Fatal(err) + } + if e.Path != "/dev/sdz" || len(turs) != 1 { + t.Fatalf("selected %q after %d commands", e.Path, len(turs)) + } +} diff --git a/tests/ginkgo/internal/testhost/prepare.go b/tests/ginkgo/internal/testhost/prepare.go index 32998a2fe..f4a0b838d 100644 --- a/tests/ginkgo/internal/testhost/prepare.go +++ b/tests/ginkgo/internal/testhost/prepare.go @@ -72,11 +72,12 @@ func (m *Modules) Prepare(ctx context.Context, opts Options, run RunProgram, che return fmt.Errorf("KDIR must be absolute") } // Target drivers consume the core's Module.symvers, so build in order. - driver := "scst_local" + // The local suite also runs the user-space fileio handler. + targets := []string{"scst", "scst_local", "usr", "scstadm"} if opts.ISCSI { - driver = "iscsi" + targets = []string{"scst", "iscsi", "scstadm"} } - for _, target := range []string{"scst", driver, "scstadm"} { + for _, target := range targets { fmt.Fprintf(progress, "Building %s for %s\n", target, release) if _, err := command(opts.BuildTimeout, "make", "-C", opts.SourceDir, fmt.Sprintf("-j%d", opts.Jobs), "KVER="+release, "KDIR="+kdir, target); err != nil { @@ -87,10 +88,11 @@ func (m *Modules) Prepare(ctx context.Context, opts Options, run RunProgram, che modules := []struct{ name, path string }{ {"scst", "scst/src/scst.ko"}, {"scst_vdisk", "scst/src/dev_handlers/scst_vdisk.ko"}, + {"scst_user", "scst/src/dev_handlers/scst_user.ko"}, {"scst_local", "scst_local/scst_local.ko"}, } if opts.ISCSI { - modules[2].name, modules[2].path = "iscsi_scst", "iscsi-scst/kernel/iscsi-scst.ko" + modules = append(modules[:2], struct{ name, path string }{"iscsi_scst", "iscsi-scst/kernel/iscsi-scst.ko"}) } dependencies := make([][]string, len(modules)) for i, m := range modules { diff --git a/tests/ginkgo/internal/testhost/prepare_test.go b/tests/ginkgo/internal/testhost/prepare_test.go index a02fc21ec..cbf3e05a6 100644 --- a/tests/ginkgo/internal/testhost/prepare_test.go +++ b/tests/ginkgo/internal/testhost/prepare_test.go @@ -38,7 +38,7 @@ func (h *host) run(_ context.Context, limit time.Duration, args ...string) (admi if h.modules == nil { h.modules = map[string]bool{"unrelated": true} if h.loaded { - for _, name := range []string{"scst", "scst_vdisk", "scst_local"} { + for _, name := range []string{"scst", "scst_vdisk", "scst_user", "scst_local"} { h.modules[name] = true } } @@ -123,7 +123,7 @@ func TestBuildAndLoadFromCheckout(t *testing.T) { opts.Build = build wantBuildCount := 0 if build { - wantBuildCount = 3 + wantBuildCount = 4 } else { // Loading existing modules must not need any build settings. opts.Jobs, opts.BuildTimeout = 0, 0 @@ -149,7 +149,7 @@ func TestBuildAndLoadFromCheckout(t *testing.T) { case "modinfo": metadata++ case "mkdir", "rmmod", "modprobe", "insmod": - if metadata != 6 || len(builds) != wantBuildCount { + if metadata != 8 || len(builds) != wantBuildCount { t.Fatal("changed modules before all builds and metadata checks completed") } changes = append(changes, args) @@ -160,7 +160,7 @@ func TestBuildAndLoadFromCheckout(t *testing.T) { } var wantBuilds [][]string if build { - for _, target := range []string{"scst", "scst_local", "scstadm"} { + for _, target := range []string{"scst", "scst_local", "usr", "scstadm"} { wantBuilds = append(wantBuilds, []string{"make", "-C", opts.SourceDir, "-j2", "KVER=6.12-test", "KDIR=/lib/modules/6.12-test/build", target}) } } @@ -169,12 +169,14 @@ func TestBuildAndLoadFromCheckout(t *testing.T) { } wantChanges := [][]string{{"mkdir", "-p", "--", "/var/lib/scst/pr", "/var/lib/scst/dif_tags", "/var/lib/scst/vdev_mode_pages"}} if loaded { - wantChanges = append(wantChanges, []string{"rmmod", "scst_local"}, []string{"rmmod", "scst_vdisk"}, []string{"rmmod", "scst"}) + wantChanges = append(wantChanges, []string{"rmmod", "scst_local"}, []string{"rmmod", "scst_user"}, + []string{"rmmod", "scst_vdisk"}, []string{"rmmod", "scst"}) } wantChanges = append(wantChanges, []string{"modprobe", "--", "crc32c_generic"}, []string{"insmod", opts.SourceDir + "/scst/src/scst.ko"}, []string{"insmod", opts.SourceDir + "/scst/src/dev_handlers/scst_vdisk.ko"}, + []string{"insmod", opts.SourceDir + "/scst/src/dev_handlers/scst_user.ko"}, []string{"insmod", opts.SourceDir + "/scst_local/scst_local.ko", "add_default_tgt=0"}) if !reflect.DeepEqual(changes, wantChanges) { t.Fatalf("module order/arguments: %q", changes) @@ -278,11 +280,12 @@ func TestUnloadOwnedModules(t *testing.T) { unload []string remaining []string }{ - {name: "complete", unload: []string{"scst_local", "scst_vdisk", "scst"}}, - {name: "replaced", loaded: true, unload: []string{"scst_local", "scst_vdisk", "scst"}}, + {name: "complete", unload: []string{"scst_local", "scst_user", "scst_vdisk", "scst"}}, + {name: "replaced", loaded: true, unload: []string{"scst_local", "scst_user", "scst_vdisk", "scst"}}, {name: "core only", fail: "insmod /checkout with spaces/scst/src/dev_handlers/scst_vdisk.ko", unload: []string{"scst"}}, - {name: "no local driver", fail: "insmod /checkout with spaces/scst_local/scst_local.ko", unload: []string{"scst_vdisk", "scst"}}, - {name: "build failure", fail: "make", loaded: true, remaining: []string{"scst", "scst_vdisk", "scst_local"}}, + {name: "no user handler", fail: "insmod /checkout with spaces/scst/src/dev_handlers/scst_user.ko", unload: []string{"scst_vdisk", "scst"}}, + {name: "no local driver", fail: "insmod /checkout with spaces/scst_local/scst_local.ko", unload: []string{"scst_user", "scst_vdisk", "scst"}}, + {name: "build failure", fail: "make", loaded: true, remaining: []string{"scst", "scst_vdisk", "scst_user", "scst_local"}}, {name: "replacement failure", fail: "rmmod scst_vdisk", loaded: true, remaining: []string{"scst", "scst_vdisk"}}, {name: "core load failure", fail: "insmod /checkout with spaces/scst/src/scst.ko"}, } { @@ -324,7 +327,7 @@ func TestUnloadOwnedModules(t *testing.T) { t.Fatalf("wrong unload command timeout: %s", limit) } } - for _, name := range []string{"scst", "scst_vdisk", "scst_local"} { + for _, name := range []string{"scst", "scst_vdisk", "scst_user", "scst_local"} { want := false for _, remaining := range tc.remaining { want = want || name == remaining @@ -354,6 +357,7 @@ func TestUnloadStopsOnFailure(t *testing.T) { }{ {name: "configured host", dirty: true}, {name: "busy local driver", fail: "rmmod scst_local", last: "rmmod scst_local"}, + {name: "busy user handler", fail: "rmmod scst_user", last: "rmmod scst_user"}, {name: "busy handler", fail: "rmmod scst_vdisk", last: "rmmod scst_vdisk"}, {name: "busy core", fail: "rmmod scst", last: "rmmod scst"}, {name: "interrupted command", fail: "rmmod scst_local", execFail: true, last: "rmmod scst_local"}, diff --git a/tests/ginkgo/suite/suite_test.go b/tests/ginkgo/suite/suite_test.go index 5c8d7b181..bfd28b920 100644 --- a/tests/ginkgo/suite/suite_test.go +++ b/tests/ginkgo/suite/suite_test.go @@ -18,7 +18,7 @@ import ( var cfg testcfg.Config var cli *admin.Client -var artifactDir, runID string +var artifactDir, runID, sourceDir string var pendingCleanups atomic.Int64 func TestSCST(t *testing.T) { @@ -29,7 +29,7 @@ func TestSCST(t *testing.T) { var _ = BeforeSuite(func(ctx SpecContext) { modules := new(testhost.Modules) - sourceDir := os.Getenv("SCST_TEST_SOURCE_DIR") + sourceDir = os.Getenv("SCST_TEST_SOURCE_DIR") if sourceDir == "" { // Direct go test invocation runs in tests/ginkgo/suite. sourceDir = must(filepath.Abs("../../..")) diff --git a/tests/ginkgo/suite/testdata/user_hook.c b/tests/ginkgo/suite/testdata/user_hook.c new file mode 100644 index 000000000..81fb0bd4b --- /dev/null +++ b/tests/ginkgo/suite/testdata/user_hook.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LD_PRELOAD fault hook for usr/fileio/fileio_tgt. Once the file named by + * SCST_TEST_HOOK_TRIGGER exists, SCST_TEST_HOOK_ACTION selects one fault: + * + * capacity: replace the block length of the next READ CAPACITY reply. + * + * Each fault is applied once and logged to stderr. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scst_user.h" + +static int (*next_ioctl)(int fd, unsigned long request, ...); +static int done; +static unsigned int exec_handle; +static unsigned long long exec_pbuf; +static unsigned char exec_cdb[2]; + +static int armed(const char *action) +{ + const char *want = getenv("SCST_TEST_HOOK_ACTION"); + const char *trigger = getenv("SCST_TEST_HOOK_TRIGGER"); + + return !done && want && trigger && strcmp(want, action) == 0 && + access(trigger, F_OK) == 0; +} + +static void corrupt_capacity(struct scst_user_get_cmd *cmd) +{ + struct scst_user_reply_cmd *reply; + unsigned int offset; + unsigned char *data; + + if (!exec_handle || !cmd->preply || !armed("capacity")) + return; + reply = (void *)(uintptr_t)cmd->preply; + if (reply->cmd_h != exec_handle || reply->subcode != SCST_USER_EXEC || + reply->exec_reply.status != 0) + return; + if (exec_cdb[0] == 0x25) + offset = 4; /* READ CAPACITY(10) */ + else if (exec_cdb[0] == 0x9e && (exec_cdb[1] & 0x1f) == 0x10) + offset = 8; /* READ CAPACITY(16) */ + else + return; + if (reply->exec_reply.resp_data_len < offset + 4) + return; + /* The reply names a buffer only if the handler allocated it. */ + data = (void *)(uintptr_t)(reply->exec_reply.pbuf ?: exec_pbuf); + if (!data) + return; + done = 1; + /* 809052976: not a power of two. */ + memcpy(&data[offset], "\x30\x39\x2b\x30", 4); + fprintf(stderr, "SCST_TEST_HOOK capacity cdb=0x%02x\n", exec_cdb[0]); +} + +int ioctl(int fd, unsigned long request, ...) +{ + struct scst_user_get_cmd *cmd; + va_list args; + void *arg; + int ret; + + if (!next_ioctl) + next_ioctl = dlsym(RTLD_NEXT, "ioctl"); + if (!next_ioctl) + abort(); + va_start(args, request); + arg = va_arg(args, void *); + va_end(args); + if (request != SCST_USER_REPLY_AND_GET_CMD || !arg) + return next_ioctl(fd, request, arg); + + cmd = arg; + corrupt_capacity(cmd); + ret = next_ioctl(fd, request, arg); + exec_handle = 0; + if (ret == 0 && cmd->subcode == SCST_USER_EXEC) { + exec_handle = cmd->cmd_h; + exec_pbuf = cmd->exec_cmd.pbuf; + memcpy(exec_cdb, cmd->exec_cmd.cdb, sizeof(exec_cdb)); + } + return ret; +} diff --git a/tests/ginkgo/suite/user_test.go b/tests/ginkgo/suite/user_test.go new file mode 100644 index 000000000..4dbb14d51 --- /dev/null +++ b/tests/ginkgo/suite/user_test.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-only +package suite + +import ( + "bytes" + _ "embed" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "scst-component-tests/internal/dataio" + "scst-component-tests/internal/testcfg" + "scst-component-tests/internal/testhost" +) + +//go:embed testdata/user_hook.c +var userHookSource []byte + +const userBlockSize = 4096 + +// userDevice starts usr/fileio/fileio_tgt with the fault hook preloaded. One +// worker thread without multi-command replies makes every command reuse the +// same cached buffer. Cleanup removes the LUN before stopping the handler, +// which unregisters the device. +func (f *fixture) userDevice(ctx SpecContext, action, path, trigger string) (string, *os.File) { + GinkgoHelper() + timeout := testcfg.Duration(cfg.CommandTimeout) + source, hook := f.file("user_hook.c"), f.file("user_hook.so") + Expect(os.WriteFile(source, userHookSource, 0o644)).To(Succeed()) + r, err := cli.RunProgram(ctx, timeout, "cc", "-shared", "-fPIC", "-O2", "-Wall", "-Werror", + "-I"+filepath.Join(sourceDir, "scst/include"), "-o", hook, source, "-ldl") + Expect(err).NotTo(HaveOccurred(), r.String()) + Expect(r.ExitCode).To(BeZero(), r.String()) + + // The handler reports its device name as the INQUIRY product (16 bytes). + name := "u" + dataio.DeviceID(f.prefix)[:15] + log := must(os.Create(f.file("fileio_tgt.log"))) + p, err := testhost.StartProcess(log, "env", "LD_PRELOAD="+hook, + "SCST_TEST_HOOK_ACTION="+action, "SCST_TEST_HOOK_TRIGGER="+trigger, + filepath.Join(sourceDir, "usr/fileio/fileio_tgt"), fmt.Sprintf("--block=%d", userBlockSize), + "--threads=1", "--multi_cmd=0", name, path) + if err != nil { + Expect(log.Close()).To(Succeed()) + Expect(err).NotTo(HaveOccurred()) + } + deferSpecCleanup(func(ctx SpecContext) { + if err := f.stopUserDevice(ctx, p, name); err != nil { + AbortSuite("user-space handler cleanup failed; inspect this disposable host before another run: " + err.Error()) + } + Expect(log.Close()).To(Succeed()) + }) + Eventually(func() error { + if err := p.Running(); err != nil { + return StopTrying(err.Error()) + } + if _, ok := must(cli.Devices(ctx))[name]; !ok { + return fmt.Errorf("device %s is not registered yet", name) + } + return nil + }, testcfg.Duration(cfg.SettleTimeout), testcfg.Duration(cfg.PollInterval)).WithContext(ctx).Should(Succeed()) + return name, log +} + +func (f *fixture) stopUserDevice(ctx SpecContext, p *testhost.Process, name string) error { + targets, err := cli.Targets(ctx) + if err != nil { + return err + } + for _, t := range f.targets { + if _, ok := targets[t]; ok { + r, err := cli.Execute(ctx, "-rem_target", t, "-driver", "scst_local") + if err != nil || r.ExitCode != 0 || r.ReportedError() { + return fmt.Errorf("%s", r.String()) + } + } + } + // The device is unregistered once every process holding the scst_user + // file descriptor has exited; SIGTERM reaches the whole process group. + if err := p.Stop(ctx); err != nil { + return err + } + deadline := time.Now().Add(testcfg.Duration(cfg.SettleTimeout)) + for { + devices, err := cli.Devices(ctx) + if err != nil { + return err + } + if _, ok := devices[name]; !ok { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("device %s remains after its handler exited", name) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(testcfg.Duration(cfg.PollInterval)): + } + } +} + +var _ = Describe("User-space handler devices", Label("scsi", "user", "smoke"), func() { + DescribeTable("keep replies, geometry and data intact across a handler fault", + func(ctx SpecContext, action string, capacityCDB []byte) { + timeout := testcfg.Duration(cfg.CommandTimeout) + Expect(dataio.RequireTools(ctx, cli.RunProgram, timeout, false)).To(Succeed()) + f := newFixture() + path := f.backingFile("user") + lba0 := bytes.Repeat([]byte("SCSTDATA"), userBlockSize/8) + file := must(os.OpenFile(path, os.O_WRONLY, 0)) + _, err := file.WriteAt(lba0, 0) + Expect(file.Close()).To(Succeed()) + Expect(err).NotTo(HaveOccurred()) + + trigger := f.file("user_trigger") + d, log := f.userDevice(ctx, action, path, trigger) + session := f.name("session") + target := f.target(ctx, "target", "session_name="+session) + f.at(ctx, target, "", "-add_group", "access") + f.at(ctx, target, "access", "-add_lun", "0", "-device", d) + f.at(ctx, target, "access", "-add_init", session) + + e := &dataio.Endpoint{Run: cli.RunProgram, Timeout: timeout, IOTimeout: testcfg.Duration(cfg.IO.Timeout), + Backend: dataio.Backend{Bytes: 16 * 1024 * 1024, BlockSize: userBlockSize}, Artifacts: artifactDir, Name: d} + Eventually(func() error { return e.FindModel(ctx, d) }, testcfg.Duration(cfg.SettleTimeout), + testcfg.Duration(cfg.PollInterval)).WithContext(ctx).Should(Succeed()) + arm := func() error { + return os.WriteFile(trigger, nil, 0o644) + } + Expect(e.UserHandlerFault(ctx, lba0, arm, capacityCDB)).To(Succeed()) + Expect(log.Sync()).To(Succeed()) + Expect(strings.Contains(string(must(os.ReadFile(log.Name()))), "SCST_TEST_HOOK "+action)).To(BeTrue(), + "the handler did not apply the %s fault", action) + }, + Entry("handler returns an invalid READ CAPACITY(10) block length", Label("negative"), + "capacity", dataio.ReadCapacity10), + Entry("handler returns an invalid READ CAPACITY(16) block length", Label("negative"), + "capacity", dataio.ReadCapacity16)) +})