vlcro/chroot.go
2026-09-06 13:24:35 -03:00

272 lines
6.9 KiB
Go

package main
import (
"crypto/rand"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"time"
)
// mount tracker
type MountTracker struct {
mounts []string
mu sync.Mutex
}
func (mt *MountTracker) Add(path string) {
mt.mu.Lock()
defer mt.mu.Unlock()
mt.mounts = append(mt.mounts, path)
}
func (mt *MountTracker) GetAll() []string {
mt.mu.Lock()
defer mt.mu.Unlock()
result := make([]string, len(mt.mounts))
copy(result, mt.mounts)
sort.SliceStable(result, func(i, j int) bool {
return len(result[i]) > len(result[j])
})
return result
}
func (mt *MountTracker) Reset() {
mt.mu.Lock()
defer mt.mu.Unlock()
mt.mounts = nil
}
// mount helpers
func findActiveMountCount(path string) int {
if path == "" {
return 0
}
cmd := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", path)
out, err := cmd.Output()
if err != nil {
return 0
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) == 1 && lines[0] == "" {
return 0
}
return len(lines)
}
func hasActiveMounts(path string) bool {
return findActiveMountCount(path) > 0
}
func retryMount(source, target, fstype string, flags uintptr, data string, maxRetries int) error {
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
time.Sleep(retryDelay)
}
err := syscall.Mount(source, target, fstype, flags, data)
if err == nil {
debugf("Mounted %s -> %s", source, target)
return nil
}
lastErr = err
debugf("Mount attempt %d failed for %s: %v", attempt+1, target, err)
}
return fmt.Errorf("mount %s failed after %d attempts: %w", target, maxRetries, lastErr)
}
func retryUnmount(target string, flags int, maxRetries int) error {
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
time.Sleep(retryDelay)
}
err := syscall.Unmount(target, flags)
if err == nil || err == syscall.EINVAL || err == syscall.ENOENT {
return nil
}
lastErr = err
debugf("Unmount attempt %d failed for %s: %v", attempt+1, target, err)
}
return fmt.Errorf("unmount %s failed after %d attempts: %w", target, maxRetries, lastErr)
}
// chroot setup
func setupChroot(tmpDir, uuid string, needDev bool, tracker *MountTracker) error {
base := filepath.Join(tmpDir, uuid)
rootfs := filepath.Join(base, "rootfs")
dirs := []string{
rootfs,
filepath.Join(base, "run"),
filepath.Join(base, "var", "cache", "zypp"),
filepath.Join(base, "var", "lib", "ca-certificates"),
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", d, err)
}
}
resolvPath := "/etc/resolv.conf"
if linkTarget, err := os.Readlink(resolvPath); err == nil {
if !filepath.IsAbs(linkTarget) {
linkTarget = filepath.Join("/etc", linkTarget)
}
target := filepath.Join(base, linkTarget)
os.MkdirAll(filepath.Dir(target), 0755)
if data, err := os.ReadFile(linkTarget); err == nil {
os.WriteFile(target, data, 0644)
}
} else if data, err := os.ReadFile(resolvPath); err == nil {
target := filepath.Join(base, resolvPath)
os.MkdirAll(filepath.Dir(target), 0755)
os.WriteFile(target, data, 0644)
}
if err := retryMount("/", rootfs, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
return fmt.Errorf("mount rootfs: %w", err)
}
tracker.Add(rootfs)
if needDev {
devPath := filepath.Join(rootfs, "dev")
if err := retryMount("/dev", devPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
return fmt.Errorf("mount /dev bind: %w", err)
}
tracker.Add(devPath)
tmpPath := filepath.Join(rootfs, "tmp")
if err := retryMount("tmpfs", tmpPath, "tmpfs", 0, "", maxRetries); err != nil {
return fmt.Errorf("mount tmpfs: %w", err)
}
tracker.Add(tmpPath)
}
runPath := filepath.Join(rootfs, "run")
if err := retryMount(filepath.Join(base, "run"), runPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
return fmt.Errorf("mount run: %w", err)
}
tracker.Add(runPath)
varPath := filepath.Join(rootfs, "var")
if err := retryMount(filepath.Join(base, "var"), varPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
return fmt.Errorf("mount var: %w", err)
}
tracker.Add(varPath)
cachePath := filepath.Join(rootfs, "var", "cache", "zypp")
if err := retryMount("/var/cache/zypp", cachePath, "", syscall.MS_BIND, "", maxRetries); err != nil {
debugf("Warning: failed to mount cache (non-critical): %v", err)
} else {
tracker.Add(cachePath)
}
certPath := filepath.Join(rootfs, "var", "lib", "ca-certificates")
if err := retryMount("/var/lib/ca-certificates", certPath, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
debugf("Warning: failed to mount ca-certs (non-critical): %v", err)
} else {
tracker.Add(certPath)
}
return nil
}
// unmount
func unmountAll(tmpDir string, tracker *MountTracker) error {
if tmpDir == "" {
return nil
}
mounts := tracker.GetAll()
var errs []string
for _, mountPath := range mounts {
cmd := exec.Command("findmnt", "-n", "-o", "TARGET", mountPath)
if err := cmd.Run(); err != nil {
continue
}
if err := retryUnmount(mountPath, syscall.MNT_DETACH, maxRetries); err != nil {
debugf("Failed to unmount %s: %v", mountPath, err)
if err := syscall.Unmount(mountPath, syscall.MNT_DETACH|syscall.MNT_FORCE); err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", mountPath, err))
}
}
}
if len(errs) > 0 {
warningf("Some unmounts failed: %s", strings.Join(errs, "; "))
}
return nil
}
// cleanup
func vlcroCleanup(tmpDir string, tracker *MountTracker) {
releaseZyppLock()
if tmpDir == "" {
return
}
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
return
}
absPath, err := filepath.Abs(tmpDir)
if err != nil || !strings.HasPrefix(absPath, "/tmp/vlcro_") {
errorf("Refusing to cleanup unsafe temp directory: %s", tmpDir)
return
}
infof("%s", colorText(colorInfo, "Cleaning up temp mounts..."))
unmountAll(tmpDir, tracker)
time.Sleep(500 * time.Millisecond)
for attempt := 0; attempt < 5; attempt++ {
if !hasActiveMounts(tmpDir) {
break
}
warningf("Active mounts still detected under %s, retrying unmount...", tmpDir)
unmountAll(tmpDir, tracker)
time.Sleep(500 * time.Millisecond)
}
if hasActiveMounts(tmpDir) {
warningf("Could not unmount all filesystems under %s. Skipping temp directory removal.", tmpDir)
return
}
infof("%s", colorText(colorInfo, "Cleaning up temp directory..."))
for attempt := 0; attempt < 3; attempt++ {
if err := os.RemoveAll(tmpDir); err == nil {
return
}
time.Sleep(500 * time.Millisecond)
}
warningf("Could not fully remove temp directory %s (will clean up at next boot)", tmpDir)
}
// uuid
func randomUUID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}