From 5342002d77ed730a9d391cb296b98c7bb4723ec6 Mon Sep 17 00:00:00 2001 From: Johannes Edmeier Date: Thu, 12 Feb 2026 21:01:50 +0100 Subject: [PATCH 1/2] fix: make mount failure a hard error in diskfill createBundle A silent mount failure causes the fill to write to the sidecar's own filesystem instead of the target, which is wrong behavior that should fail fast. --- go/action_kit_commons/diskfill/diskfill_runc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/action_kit_commons/diskfill/diskfill_runc.go b/go/action_kit_commons/diskfill/diskfill_runc.go index 87ad31a..784c88c 100644 --- a/go/action_kit_commons/diskfill/diskfill_runc.go +++ b/go/action_kit_commons/diskfill/diskfill_runc.go @@ -142,7 +142,7 @@ func createBundle(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarO if targetPath != "" { if err := bundle.MountFromProcess(ctx, sidecar.TargetProcess.Pid, targetPath, mountpointInContainer); err != nil { - log.Warn().Err(err).Msgf("failed to mount %s", targetPath) + return nil, fmt.Errorf("failed to mount %s: %w", targetPath, err) } } From 7a02e9653b63bba9f4557b421e61972d50dbd720 Mon Sep 17 00:00:00 2001 From: Johannes Edmeier Date: Thu, 12 Feb 2026 21:01:56 +0100 Subject: [PATCH 2/2] feat: add target path validation for fill disk attacks Add CheckPathWritableRunc and CheckPathWritableProcess functions that verify the target directory exists and is writable before starting the fill. The runc variant creates a short-lived sidecar in the target's mount namespace; the process variant uses direct commands. Both clean up after themselves. --- .../diskfill/diskfill_process.go | 19 ++++++++++++-- .../diskfill/diskfill_runc.go | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/go/action_kit_commons/diskfill/diskfill_process.go b/go/action_kit_commons/diskfill/diskfill_process.go index 41a3101..fd2c87c 100644 --- a/go/action_kit_commons/diskfill/diskfill_process.go +++ b/go/action_kit_commons/diskfill/diskfill_process.go @@ -7,13 +7,14 @@ package diskfill import ( "context" "fmt" - "github.com/rs/zerolog/log" - "github.com/steadybit/action-kit/go/action_kit_commons/utils" "os/exec" "path/filepath" "strconv" "strings" "time" + + "github.com/rs/zerolog/log" + "github.com/steadybit/action-kit/go/action_kit_commons/utils" ) type diskfillProcess struct { @@ -92,3 +93,17 @@ func (df *diskfillProcess) Args() []string { func (df *diskfillProcess) Noop() bool { return df.cmd.Args[0] == "echo" && df.cmd.Args[1] == "noop" } + +func CheckPathWritableProcess(ctx context.Context, targetPath string) error { + if out, err := utils.RootCommandContext(ctx, "test", "-d", targetPath).CombinedOutput(); err != nil { + return fmt.Errorf("target path %q does not exist: %w: %s", targetPath, err, string(out)) + } + + checkFile := filepath.Join(targetPath, ".steadybit-diskfill-check") + if out, err := utils.RootCommandContext(ctx, "touch", checkFile).CombinedOutput(); err != nil { + return fmt.Errorf("target path %q is not writable: %w: %s", targetPath, err, string(out)) + } + + _ = utils.RootCommandContext(ctx, "rm", "-f", checkFile).Run() + return nil +} diff --git a/go/action_kit_commons/diskfill/diskfill_runc.go b/go/action_kit_commons/diskfill/diskfill_runc.go index 784c88c..ec8a23a 100644 --- a/go/action_kit_commons/diskfill/diskfill_runc.go +++ b/go/action_kit_commons/diskfill/diskfill_runc.go @@ -5,6 +5,7 @@ package diskfill import ( + "bytes" "context" "fmt" "path/filepath" @@ -183,6 +184,31 @@ func createBundle(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarO return bundle, nil } +func CheckPathWritableRunc(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarOpts, targetPath string) error { + bundle, err := createBundle(ctx, r, sidecar, targetPath, + "sh", "-c", "test -d "+mountpointInContainer+" && touch "+mountpointInContainer+"/.steadybit-diskfill-check && rm -f "+mountpointInContainer+"/.steadybit-diskfill-check") + if err != nil { + return fmt.Errorf("target path %q is not accessible: %w", targetPath, err) + } + defer func() { + if err := bundle.Remove(); err != nil { + log.Warn().Str("id", bundle.ContainerId()).Err(err).Msg("failed to remove bundle") + } + }() + + var errb bytes.Buffer + err = r.Run(ctx, bundle, ociruntime.IoOpts{Stderr: &errb}) + defer func() { + if err := r.Delete(context.Background(), bundle.ContainerId(), true); err != nil { + log.Debug().Str("id", bundle.ContainerId()).Err(err).Msg("failed to delete check container") + } + }() + if err != nil { + return fmt.Errorf("target path %q does not exist or is not writable: %w: %s", targetPath, err, errb.String()) + } + return nil +} + func getNextContainerId(executionId uuid.UUID, suffix string) string { return fmt.Sprintf("sb-diskfill-%d-%s-%s", time.Now().UnixMilli(), utils.ShortenUUID(executionId), suffix) }