Skip to content
6 changes: 6 additions & 0 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ jobs:
${{ matrix.package }}/go.mod
${{ matrix.package }}/go.sum

- name: Setup envd tests
run: |
# Install bindfs for FUSE mount tests
sudo apt-get update && sudo apt-get install -y bindfs
if: matrix.package == 'packages/envd'

- name: Setup orchestrator tests
run: |
# Enable unprivileged uffd mode
Expand Down
25 changes: 25 additions & 0 deletions packages/envd/internal/services/filesystem/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ import (
rpc "github.com/e2b-dev/infra/packages/envd/internal/services/spec/filesystem"
)

// Filesystem magic numbers from Linux kernel (include/uapi/linux/magic.h)
const (
nfsSuperMagic = 0x6969
cifsMagic = 0xFF534D42
smbSuperMagic = 0x517B
smb2MagicNumber = 0xFE534D42
fuseSuperMagic = 0x65735546
)

// IsPathOnNetworkMount checks if the given path is on a network filesystem mount.
// Returns true if the path is on NFS, CIFS, SMB, or FUSE filesystem.
func IsPathOnNetworkMount(path string) (bool, error) {
var statfs syscall.Statfs_t
if err := syscall.Statfs(path, &statfs); err != nil {
return false, fmt.Errorf("failed to statfs %s: %w", path, err)
}

switch statfs.Type {
case nfsSuperMagic, cifsMagic, smbSuperMagic, smb2MagicNumber, fuseSuperMagic:
return true, nil
default:
return false, nil
}
}

// getEntryType determines the type of file entry based on its mode and path.
// If the file is a symlink, it follows the symlink to determine the actual type.
func getEntryType(mode os.FileMode) rpc.FileType {
Expand Down
48 changes: 48 additions & 0 deletions packages/envd/internal/services/filesystem/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package filesystem

import (
"context"
"os"
"os/exec"
"os/user"
"path/filepath"
"testing"
Expand All @@ -13,6 +15,52 @@ import (
rpc "github.com/e2b-dev/infra/packages/envd/internal/services/spec/filesystem"
)

func TestIsPathOnNetworkMount(t *testing.T) {
t.Parallel()

// Test with a regular directory (should not be on network mount)
tempDir := t.TempDir()
isNetwork, err := IsPathOnNetworkMount(tempDir)
require.NoError(t, err)
assert.False(t, isNetwork, "temp directory should not be on a network mount")
Comment thread
mishushakov marked this conversation as resolved.
}

func TestIsPathOnNetworkMount_FuseMount(t *testing.T) {
t.Parallel()

// Require bindfs to be available
_, err := exec.LookPath("bindfs")
require.NoError(t, err, "bindfs must be installed for this test")

// Require fusermount to be available (needed for unmounting)
_, err = exec.LookPath("fusermount")
require.NoError(t, err, "fusermount must be installed for this test")

// Create source and mount directories
sourceDir := t.TempDir()
mountDir := t.TempDir()

// Mount sourceDir onto mountDir using bindfs (FUSE)
ctx := context.Background()
cmd := exec.CommandContext(ctx, "bindfs", sourceDir, mountDir)
require.NoError(t, cmd.Run(), "failed to mount bindfs")

// Ensure we unmount on cleanup
t.Cleanup(func() {
_ = exec.CommandContext(context.Background(), "fusermount", "-u", mountDir).Run()
})

// Test that the FUSE mount is detected
isNetwork, err := IsPathOnNetworkMount(mountDir)
require.NoError(t, err)
assert.True(t, isNetwork, "FUSE mount should be detected as network filesystem")

// Test that the source directory is NOT detected as network mount
isNetworkSource, err := IsPathOnNetworkMount(sourceDir)
require.NoError(t, err)
assert.False(t, isNetworkSource, "source directory should not be detected as network filesystem")
}

func TestGetEntryType(t *testing.T) {
t.Parallel()

Expand Down
9 changes: 9 additions & 0 deletions packages/envd/internal/services/filesystem/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ func (s Service) watchHandler(ctx context.Context, req *connect.Request[rpc.Watc
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("path %s not a directory: %w", watchPath, err))
}

// Check if path is on a network filesystem mount
isNetworkMount, err := IsPathOnNetworkMount(watchPath)
Comment thread
mishushakov marked this conversation as resolved.
if err != nil {
return connect.NewError(connect.CodeInternal, fmt.Errorf("error checking mount status: %w", err))
}
if isNetworkMount {
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("cannot watch path on network filesystem: %s", watchPath))
}

w, err := fsnotify.NewWatcher()
if err != nil {
return connect.NewError(connect.CodeInternal, fmt.Errorf("error creating watcher: %w", err))
Expand Down
9 changes: 9 additions & 0 deletions packages/envd/internal/services/filesystem/watch_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ func (s Service) CreateWatcher(ctx context.Context, req *connect.Request[rpc.Cre
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("path %s not a directory: %w", watchPath, err))
}

// Check if path is on a network filesystem mount
isNetworkMount, err := IsPathOnNetworkMount(watchPath)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error checking mount status: %w", err))
}
if isNetworkMount {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("cannot watch path on network filesystem: %s", watchPath))
}

watcherId := "w" + id.Generate()

w, err := CreateFileWatcher(ctx, watchPath, req.Msg.GetRecursive(), watcherId, s.logger)
Expand Down
Loading