first commit
This commit is contained in:
commit
c1eb7b0e94
8 changed files with 2304 additions and 0 deletions
272
chroot.go
Normal file
272
chroot.go
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
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])
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue