Skip to content

Commit 46db175

Browse files
committed
feat: Add file bind mount support via arca-filesystem-service
VirtioFS only supports directory shares, so file bind mounts require mounting the parent directory via VirtioFS and then creating a bind mount for the specific file inside the container's mount namespace. Changes: - Add CreateBindMount gRPC endpoint to arca-filesystem-service - Implement mount namespace switching to perform bind mount in container context - Add container PID discovery via ARCA_CONTAINER_ID environment variable - Fix Mount.swift to create empty file (not directory) for file bind mount targets - Rename arca-overlayfs-service to arca-filesystem-service (more accurate name) - Update cross-compilation build script for Linux ARM64 The bind mount is performed inside the container's mount namespace so it is visible to the container process and persists correctly. Closes Arca issue apple#7
1 parent 4339433 commit 46db175

File tree

8 files changed

+480
-41
lines changed

8 files changed

+480
-41
lines changed

Sources/ContainerizationOS/Mount/Mount.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,20 @@ extension Mount {
151151
if let perms = createWithPerms {
152152
try mkdirAll(targetParent, perms)
153153
}
154-
try mkdirAll(target, 0o755)
154+
155+
// Check if this is a file bind mount (signaled by "arca-file-bind" option)
156+
// For file bind mounts, create an empty file at the target instead of a directory
157+
let isFileBindMount = self.options.contains("arca-file-bind")
158+
if isFileBindMount {
159+
// Create parent directory if needed
160+
try mkdirAll(targetParent, 0o755)
161+
// Create empty file at target
162+
if !FileManager.default.fileExists(atPath: target) {
163+
_ = FileManager.default.createFile(atPath: target, contents: nil)
164+
}
165+
} else {
166+
try mkdirAll(target, 0o755)
167+
}
155168

156169
if opts.flags & Int32(MS_REMOUNT) == 0 || !dataString.isEmpty {
157170
let result = _mount(self.source, target, self.type, UInt(originalFlags), dataString)

vminitd/Sources/vminitd/Application.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,27 +146,28 @@ struct Application {
146146
log.warning("arca-wireguard-service binary not found at \(wireGuardServicePath), WireGuard networking will not be available")
147147
}
148148

149-
// Start arca-overlayfs-service in background for OverlayFS layer mounting
149+
// Start arca-filesystem-service in background for filesystem operations
150150
// This service listens on vsock port 51821 (accessible from host via container.dialVsock())
151-
let overlayFSServicePath = "/sbin/arca-overlayfs-service"
152-
let overlayFSServiceExists = FileManager.default.fileExists(atPath: overlayFSServicePath)
153-
log.info("arca-overlayfs-service binary exists: \(overlayFSServiceExists) at \(overlayFSServicePath)")
154-
155-
if overlayFSServiceExists {
156-
log.info("starting arca-overlayfs-service...")
157-
var overlayFSService = Command(overlayFSServicePath)
151+
// Provides: filesystem sync, upperdir enumeration (docker diff), bind mounts (file volumes), archive operations
152+
let filesystemServicePath = "/sbin/arca-filesystem-service"
153+
let filesystemServiceExists = FileManager.default.fileExists(atPath: filesystemServicePath)
154+
log.info("arca-filesystem-service binary exists: \(filesystemServiceExists) at \(filesystemServicePath)")
155+
156+
if filesystemServiceExists {
157+
log.info("starting arca-filesystem-service...")
158+
var filesystemService = Command(filesystemServicePath)
158159
// Leave stdin/stdout/stderr as nil for detached background service
159-
overlayFSService.stdin = nil
160-
overlayFSService.stdout = nil
161-
overlayFSService.stderr = .standardError // Log errors to vminitd stderr
160+
filesystemService.stdin = nil
161+
filesystemService.stdout = nil
162+
filesystemService.stderr = .standardError // Log errors to vminitd stderr
162163
do {
163-
try overlayFSService.start()
164-
log.info("arca-overlayfs-service started successfully on vsock port 51821")
164+
try filesystemService.start()
165+
log.info("arca-filesystem-service started successfully on vsock port 51821")
165166
} catch {
166-
log.error("failed to start arca-overlayfs-service: \(error)")
167+
log.error("failed to start arca-filesystem-service: \(error)")
167168
}
168169
} else {
169-
log.warning("arca-overlayfs-service binary not found at \(overlayFSServicePath), OverlayFS mounting will not be available")
170+
log.warning("arca-filesystem-service binary not found at \(filesystemServicePath), filesystem operations will not be available")
170171
}
171172

172173
// Start arca-process-service in background for process control
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
#!/bin/bash
2-
# Build Arca Filesystem Service binary
2+
# Build Arca Filesystem Service binary for Linux ARM64
3+
34
set -e
45

5-
cd "$(dirname "$0")"
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
cd "$SCRIPT_DIR"
8+
9+
echo "Building arca-filesystem-service for Linux ARM64..."
10+
11+
# Cross-compile for Linux ARM64
12+
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build \
13+
-o arca-filesystem-service \
14+
-ldflags="-s -w" \
15+
./cmd/arca-filesystem-service
616

7-
echo "Building arca-filesystem-service..."
8-
go build -o arca-filesystem-service ./cmd/arca-filesystem-service
17+
if [ ! -f arca-filesystem-service ]; then
18+
echo "ERROR: Build failed - arca-filesystem-service binary not created"
19+
exit 1
20+
fi
921

10-
echo "✓ Build complete: arca-filesystem-service"
11-
ls -lh arca-filesystem-service
22+
echo "✓ Built arca-filesystem-service ($(du -h arca-filesystem-service | awk '{print $1}')"

vminitd/extensions/filesystem-service/filesystem.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ import (
1313
"context"
1414
"fmt"
1515
"io"
16+
"io/ioutil"
1617
"log"
1718
"os"
1819
"path/filepath"
20+
"runtime"
21+
"strconv"
1922
"strings"
2023
"syscall"
2124
"time"
@@ -29,6 +32,82 @@ type Server struct {
2932
pb.UnimplementedFilesystemServiceServer
3033
}
3134

35+
// findContainerPID finds the PID of a container by searching /proc for ARCA_CONTAINER_ID
36+
func findContainerPID(containerID string) (int, error) {
37+
entries, err := ioutil.ReadDir("/proc")
38+
if err != nil {
39+
return 0, fmt.Errorf("failed to read /proc: %w", err)
40+
}
41+
42+
for _, entry := range entries {
43+
if !entry.IsDir() {
44+
continue
45+
}
46+
47+
// Check if directory name is numeric (PID)
48+
pid, err := strconv.Atoi(entry.Name())
49+
if err != nil {
50+
continue
51+
}
52+
53+
// Read /proc/[pid]/environ to find ARCA_CONTAINER_ID
54+
environPath := filepath.Join("/proc", entry.Name(), "environ")
55+
environData, err := ioutil.ReadFile(environPath)
56+
if err != nil {
57+
continue
58+
}
59+
60+
// environ is null-separated key=value pairs
61+
for _, env := range bytes.Split(environData, []byte{0}) {
62+
if bytes.HasPrefix(env, []byte("ARCA_CONTAINER_ID=")) {
63+
foundID := string(bytes.TrimPrefix(env, []byte("ARCA_CONTAINER_ID=")))
64+
if foundID == containerID {
65+
return pid, nil
66+
}
67+
}
68+
}
69+
}
70+
71+
return 0, fmt.Errorf("container PID not found for ID: %s", containerID)
72+
}
73+
74+
// withMountNamespace executes a function inside a mount namespace
75+
// Pattern similar to WireGuard's netns switching but for mount namespaces
76+
func withMountNamespace(nsPath string, fn func() error) error {
77+
// Lock goroutine to OS thread for namespace operations
78+
runtime.LockOSThread()
79+
defer runtime.UnlockOSThread()
80+
81+
// Open current mount namespace to return to it later
82+
rootMntFd, err := unix.Open("/proc/self/ns/mnt", unix.O_RDONLY, 0)
83+
if err != nil {
84+
return fmt.Errorf("failed to open root mount namespace: %w", err)
85+
}
86+
defer unix.Close(rootMntFd)
87+
88+
// Open target mount namespace
89+
targetMntFd, err := unix.Open(nsPath, unix.O_RDONLY, 0)
90+
if err != nil {
91+
return fmt.Errorf("failed to open target mount namespace: %w", err)
92+
}
93+
defer unix.Close(targetMntFd)
94+
95+
// Enter target mount namespace
96+
if err := unix.Setns(targetMntFd, unix.CLONE_NEWNS); err != nil {
97+
return fmt.Errorf("failed to enter mount namespace: %w", err)
98+
}
99+
100+
// Ensure we return to root namespace
101+
defer func() {
102+
if err := unix.Setns(rootMntFd, unix.CLONE_NEWNS); err != nil {
103+
log.Printf("Warning: failed to return to root mount namespace: %v", err)
104+
}
105+
}()
106+
107+
// Execute the function inside the namespace
108+
return fn()
109+
}
110+
32111
// SyncFilesystem flushes all filesystem buffers to disk
33112
// Calls the sync() syscall to ensure all cached writes are persisted
34113
func (s *Server) SyncFilesystem(ctx context.Context, req *pb.SyncFilesystemRequest) (*pb.SyncFilesystemResponse, error) {
@@ -357,3 +436,122 @@ func (s *Server) WriteArchive(ctx context.Context, req *pb.WriteArchiveRequest)
357436
Success: true,
358437
}, nil
359438
}
439+
440+
// CreateBindMount creates a bind mount from source to target
441+
// Works like "mount --bind /source /target" inside the container
442+
// Used for file bind mounts (VirtioFS only supports directory shares)
443+
func (s *Server) CreateBindMount(ctx context.Context, req *pb.CreateBindMountRequest) (*pb.CreateBindMountResponse, error) {
444+
log.Printf("CreateBindMount: containerID=%s source=%s target=%s readOnly=%v", req.ContainerId, req.Source, req.Target, req.ReadOnly)
445+
446+
// Find container PID by searching /proc for ARCA_CONTAINER_ID environment variable
447+
containerPID, err := findContainerPID(req.ContainerId)
448+
if err != nil {
449+
errMsg := fmt.Sprintf("failed to find container PID: %v", err)
450+
log.Printf("ERROR: %s", errMsg)
451+
return &pb.CreateBindMountResponse{
452+
Success: false,
453+
Error: errMsg,
454+
}, nil
455+
}
456+
log.Printf("✓ Found container PID: %d", containerPID)
457+
458+
// Resolve target path to absolute VM path
459+
// Target is container-relative (e.g., "/test.txt"), resolve to VM path (e.g., "/run/container/{id}/rootfs/test.txt")
460+
targetAbsolute := fmt.Sprintf("/run/container/%s/rootfs%s", req.ContainerId, req.Target)
461+
log.Printf("Resolved target path: %s -> %s", req.Target, targetAbsolute)
462+
463+
// All bind mount operations must happen inside the container's mount namespace
464+
// The source file is mounted via VirtioFS inside the container's mount namespace
465+
// The target file must be created inside the container's mount namespace
466+
mntNsPath := fmt.Sprintf("/proc/%d/ns/mnt", containerPID)
467+
log.Printf("Will perform all operations in container mount namespace: %s", mntNsPath)
468+
469+
// Perform all operations inside the container's mount namespace
470+
err = withMountNamespace(mntNsPath, func() error {
471+
log.Printf("Inside container mount namespace")
472+
473+
// Validate source exists
474+
sourceInfo, err := os.Stat(req.Source)
475+
if err != nil {
476+
log.Printf("ERROR: source path does not exist: %v", err)
477+
return fmt.Errorf("source path does not exist: %w", err)
478+
}
479+
log.Printf("✓ Source exists: %s (isDir=%v, size=%d)", req.Source, sourceInfo.IsDir(), sourceInfo.Size())
480+
481+
// Ensure parent directory of target exists
482+
targetParent := filepath.Dir(targetAbsolute)
483+
if err := os.MkdirAll(targetParent, 0755); err != nil {
484+
log.Printf("ERROR: failed to create target parent directory: %v", err)
485+
return fmt.Errorf("failed to create target parent directory: %w", err)
486+
}
487+
log.Printf("✓ Target parent directory exists: %s", targetParent)
488+
489+
// Check if target exists
490+
targetInfo, err := os.Stat(targetAbsolute)
491+
if err != nil {
492+
if os.IsNotExist(err) {
493+
// Create target matching source type
494+
if sourceInfo.IsDir() {
495+
log.Printf("Creating target directory: %s", targetAbsolute)
496+
if err := os.Mkdir(targetAbsolute, 0755); err != nil {
497+
log.Printf("ERROR: failed to create target directory: %v", err)
498+
return fmt.Errorf("failed to create target directory: %w", err)
499+
}
500+
log.Printf("✓ Target directory created")
501+
} else {
502+
// Create empty file
503+
log.Printf("Creating target file: %s", targetAbsolute)
504+
f, err := os.Create(targetAbsolute)
505+
if err != nil {
506+
log.Printf("ERROR: failed to create target file: %v", err)
507+
return fmt.Errorf("failed to create target file: %w", err)
508+
}
509+
f.Close()
510+
log.Printf("✓ Target file created")
511+
}
512+
} else {
513+
log.Printf("ERROR: failed to stat target: %v", err)
514+
return fmt.Errorf("failed to stat target: %w", err)
515+
}
516+
} else {
517+
log.Printf("✓ Target already exists: isDir=%v", targetInfo.IsDir())
518+
// Target exists - verify types match
519+
if sourceInfo.IsDir() != targetInfo.IsDir() {
520+
log.Printf("ERROR: source and target type mismatch")
521+
return fmt.Errorf("source and target must both be files or both be directories")
522+
}
523+
}
524+
525+
// Perform bind mount using syscall (we're already in the namespace)
526+
log.Printf("Performing bind mount: %s -> %s", req.Source, targetAbsolute)
527+
if err := unix.Mount(req.Source, targetAbsolute, "", unix.MS_BIND, ""); err != nil {
528+
log.Printf("ERROR: failed to bind mount: %v", err)
529+
return fmt.Errorf("failed to bind mount: %w", err)
530+
}
531+
log.Printf("✓ Bind mount successful")
532+
533+
// If read-only, remount with read-only flag
534+
if req.ReadOnly {
535+
log.Printf("Remounting as read-only...")
536+
if err := unix.Mount("", targetAbsolute, "", unix.MS_BIND|unix.MS_REMOUNT|unix.MS_RDONLY, ""); err != nil {
537+
log.Printf("ERROR: failed to remount as read-only: %v", err)
538+
return fmt.Errorf("failed to remount as read-only: %w", err)
539+
}
540+
log.Printf("✓ Remounted as read-only")
541+
}
542+
543+
log.Printf("Bind mount created successfully: %s -> %s (container path: %s)", req.Source, targetAbsolute, req.Target)
544+
return nil
545+
})
546+
547+
if err != nil {
548+
return &pb.CreateBindMountResponse{
549+
Success: false,
550+
Error: err.Error(),
551+
}, nil
552+
}
553+
554+
return &pb.CreateBindMountResponse{
555+
Success: true,
556+
}, nil
557+
}

0 commit comments

Comments
 (0)