Skip to content

Commit ecb15b2

Browse files
authored
Adds program to allow syscalls to be canceled
1 parent 9ba9d1b commit ecb15b2

File tree

12 files changed

+272
-39
lines changed

12 files changed

+272
-39
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ ARG BIN=trident_orchestrator
2323
ARG CLI_BIN=tridentctl
2424
ARG CHWRAP_BIN=chwrap.tar
2525
ARG NODE_PREP_BIN=node_prep
26+
ARG SYSWRAP_BIN=syswrap
2627

2728
COPY ${BIN} /trident_orchestrator
2829
COPY ${CLI_BIN} /bin/tridentctl
2930
COPY ${NODE_PREP_BIN} /node_prep
31+
COPY ${SYSWRAP_BIN} /syswrap
3032
ADD ${CHWRAP_BIN} /
3133

3234
ENTRYPOINT ["/bin/tridentctl"]

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ binaries_for_platform = $(call go_build,tridentctl,./cli,$1,$2)\
200200
$(if $(findstring darwin,$1),,\
201201
&& $(call go_build,trident_orchestrator,.,$1,$2)\
202202
$(if $(findstring linux,$1),\
203+
&& $(call go_build,syswrap,./cmd/syswrap,$1,$2) \
203204
&& $(call chwrap_build,$1,$2) \
204205
&& $(call node_prep_build,$1,$2) ))
205206

@@ -239,6 +240,7 @@ docker_build_linux = $1 build \
239240
--build-arg CLI_BIN=$(call binary_path,tridentctl,$2) \
240241
--build-arg NODE_PREP_BIN=$(call binary_path,node_prep,$2) \
241242
--build-arg CHWRAP_BIN=$(call binary_path,chwrap.tar,$2) \
243+
--build-arg SYSWRAP_BIN=${call binary_path,syswrap,$2} \
242244
--tag $3 \
243245
--rm \
244246
$(if $(findstring $(DOCKER_BUILDX_BUILD_CLI),$1),--builder trident-builder) \

cmd/syswrap/main.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"github.com/netapp/trident/internal/syswrap/unix"
10+
)
11+
12+
var syscalls = map[string]func([]string) (interface{}, error){
13+
"statfs": unix.Statfs,
14+
"exists": unix.Exists,
15+
}
16+
17+
type result struct {
18+
output interface{}
19+
err error
20+
}
21+
22+
func main() {
23+
timeout, syscall, args, err := parseArgs(os.Args)
24+
if err != nil {
25+
exit(err)
26+
}
27+
28+
select {
29+
case <-time.After(timeout):
30+
exit(fmt.Errorf("timed out waiting for %s", syscall))
31+
case res := <-func() chan result {
32+
r := make(chan result)
33+
go func() {
34+
s, ok := syscalls[syscall]
35+
if !ok {
36+
r <- result{
37+
err: fmt.Errorf("unknown syscall: %s", syscall),
38+
}
39+
}
40+
i, e := s(args)
41+
r <- result{
42+
output: i,
43+
err: e,
44+
}
45+
}()
46+
return r
47+
}():
48+
if res.err != nil {
49+
exit(res.err)
50+
}
51+
_ = json.NewEncoder(os.Stdout).Encode(res.output)
52+
}
53+
}
54+
55+
func exit(err error) {
56+
_, _ = fmt.Fprintf(os.Stderr, "error: %v", err)
57+
os.Exit(1)
58+
}
59+
60+
func parseArgs(args []string) (timeout time.Duration, syscall string, syscallArgs []string, err error) {
61+
if len(args) < 3 {
62+
err = fmt.Errorf("expected at least 3 arguments")
63+
return
64+
}
65+
timeout, err = time.ParseDuration(args[1])
66+
if err != nil {
67+
return
68+
}
69+
70+
syscall = args[2]
71+
72+
if len(args) > 3 {
73+
syscallArgs = args[3:]
74+
}
75+
return
76+
}

frontend/csi/node_server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
tridentconfig "github.com/netapp/trident/config"
2626
"github.com/netapp/trident/internal/fiji"
27+
"github.com/netapp/trident/internal/syswrap"
2728
. "github.com/netapp/trident/logging"
2829
"github.com/netapp/trident/pkg/collection"
2930
"github.com/netapp/trident/pkg/convert"
@@ -54,6 +55,7 @@ const (
5455
maximumNodeReconciliationJitter = 5000 * time.Millisecond
5556
nvmeMaxFlushWaitDuration = 6 * time.Minute
5657
csiNodeLockTimeout = 60 * time.Second
58+
fsUnavailableTimeout = 5 * time.Second
5759
)
5860

