Skip to content

Commit 7ece0ea

Browse files
committed
feat(security): limit WASI filesystem access in file-ops to necessary directories only
Replaces full filesystem access (--dir=/::/ ) with limited directory mappings in the file-ops external WASM wrapper. This improves security by granting WASI access only to directories actually needed for file operations. **Changes:** - tools/file_ops_external/main.go: Add path resolution and limited directory mapping - Parse file-ops arguments to identify file/directory paths - Resolve symlinks to real paths (handles Bazel sandbox symlinks) - Map only necessary directories to WASI (not entire filesystem) - Add debug logging for mapped directories **Security Impact:** Before: WASI had full filesystem access (--dir=/::/ ) After: WASI only accesses directories containing source/dest files **Benefits:** ✅ Maintains WASI security model (limited filesystem access) ✅ Maintains Bazel sandbox hermeticity ✅ Works with Bazel's symlinked sandbox paths ✅ Follows same approach as wasmsign2 wrapper **Testing:** - Verified file copy operations work correctly - Tested with Go component builds (calculator_component) This change aligns file-ops with the wasmsign2 wrapper security model, ensuring both tools maintain proper WASI security boundaries.
1 parent 8aa961e commit 7ece0ea

File tree

1 file changed

+116
-15
lines changed

1 file changed

+116
-15
lines changed

tools/file_ops_external/main.go

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package main
22

33
import (
4+
"fmt"
45
"log"
56
"os"
67
"os/exec"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/bazelbuild/rules_go/go/runfiles"
912
)
1013

1114
// Wrapper for external file operations WASM component with LOCAL AOT
1215
// This wrapper executes the WASM component via wasmtime, using locally-compiled
13-
// AOT for 100x faster startup with guaranteed Wasmtime version compatibility
16+
// AOT for 100x faster startup with guaranteed Wasmtime version compatibility.
17+
//
18+
// Security: Maps only necessary directories to WASI instead of full filesystem access.
1419
func main() {
1520
// Initialize Bazel runfiles
1621
r, err := runfiles.New()
@@ -49,36 +54,42 @@ func main() {
4954
log.Fatalf("WASM component not found at %s: %v", wasmComponent, err)
5055
}
5156

52-
// Build wasmtime command
57+
// Parse file-ops arguments and resolve paths
58+
resolvedArgs, dirs, err := resolveFileOpsPaths(os.Args[1:])
59+
if err != nil {
60+
log.Fatalf("Failed to resolve paths: %v", err)
61+
}
62+
63+
// Build wasmtime command with limited directory mappings
5364
var args []string
65+
args = append(args, "run")
66+
67+
// Add unique directory mappings (instead of --dir=/::/ for full access)
68+
uniqueDirs := uniqueStrings(dirs)
69+
for _, dir := range uniqueDirs {
70+
args = append(args, "--dir", dir)
71+
}
5472

5573
if useAOT {
5674
// Use locally-compiled AOT - guaranteed compatible with current Wasmtime version
5775
if os.Getenv("FILE_OPS_DEBUG") != "" {
5876
log.Printf("DEBUG: Using locally-compiled AOT at %s", aotPath)
77+
log.Printf("DEBUG: Mapped directories: %v", uniqueDirs)
5978
}
6079

61-
args = []string{
62-
"run",
63-
"--dir=/::/", // Preopen root directory for full filesystem access
64-
"--allow-precompiled",
65-
aotPath,
66-
}
80+
args = append(args, "--allow-precompiled", aotPath)
6781
} else {
6882
// Fallback to regular WASM (still much faster than embedded Go binary)
6983
if os.Getenv("FILE_OPS_DEBUG") != "" {
7084
log.Printf("DEBUG: AOT not available, using regular WASM")
85+
log.Printf("DEBUG: Mapped directories: %v", uniqueDirs)
7186
}
7287

73-
args = []string{
74-
"run",
75-
"--dir=/::/",
76-
wasmComponent,
77-
}
88+
args = append(args, wasmComponent)
7889
}
7990

80-
// Append original arguments
81-
args = append(args, os.Args[1:]...)
91+
// Append resolved file-ops arguments
92+
args = append(args, resolvedArgs...)
8293

8394
// Execute wasmtime
8495
cmd := exec.Command(wasmtimeBinary, args...)
@@ -93,3 +104,93 @@ func main() {
93104
log.Fatalf("Failed to execute wasmtime: %v", err)
94105
}
95106
}
107+
108+
// resolveFileOpsPaths resolves file paths in file-ops arguments
109+
// Returns resolved arguments and list of directories to map
110+
func resolveFileOpsPaths(args []string) ([]string, []string, error) {
111+
resolvedArgs := make([]string, 0, len(args))
112+
dirs := make([]string, 0)
113+
114+
// Flags that expect file/directory paths
115+
pathFlags := map[string]bool{
116+
"--src": true,
117+
"--dest": true,
118+
"--path": true,
119+
"--dir": true,
120+
"--output": true,
121+
}
122+
123+
for i := 0; i < len(args); i++ {
124+
arg := args[i]
125+
126+
// Check if this is a flag that expects a path
127+
if pathFlags[arg] && i+1 < len(args) {
128+
// Next argument is a file path
129+
resolvedArgs = append(resolvedArgs, arg)
130+
i++
131+
path := args[i]
132+
133+
// Resolve to real path (follows symlinks)
134+
realPath, err := filepath.EvalSymlinks(path)
135+
if err != nil {
136+
// If symlink evaluation fails, try absolute path
137+
realPath, err = filepath.Abs(path)
138+
if err != nil {
139+
return nil, nil, fmt.Errorf("failed to resolve path %s: %w", path, err)
140+
}
141+
}
142+
143+
resolvedArgs = append(resolvedArgs, realPath)
144+
145+
// Add directory for mapping
146+
dir := filepath.Dir(realPath)
147+
dirs = append(dirs, dir)
148+
} else if strings.Contains(arg, "=") && (strings.HasPrefix(arg, "--src=") ||
149+
strings.HasPrefix(arg, "--dest=") ||
150+
strings.HasPrefix(arg, "--path=") ||
151+
strings.HasPrefix(arg, "--dir=") ||
152+
strings.HasPrefix(arg, "--output=")) {
153+
// Handle --flag=value format
154+
parts := strings.SplitN(arg, "=", 2)
155+
if len(parts) == 2 {
156+
flag := parts[0]
157+
path := parts[1]
158+
159+
realPath, err := filepath.EvalSymlinks(path)
160+
if err != nil {
161+
realPath, err = filepath.Abs(path)
162+
if err != nil {
163+
return nil, nil, fmt.Errorf("failed to resolve path %s: %w", path, err)
164+
}
165+
}
166+
167+
resolvedArgs = append(resolvedArgs, flag+"="+realPath)
168+
169+
dir := filepath.Dir(realPath)
170+
dirs = append(dirs, dir)
171+
} else {
172+
resolvedArgs = append(resolvedArgs, arg)
173+
}
174+
} else {
175+
// Not a path argument, pass through as-is
176+
resolvedArgs = append(resolvedArgs, arg)
177+
}
178+
}
179+
180+
return resolvedArgs, dirs, nil
181+
}
182+
183+
// uniqueStrings returns unique strings from a slice
184+
func uniqueStrings(strs []string) []string {
185+
seen := make(map[string]bool)
186+
result := make([]string, 0, len(strs))
187+
188+
for _, s := range strs {
189+
if !seen[s] {
190+
seen[s] = true
191+
result = append(result, s)
192+
}
193+
}
194+
195+
return result
196+
}

0 commit comments

Comments
 (0)