platformtest: replace logmockzfs with multi-personality safety interposer

Replace the shell-script-based logmockzfs wrapper with a Go-native
multi-personality binary (busybox-style). The platformtest binary now
acts as a safety interposer for zfs/zpool when invoked via symlinks,
validating that all commands reference the hardcoded test pool before
delegating through sudo.

Key changes:
- New interposer.go: SetupInterposerPath creates a tmpdir with symlinks
  and replaces PATH; RunInterposer validates args and delegates via sudo
- Pool name, image path, and mountpoint are now hardcoded constants
- Remove ZpoolCreateArgs, Zpool struct, and CLI flags for pool config
- Remove logmockzfs/ shell scripts (logzfsenv, zfs wrapper)
- Simplify Makefile: no more logmockzfs invocation or root check
- Add -no-interposer flag for direct execution as root
- Safety: block -a (all) flags without pool reference, validate
  mountpoint paths against traversal, command-aware flag parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Schwarz
2026-02-10 19:56:38 +00:00
parent c393f35453
commit b3f24861c4
8 changed files with 357 additions and 152 deletions
+25 -18
View File
@@ -26,18 +26,23 @@ var bold = color.New(color.Bold)
var boldRed = color.New(color.Bold, color.FgHiRed)
var boldGreen = color.New(color.Bold, color.FgHiGreen)
const DefaultPoolImageSize = 200 * (1 << 20)
func main() {
// Multi-personality binary: check argv[0] to determine mode.
// When invoked via a symlink named "zfs" or "zpool", act as
// a safety interposer that validates commands and delegates
// to the real binary via sudo.
switch filepath.Base(os.Args[0]) {
case "zfs":
os.Exit(platformtest.RunInterposer("zfs"))
case "zpool":
os.Exit(platformtest.RunInterposer("zpool"))
}
// Test harness mode
var args HarnessArgs
flag.StringVar(&args.CreateArgs.PoolName, "poolname", "", "")
flag.StringVar(&args.CreateArgs.ImagePath, "imagepath", "", "")
flag.Int64Var(&args.CreateArgs.ImageSize, "imagesize", DefaultPoolImageSize, "")
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
flag.BoolVar(&args.NoInterposer, "no-interposer", false, "skip safety interposer; symlink zfs/zpool directly to real binaries (requires running as root)")
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
flag.StringVar(&args.Run, "run", "", "")
flag.StringVar(&args.Run, "run", "", "regex to filter test cases")
flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
flag.Parse()
@@ -49,7 +54,7 @@ func main() {
var exitWithErr = fmt.Errorf("exit with error")
type HarnessArgs struct {
CreateArgs platformtest.ZpoolCreateArgs
NoInterposer bool
StopAndKeepPoolOnFail bool
Run string
}
@@ -90,6 +95,14 @@ func (i *invocation) RegisterChild(c *invocation) error {
func HarnessRun(args HarnessArgs) error {
// Set up interposer: create symlinks and replace PATH.
// In direct mode, zfs/zpool symlink to real binaries (no sudo, no validation).
_, cleanup, err := platformtest.SetupInterposerPath("", args.NoInterposer)
if err != nil {
return errors.Wrap(err, "set up interposer PATH")
}
defer cleanup()
runRE := regexp.MustCompile(args.Run)
outlets := logger.NewOutlets()
@@ -104,11 +117,6 @@ func HarnessRun(args HarnessArgs) error {
}
outlets.Add(outlet, level)
logger := logger.NewLogger(outlets, 1*time.Second)
if err := args.CreateArgs.Validate(); err != nil {
logger.Error(err.Error())
panic(err)
}
ctx := context.Background()
defer trace.WithTaskFromStackUpdateCtx(&ctx)()
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
@@ -128,14 +136,13 @@ func HarnessRun(args HarnessArgs) error {
bold.Printf("BEGIN TEST CASE %s\n", inv)
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
if err != nil {
if err := platformtest.CreateOrReplaceZpool(ctx, ex); err != nil {
panic(errors.Wrap(err, "create test pool"))
}
ctx := &platformtest.Context{
Context: ctx,
RootDataset: filepath.Join(pool.Name(), "rootds"),
RootDataset: filepath.Join(platformtest.PlatformTestPoolName, "rootds"),
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
stinv := newInvocation(stf, id)
err := inv.RegisterChild(stinv)
@@ -158,7 +165,7 @@ func HarnessRun(args HarnessArgs) error {
return exitWithErr
}
if err := pool.Destroy(ctx, ex); err != nil {
if err := platformtest.DestroyZpool(ctx, ex); err != nil {
panic(fmt.Sprintf("error destroying test pool: %s", err))
}