5961
var (
@@ -350,7 +352,7 @@ func (p *Plugin) NodeGetVolumeStats(
350352
}
351353

352354
// Ensure volume is published at path
353-
exists, err := p.osutils.PathExists(req.GetVolumePath())
355+
exists, err := syswrap.Exists(ctx, req.GetVolumePath(), fsUnavailableTimeout)
354356
if !exists || err != nil {
355357
return nil, status.Error(codes.NotFound,
356358
fmt.Sprintf("could not find volume mount at path: %s; %v", req.GetVolumePath(), err))

internal/syswrap/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# syswrap
2+
3+
`syswrap` is a small program to wrap system calls with timeouts. In Go, syscalls cannot be cleaned up if they are
4+
blocked, for example if `lstat` is called on an inaccessible NFS mount, but because Go can create additional goroutines
5+
the calling application will not block. This can cause resource exhaustion if syscalls are not bounded.
6+
7+
Linux will clean up syscalls if the owning process ends, so `syswrap` exists to allow Trident to create a new process
8+
that owns a potentially blocking syscall.
9+
10+
The Trident-accessible interface is in this package.
11+
12+
## Usage
13+
14+
`syswrap <timeout> <syscall> <syscall args...>`
15+
16+
`<timeout>` is in Go format, i.e. `30s`
17+
18+
`<syscall>` is the name of the call, see `cmd/syswrap/main.go`
19+
20+
`<syscall args...>` are string representations of any arguments required by the call.
21+
22+
## Adding Syscalls
23+
24+
See `syswrap.Exists` for a cross-platform example, and `syswrap.Statfs` for a Linux-only example.
25+
26+
There are 3 steps to adding a new syscall:
27+
28+
1. Add func to `internal/syswrap` package. This func calls the syswrap binary, and it should also call the syscall
29+
itself if the syswrap binary is not found.
30+
2. Add func to `internal/syswrap/unix` package. The func must have the signature
31+
`func(args []string) (output interface{}, err error)`, and parse its args then call the syscall. Any fields in output
32+
that need to be used by Trident must be exported.
33+
3. Add func name to `cmd/syswrap/main.syscalls` map.

internal/syswrap/syswrap.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build windows || darwin
2+
3+
package syswrap
4+
5+
import (
6+
"context"
7+
"os"
8+
"time"
9+
)
10+
11+
func Exists(_ context.Context, path string, _ time.Duration) (bool, error) {
12+
_, err := os.Stat(path)
13+
return err == nil, nil
14+
}

internal/syswrap/syswrap_linux.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Package syswrap wraps syscalls that need to be canceled
2+
package syswrap
3+
4+
import (
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"os"
9+
"time"
10+
11+
"golang.org/x/sys/unix"
12+
13+
"github.com/netapp/trident/utils/exec"
14+
)
15+
16+
const syswrapBin = "/syswrap"
17+
18+
func Statfs(ctx context.Context, path string, timeout time.Duration) (unix.Statfs_t, error) {
19+
buf, err := exec.NewCommand().Execute(ctx, syswrapBin, timeout.String(), "statfs", path)
20+
if err != nil {
21+
// If syswrap is unavailable fall back to blocking call. This may hang if NFS backend is unreachable
22+
var pe *os.PathError
23+
ok := errors.As(err, &pe)
24+
if !ok {
25+
return unix.Statfs_t{}, err
26+
}
27+
28+
var fsStat unix.Statfs_t
29+
err = unix.Statfs(path, &fsStat)
30+
return fsStat, err
31+
}
32+
33+
var b unix.Statfs_t
34+
err = json.Unmarshal(buf, &b)
35+
return b, err
36+
}
37+
38+
func Exists(ctx context.Context, path string, timeout time.Duration) (bool, error) {
39+
buf, err := exec.NewCommand().Execute(ctx, syswrapBin, timeout.String(), "exists", path)
40+
if err != nil {
41+
// If syswrap is unavailable fall back to blocking call. This may hang if NFS backend is unreachable
42+
var pe *os.PathError
43+
ok := errors.As(err, &pe)
44+
if !ok {
45+
return false, err
46+
}
47+
48+
_, err = os.Stat(path)
49+
return err == nil, err
50+
}
51+
52+
var b bool
53+
err = json.Unmarshal(buf, &b)
54+
return b, err
55+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package syswrap
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
11+
"github.com/netapp/trident/utils/exec"
12+
)
13+
14+
// errors.Is should work to detect PathError from Execute, but it currently does not. If this test starts to fail
15+
// then the errors.As calls in this package should be converted to errors.Is.
16+
func TestSyswrapUnavailableRequiresAs(t *testing.T) {
17+
_, err := os.Stat(syswrapBin)
18+
assert.Error(t, err)
19+
wd, err := os.Getwd()
20+
assert.NoError(t, err)
21+
_, err = exec.NewCommand().Execute(context.Background(), syswrapBin, "1s", "statfs", wd)
22+
assert.Error(t, err)
23+
assert.False(t, errors.Is(err, &os.PathError{}))
24+
25+
var pe *os.PathError
26+
assert.True(t, errors.As(err, &pe))
27+
}

internal/syswrap/unix/syscall.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package unix parses string arguments and calls the system call
2+
package unix
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"golang.org/x/sys/unix"
9+
)
10+
11+
func Statfs(args []string) (interface{}, error) {
12+
if len(args) != 1 {
13+
return nil, fmt.Errorf("expected 1 argument")
14+
}
15+
var buf unix.Statfs_t
16+
err := unix.Statfs(args[0], &buf)
17+
return &buf, err
18+
}
19+
20+
func Exists(args []string) (interface{}, error) {
21+
if len(args) != 1 {
22+
return nil, fmt.Errorf("expected 1 argument")
23+
}
24+
_, err := os.Stat(args[0])
25+
exists := err == nil
26+
return &exists, err
27+
}

mocks/mock_utils/mock_osutils/mock_osutils.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)