fix: preserve snapshot symlinks and guard incomplete upgrade copies
This commit is contained in:
+45
-20
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -100,9 +101,17 @@ func upgradeSync(r *os.Root) error {
|
||||
|
||||
func upgradeCopy(ctx context.Context, r *os.Root, source, target string, limit int64) error {
|
||||
remaining, entries := limit, 100000
|
||||
var links []string
|
||||
var copyTree func(string, string, int) error
|
||||
copyTree = func(src, dst string, depth int) error {
|
||||
copyTree = func(src, dst string, depth int) (copyErr error) {
|
||||
defer func() {
|
||||
if copyErr != nil {
|
||||
if pathErr, ok := copyErr.(*os.PathError); ok {
|
||||
copyErr = fmt.Errorf("snapshot copy: %w", &os.PathError{Op: pathErr.Op, Path: src, Err: pathErr.Err})
|
||||
return
|
||||
}
|
||||
copyErr = fmt.Errorf("%s: %w", src, copyErr)
|
||||
}
|
||||
}()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -155,14 +164,11 @@ func upgradeCopy(ctx context.Context, r *os.Root, source, target string, limit i
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(source, src)
|
||||
if err != nil || filepath.IsAbs(link) || !filepath.IsLocal(filepath.Join(filepath.Dir(rel), link)) {
|
||||
return errors.New("unsafe snapshot link")
|
||||
}
|
||||
// Preserve the link itself, including dangling and container-absolute
|
||||
// targets. Never resolve it against the panel's filesystem.
|
||||
if err = r.Symlink(link, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
links = append(links, rel)
|
||||
return r.Lchown(dst, 1000, 1000)
|
||||
}
|
||||
if !st.Mode().IsRegular() || st.Size() > remaining {
|
||||
@@ -201,17 +207,6 @@ func upgradeCopy(ctx context.Context, r *os.Root, source, target string, limit i
|
||||
if err := copyTree(source, target, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot, err := r.OpenRoot(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer snapshot.Close()
|
||||
// Resolve only within the completed snapshot, including intermediate symlinks.
|
||||
for _, link := range links {
|
||||
if _, err := snapshot.Stat(link); err != nil {
|
||||
return errors.New("unsafe or unresolved snapshot link")
|
||||
}
|
||||
}
|
||||
return upgradeSync(r)
|
||||
}
|
||||
|
||||
@@ -460,8 +455,38 @@ func (a *API) upgrade(w http.ResponseWriter, r *http.Request, i store.Instance)
|
||||
fail(w, 413, "quota exceeded or snapshot size unavailable")
|
||||
return
|
||||
}
|
||||
if upgradeCopy(ctx, root, "home", snapshot, snapshotBytes) != nil {
|
||||
fail(w, 500, "home snapshot failed")
|
||||
// Only publish a completed copy under a name the snapshot API recognizes.
|
||||
staging := "." + snapshot + "-partial"
|
||||
if _, err := a.s.DB.Exec("CREATE TABLE IF NOT EXISTS snapshot_copy_state (instance_id TEXT NOT NULL, id TEXT NOT NULL, ready INTEGER NOT NULL, PRIMARY KEY(instance_id,id))"); err != nil {
|
||||
fail(w, 500, "snapshot completion metadata unavailable")
|
||||
return
|
||||
}
|
||||
if _, err := a.s.DB.Exec("INSERT INTO snapshot_copy_state VALUES(?,?,0)", i.ID, snapshot); err != nil {
|
||||
fail(w, 500, "snapshot completion metadata unavailable")
|
||||
return
|
||||
}
|
||||
if copyErr := upgradeCopy(ctx, root, "home", staging, snapshotBytes); copyErr != nil {
|
||||
if cleanupErr := root.RemoveAll(staging); cleanupErr != nil {
|
||||
a.s.AddAudit(i.UID, "upgrade.snapshot.cleanup_failed", i.ID, staging)
|
||||
fail(w, 500, "home snapshot failed: "+copyErr.Error()+"; partial snapshot cleanup required")
|
||||
return
|
||||
}
|
||||
fail(w, 500, "home snapshot failed: "+copyErr.Error())
|
||||
return
|
||||
}
|
||||
if err := root.Rename(staging, snapshot); err != nil {
|
||||
if root.RemoveAll(staging) != nil {
|
||||
a.s.AddAudit(i.UID, "upgrade.snapshot.cleanup_failed", i.ID, staging)
|
||||
}
|
||||
fail(w, 500, "home snapshot publication failed")
|
||||
return
|
||||
}
|
||||
if upgradeSync(root) != nil {
|
||||
fail(w, 500, "home snapshot publication sync failed")
|
||||
return
|
||||
}
|
||||
if _, err := a.s.DB.Exec("UPDATE snapshot_copy_state SET ready=1 WHERE instance_id=? AND id=?", i.ID, snapshot); err != nil {
|
||||
fail(w, 500, "snapshot completion metadata unavailable")
|
||||
return
|
||||
}
|
||||
snapReady = true
|
||||
|
||||
Reference in New Issue
Block a user