Files
aego/app/export/seal.go
sanspie 2f768d4166
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci / checks (push) Successful in 7m12s
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m42s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
fix: preserve native export and updater checks
2026-11-06 21:04:10 +03:00

149 lines
3.5 KiB
Go

package export
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
)
// Agent launches verify all executable, data and web files, not only the manifest.
// A private copy pins the exact bytes while the game or loopback server uses them.
func Seal(dir string) ([32]byte, error) { return walkExport(dir, nil) }
func VerifiedCopy(dir string, want [32]byte) (string, error) {
tmp, err := os.MkdirTemp("", "aego-export-")
if err != nil {
return "", err
}
target, err := os.OpenRoot(tmp)
if err != nil {
os.RemoveAll(tmp)
return "", err
}
got, err := walkExport(dir, target)
target.Close()
if err == nil && got != want {
err = errors.New("export contents changed; export again before launch")
}
if err != nil {
os.RemoveAll(tmp)
return "", err
}
return tmp, nil
}
func walkExport(dir string, dst *os.Root) ([32]byte, error) {
var sum [32]byte
clean, err := filepath.Abs(dir)
if err != nil {
return sum, err
}
info, err := os.Lstat(clean)
if err != nil {
return sum, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return sum, errors.New("export directory was replaced by a symlink or is not a directory")
}
// System paths such as /var on macOS may have symlinked ancestors. Resolve
// them, but pin the final directory and still refuse links inside the export.
actual, err := filepath.EvalSymlinks(clean)
if err != nil {
return sum, err
}
root, err := os.OpenRoot(actual)
if err != nil {
return sum, err
}
defer root.Close()
opened, err := root.Stat(".")
if err != nil {
return sum, err
}
if !os.SameFile(info, opened) {
return sum, errors.New("export directory changed during verification")
}
const maxFiles = 32768
const maxBytes = 1 << 30
var count int
var total int64
h := sha256.New()
buf := make([]byte, 64<<10)
err = fs.WalkDir(root.FS(), ".", func(name string, de fs.DirEntry, err error) error {
if err != nil {
return err
}
if name == "." {
return nil
}
count++
if count > maxFiles {
return errors.New("agent export exceeds file limit")
}
rel := filepath.FromSlash(name)
mode := de.Type()
if mode&os.ModeSymlink != 0 {
return fmt.Errorf("symlink in export: %s", name)
}
if de.IsDir() {
if dst != nil {
return dst.Mkdir(rel, 0700)
}
return nil
}
if !mode.IsRegular() {
return fmt.Errorf("non-regular export file: %s", name)
}
f, err := root.Open(rel)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Size() < 0 || info.Size() > maxBytes-total {
return errors.New("invalid file or export exceeds 1 GiB agent launch limit")
}
total += info.Size()
var header [16]byte
binary.LittleEndian.PutUint32(header[:4], uint32(len(name)))
binary.LittleEndian.PutUint32(header[4:8], uint32(info.Mode().Perm()&0111))
binary.LittleEndian.PutUint64(header[8:], uint64(info.Size()))
h.Write(header[:])
io.WriteString(h, name)
var writer io.Writer = h
var out *os.File
if dst != nil {
out, err = dst.OpenFile(rel, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600|info.Mode().Perm()&0111)
if err != nil {
return err
}
writer = io.MultiWriter(h, out)
}
n, err := io.CopyBuffer(writer, io.LimitReader(f, info.Size()+1), buf)
if out != nil {
if e := out.Close(); err == nil {
err = e
}
}
if err != nil {
return err
}
if n != info.Size() {
return errors.New("export changed during verification")
}
return nil
})
if err == nil {
copy(sum[:], h.Sum(nil))
}
return sum, err
}