mirror of
https://github.com/SCST-project/scst.git
synced 2026-10-22 06:02:33 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
Reference in New Issue
Block a user