Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/vishvananda/netns v0.0.5
go.uber.org/mock v0.6.0
go.uber.org/zap v1.27.1
golang.org/x/sys v0.40.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.0
Expand Down Expand Up @@ -149,7 +150,6 @@ require (
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
Expand Down
10 changes: 8 additions & 2 deletions pkg/host/internal/sriov/sriov.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import (
"github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/vars"
)

const (
// sysfsWriteTimeout is the timeout for writing to sysfs files (e.g. sriov_numvfs).
// Kernel drivers can block indefinitely on these writes if the device is in a bad state.
sysfsWriteTimeout = 2 * time.Minute
)

type interfaceToConfigure struct {
Iface sriovnetworkv1.Interface
IfaceStatus sriovnetworkv1.InterfaceExt
Expand Down Expand Up @@ -76,15 +82,15 @@ func (s *sriov) SetSriovNumVfs(pciAddr string, numVfs int) error {
log.Log.V(2).Info("SetSriovNumVfs(): set NumVfs", "device", pciAddr, "numVfs", numVfs)
numVfsFilePath := filepath.Join(vars.FilesystemRoot, consts.SysBusPciDevices, pciAddr, consts.NumVfsFile)
bs := []byte(strconv.Itoa(numVfs))
err := os.WriteFile(numVfsFilePath, []byte("0"), os.ModeAppend)
err := utils.WriteFileWithTimeout(numVfsFilePath, []byte("0"), os.ModeAppend, sysfsWriteTimeout)
if err != nil {
log.Log.Error(err, "SetSriovNumVfs(): fail to reset NumVfs file", "path", numVfsFilePath)
return err
}
if numVfs == 0 {
return nil
}
err = os.WriteFile(numVfsFilePath, bs, os.ModeAppend)
err = utils.WriteFileWithTimeout(numVfsFilePath, bs, os.ModeAppend, sysfsWriteTimeout)
if err != nil {
log.Log.Error(err, "SetSriovNumVfs(): fail to set NumVfs file", "path", numVfsFilePath)
return err
Expand Down
20 changes: 20 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,23 @@ func GetChrootExtension() string {
}
return fmt.Sprintf("chroot %s%s", vars.FilesystemRoot, consts.Host)
}

// WriteFileWithTimeout writes data to a file with a timeout.
// This is useful for writing to sysfs files where the kernel driver may block
// indefinitely if it is in a bad state.
// Note: if the timeout expires, the write goroutine will remain blocked in the
// kernel; it cannot be canceled but will be cleaned up when the process exits.
func WriteFileWithTimeout(path string, data []byte, perm os.FileMode, timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
ch := make(chan error, 1)
go func() {
ch <- os.WriteFile(path, data, perm)
}()
select {
case err := <-ch:
return err
case <-timer.C:
return fmt.Errorf("timeout writing to file %s after %v", path, timeout)
}
}
46 changes: 46 additions & 0 deletions pkg/utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package utils

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)

func TestWriteFileWithTimeout_Success(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "test")
data := []byte("hello")

err := WriteFileWithTimeout(tmpFile, data, 0644, 5*time.Second)
assert.NoError(t, err, "expected no error writing file")

got, err := os.ReadFile(tmpFile)
assert.NoError(t, err, "expected no error reading file")
assert.Equal(t, data, got, "file contents do not match expected")
}

func TestWriteFileWithTimeout_WriteError(t *testing.T) {
// Writing to a path that doesn't exist should return the underlying error, not a timeout.
err := WriteFileWithTimeout("/nonexistent/dir/file", []byte("x"), 0644, 5*time.Second)
assert.Error(t, err, "expected error for nonexistent path")
}

func TestWriteFileWithTimeout_Timeout(t *testing.T) {
// A named pipe (FIFO) blocks on open/write until a reader is connected,
// which makes it a reliable way to simulate a blocking write.
fifoPath := filepath.Join(t.TempDir(), "fifo")
assert.NoError(t, unix.Mkfifo(fifoPath, 0600), "failed to create FIFO")
defer os.Remove(fifoPath)

start := time.Now()
err := WriteFileWithTimeout(fifoPath, []byte("data"), 0644, 100*time.Millisecond)
elapsed := time.Since(start)

assert.EqualError(t, err, "timeout writing to file "+fifoPath+" after 100ms", "expected timeout error")
// The elapsed time should be close to the specified timeout, indicating that the function properly timed out.
assert.GreaterOrEqual(t, elapsed, 100*time.Millisecond, "function returned too quickly, timeout may not have triggered")
assert.Less(t, elapsed, 5*time.Second, "function took too long, timeout did not fire in time")
}
Loading