Skip to content

Commit ef8962a

Browse files
committed
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.
1 parent 5342002 commit ef8962a

7 files changed

Lines changed: 65 additions & 39 deletions

File tree

go/action_kit_commons/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 1.5.15
4+
5+
- Add filldisk helpers to validate target directory
6+
37
## 1.5.14
48

59
- Set OOM score adjustment in disc fill command

go/action_kit_commons/diskfill/diskfill_process.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ package diskfill
77
import (
88
"context"
99
"fmt"
10-
"github.com/rs/zerolog/log"
11-
"github.com/steadybit/action-kit/go/action_kit_commons/utils"
1210
"os/exec"
1311
"path/filepath"
1412
"strconv"
1513
"strings"
1614
"time"
15+
16+
"github.com/rs/zerolog/log"
17+
"github.com/steadybit/action-kit/go/action_kit_commons/utils"
1718
)
1819

1920
type diskfillProcess struct {
@@ -92,3 +93,17 @@ func (df *diskfillProcess) Args() []string {
9293
func (df *diskfillProcess) Noop() bool {
9394
return df.cmd.Args[0] == "echo" && df.cmd.Args[1] == "noop"
9495
}
96+
97+
func CheckPathWritableProcess(ctx context.Context, targetPath string) error {
98+
if out, err := utils.RootCommandContext(ctx, "test", "-d", targetPath).CombinedOutput(); err != nil {
99+
return fmt.Errorf("target path %q does not exist: %w: %s", targetPath, err, string(out))
100+
}
101+
102+
checkFile := filepath.Join(targetPath, ".steadybit-diskfill-check")
103+
if out, err := utils.RootCommandContext(ctx, "touch", checkFile).CombinedOutput(); err != nil {
104+
return fmt.Errorf("target path %q is not writable: %w: %s", targetPath, err, string(out))
105+
}
106+
107+
_ = utils.RootCommandContext(ctx, "rm", "-f", checkFile).Run()
108+
return nil
109+
}

go/action_kit_commons/diskfill/diskfill_runc.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package diskfill
66

77
import (
8+
"bytes"
89
"context"
910
"fmt"
1011
"path/filepath"
@@ -142,7 +143,7 @@ func createBundle(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarO
142143

143144
if targetPath != "" {
144145
if err := bundle.MountFromProcess(ctx, sidecar.TargetProcess.Pid, targetPath, mountpointInContainer); err != nil {
145-
return nil, fmt.Errorf("failed to mount %s: %w", targetPath, err)
146+
return nil, err
146147
}
147148
}
148149

@@ -183,6 +184,30 @@ func createBundle(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarO
183184
return bundle, nil
184185
}
185186

187+
func CheckPathWritableRunc(ctx context.Context, r ociruntime.OciRuntime, sidecar SidecarOpts, targetPath string) error {
188+
bundle, err := createBundle(ctx, r, sidecar, targetPath, "sh", "-c", fmt.Sprintf("test -d %s && touch %s/.steadybit-diskfill-check && rm -f %s/.steadybit-diskfill-check", mountpointInContainer, mountpointInContainer, mountpointInContainer))
189+
if err != nil {
190+
return fmt.Errorf("target path %q is not accessible: %w", targetPath, err)
191+
}
192+
defer func() {
193+
if err := bundle.Remove(); err != nil {
194+
log.Warn().Str("id", bundle.ContainerId()).Err(err).Msg("failed to remove bundle")
195+
}
196+
}()
197+
198+
var errb bytes.Buffer
199+
err = r.Run(ctx, bundle, ociruntime.IoOpts{Stderr: &errb})
200+
defer func() {
201+
if err := r.Delete(context.Background(), bundle.ContainerId(), true); err != nil {
202+
log.Debug().Str("id", bundle.ContainerId()).Err(err).Msg("failed to delete check container")
203+
}
204+
}()
205+
if err != nil {
206+
return fmt.Errorf("target path %q does not exist or is not writable: %w: %s", targetPath, err, errb.String())
207+
}
208+
return nil
209+
}
210+
186211
func getNextContainerId(executionId uuid.UUID, suffix string) string {
187212
return fmt.Sprintf("sb-diskfill-%d-%s-%s", time.Now().UnixMilli(), utils.ShortenUUID(executionId), suffix)
188213
}

go/action_kit_commons/diskfill/read_diskspace.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ func readDiskUsageProcess(ctx context.Context, path string) (*DiskUsage, error)
6868
var outb, errb bytes.Buffer
6969
cmd.Stdout = &outb
7070
cmd.Stderr = &errb
71-
72-
err := cmd.Run()
73-
if err != nil {
71+
if err := cmd.Run(); err != nil {
7472
return nil, fmt.Errorf("failed to read disk usage: %w: %s", err, errb.String())
7573
}
7674

go/action_kit_commons/network/dig_runner.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"github.com/steadybit/action-kit/go/action_kit_commons/utils"
1717
"io"
1818
"os/exec"
19-
"runtime/trace"
2019
)
2120

2221
type DigRunner interface {
@@ -50,7 +49,6 @@ type RuncDigRunner struct {
5049
var _ DigRunner = (*RuncDigRunner)(nil)
5150

5251
func (r *RuncDigRunner) Run(ctx context.Context, arg []string, stdin io.Reader) ([]byte, error) {
53-
defer trace.StartRegion(ctx, "RuncDigRunner.Run").End()
5452
id := getNextContainerId(r.Sidecar.ExecutionId, "dig", r.Sidecar.IdSuffix)
5553

5654
bundle, err := r.Runc.Create(ctx, "/", id)

go/action_kit_commons/ociruntime/container_bundle.go

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
package ociruntime
66

77
import (
8-
"bytes"
98
"context"
109
"errors"
1110
"fmt"
12-
"github.com/rs/zerolog/log"
13-
"github.com/steadybit/action-kit/go/action_kit_commons/utils"
1411
"os"
1512
"path/filepath"
16-
"runtime/trace"
1713
"strconv"
14+
15+
"github.com/rs/zerolog/log"
16+
"github.com/steadybit/action-kit/go/action_kit_commons/utils"
1817
)
1918

2019
type containerBundle struct {
@@ -86,38 +85,28 @@ func (b *containerBundle) mountRootfsOverlay(ctx context.Context, image string)
8685
}
8786

8887
func (b *containerBundle) CopyFileFromProcess(ctx context.Context, pid int, fromPath, toPath string) error {
89-
defer trace.StartRegion(ctx, "utils.CopyFileFromProcessToBundle").End()
90-
var out bytes.Buffer
91-
cmd := utils.RootCommandContext(ctx, "cat", filepath.Join("/proc", strconv.Itoa(pid), "root", fromPath))
92-
cmd.Stdout = &out
93-
cmd.Stderr = &out
94-
if err := cmd.Run(); err != nil {
95-
return fmt.Errorf("%w: %s", err, out.String())
88+
out, err := utils.RootCommandContext(ctx, "cat", filepath.Join("/proc", strconv.Itoa(pid), "root", fromPath)).CombinedOutput()
89+
if err != nil {
90+
return fmt.Errorf("failed to mount %s (%w): %s", fromPath, err, out)
9691
}
9792

98-
return os.WriteFile(filepath.Join(b.path, "rootfs", toPath), out.Bytes(), 0644)
93+
return os.WriteFile(filepath.Join(b.path, "rootfs", toPath), out, 0644)
9994
}
10095

10196
func (b *containerBundle) MountFromProcess(ctx context.Context, fromPid int, fromPath, toPath string) error {
102-
defer trace.StartRegion(ctx, "utils.MountFromProcessToBundle").End()
103-
10497
mountpoint := filepath.Join(b.path, "rootfs", toPath)
10598
log.Trace().
10699
Int("fromPid", fromPid).
107100
Str("fromPath", fromPath).
108-
Str("mountpoint", mountpoint).
101+
Str("mount-point", mountpoint).
109102
Msg("mount from process to bundle")
110103

111104
if err := os.Mkdir(mountpoint, 0755); err != nil && !os.IsExist(err) {
112-
return fmt.Errorf("failed to create mountpoint %s: %w", mountpoint, err)
105+
return fmt.Errorf("failed to create mount point %s: %w", mountpoint, err)
113106
}
114107

115-
var out bytes.Buffer
116-
cmd := utils.RootCommandContext(ctx, nsmountPath, strconv.Itoa(fromPid), fromPath, strconv.Itoa(os.Getpid()), mountpoint)
117-
cmd.Stdout = &out
118-
cmd.Stderr = &out
119-
if err := cmd.Run(); err != nil {
120-
return fmt.Errorf("%w: %s", err, out.String())
108+
if out, err := utils.RootCommandContext(ctx, nsmountPath, strconv.Itoa(fromPid), fromPath, strconv.Itoa(os.Getpid()), mountpoint).CombinedOutput(); err != nil {
109+
return fmt.Errorf("failed to mount %s (%w): %s", fromPath, err, out)
121110
}
122111
b.addFinalizer(func() error {
123112
return unmount(context.Background(), mountpoint)

go/action_kit_commons/ociruntime/utils.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,19 +515,16 @@ func searchNamespacePathInProcess(ctx context.Context, inode uint64, nsType spec
515515
}
516516

517517
func readCgroupPath(ctx context.Context, pid int) (string, error) {
518-
var out bytes.Buffer
519-
cmd := utils.RootCommandContext(ctx, nsenterPath, "-t", "1", "-C", "--", "cat", filepath.Join("/proc", strconv.Itoa(pid), "cgroup"))
520-
cmd.Stdout = &out
521-
cmd.Stderr = &out
522-
if err := cmd.Run(); err != nil {
523-
return "", fmt.Errorf("%w: %s", err, out.String())
518+
out, err := utils.RootCommandContext(ctx, nsenterPath, "-t", "1", "-C", "--", "cat", filepath.Join("/proc", strconv.Itoa(pid), "cgroup")).CombinedOutput()
519+
if err != nil {
520+
return "", fmt.Errorf("%w: %s", err, out)
524521
}
525522

526-
if cgroup := parseProcCgroupFile(out.String()); cgroup != "" {
523+
if cgroup := parseProcCgroupFile(string(out)); cgroup != "" {
527524
return cgroup, nil
528-
} else {
529-
return "", fmt.Errorf("failed to read cgroup for pid %d\n%s", pid, out.String())
530525
}
526+
527+
return "", fmt.Errorf("failed to read cgroup for pid %d\n%s", pid, out)
531528
}
532529

533530
func parseProcCgroupFile(s string) string {

0 commit comments

Comments
 (0)