tests: Exercise invalid READ CAPACITY replies from an scst_user handler

Run usr/fileio/fileio_tgt over scst_local with an LD_PRELOAD hook that
replaces the block length of one READ CAPACITY(10) or (16) reply with
809052976, as a user space backend did in production. The invalid reply
must be rejected with INTERNAL TARGET FAILURE sense, and afterwards READ
CAPACITY(10) and (16), reads and writes must return the right geometry
and data.

The local suite now builds usr and loads scst_user, and the CI image
builds usr for the Ginkgo guest.
This commit is contained in:
wenlxie
2026-10-18 13:12:41 +03:00
committed by Gleb Chesnokov
parent 700c7ab4ab
commit a6167625b0
9 changed files with 455 additions and 16 deletions
+1
View File
@@ -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
+6
View File
@@ -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.
+122
View File
@@ -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)
}
+66
View File
@@ -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))
}
}
+6 -4
View File
@@ -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 {
+14 -10
View File
@@ -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"},
+2 -2
View File
@@ -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("../../.."))
+94
View File
@@ -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 <dlfcn.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#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;
}
+144
View File
@@ -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))
})