aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-06-21 01:51:24 +0900
committerOphestra <cat@gensokyo.uk>2026-06-21 02:09:01 +0900
commit218f7fa34565a8cfdf93824cd6d69f04b3cd4141 (patch)
tree884295afcfb9613a92b7800efd44aca7e578d0ee
parentcd493fd95f78b023c159c10571a0de8d701c4b27 (diff)
cmd/app: enforce mutable instance exclusion
This avoids invoking undefined behaviour in the underlying overlay filesystem implementation. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--cmd/app/app.go10
-rw-r--r--cmd/app/app_test.go1
-rw-r--r--cmd/app/lock.go111
-rw-r--r--cmd/app/main.go13
4 files changed, 132 insertions, 3 deletions
diff --git a/cmd/app/app.go b/cmd/app/app.go
index 9fa49696..a8508676 100644
--- a/cmd/app/app.go
+++ b/cmd/app/app.go
@@ -31,7 +31,12 @@ func parsePair(s string) (source, target *check.Absolute, err error) {
// parse decodes a high-level configuration stream and returns its
// corresponding [hst.Config].
-func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
+func parse(
+ id string,
+ base *check.Absolute,
+ r io.Reader,
+ templateP *string,
+) (*hst.Config, error) {
shell := fhs.AbsRoot.Append("bin", "zsh")
home := hst.AbsPrivateTmp.Append("home")
@@ -99,6 +104,9 @@ func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
} else if v, err := strconv.Atoi(identity); err != nil {
return nil, err
} else {
+ if templateP != nil {
+ *templateP = template
+ }
c.Identity = v
root.Upper = base.Append("template", template)
}
diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go
index f4811801..1aab4759 100644
--- a/cmd/app/app_test.go
+++ b/cmd/app/app_test.go
@@ -130,6 +130,7 @@ talk com.canonical.Unity
tc.name,
base,
strings.NewReader(tc.data),
+ nil,
)
if !reflect.DeepEqual(err, tc.err) {
diff --git a/cmd/app/lock.go b/cmd/app/lock.go
new file mode 100644
index 00000000..d43ede93
--- /dev/null
+++ b/cmd/app/lock.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+ "errors"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+
+ "hakurei.app/check"
+ "hakurei.app/fhs"
+ "hakurei.app/hst"
+ "hakurei.app/internal/env"
+ "hakurei.app/internal/lockedfile"
+ "hakurei.app/internal/outcome"
+)
+
+// MutationConflictError describes an active mutable instance.
+type MutationConflictError string
+
+func (e MutationConflictError) Error() string {
+ return "mutable instance active at " + string(e)
+}
+
+// informTemplate guards intention of a template or its derivatives.
+func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) {
+ mu := lockedfile.MutexAt(base.Append("lock", name).String())
+ if unlock, err := mu.Lock(); err != nil {
+ return nil, err
+ } else {
+ defer unlock()
+ }
+
+ marker := base.Append("lock", "."+name)
+ if p, err := os.ReadFile(marker.String()); err == nil {
+ if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil {
+ return nil, MutationConflictError(p)
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return nil, err
+ }
+ log.Printf("removing stale marker by %s", string(p))
+ if err = os.Remove(marker.String()); err != nil {
+ return nil, err
+ }
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return nil, err
+ }
+
+ if !mutable {
+ return nil, nil
+ }
+
+ var active []hst.ID
+ var sc hst.Paths
+ env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
+ entries, copyError := outcome.NewStore(&sc).All()
+ var s hst.State
+ for eh := range entries {
+ if _, err := eh.Load(&s); err != nil {
+ return nil, err
+ }
+
+ if s.Validate(0) != nil || len(s.Container.Filesystem) < 1 {
+ continue
+ }
+ root, ok := s.Container.Filesystem[0].FilesystemConfig.(*hst.FSOverlay)
+ if !ok || root == nil {
+ continue
+ }
+
+ if !root.Target.Is(fhs.AbsRoot) ||
+ len(root.Lower) != 1 ||
+ !root.Lower[0].Is(base.Append("initial")) ||
+ !root.Upper.Is(base.Append("template", name)) ||
+ root.Work != nil {
+ continue
+ }
+
+ active = append(active, s.ID)
+ }
+ if err := copyError(); err != nil {
+ return nil, err
+ }
+
+ if len(active) != 0 {
+ var buf strings.Builder
+ buf.WriteString("derivative instances still active:")
+ for _, id := range active {
+ buf.WriteString("\n\t")
+ buf.WriteString(id.String())
+ }
+ return nil, errors.New(buf.String())
+ }
+
+ return func() error { return os.RemoveAll(marker.String()) }, os.WriteFile(
+ marker.String(),
+ []byte(strconv.Itoa(os.Getpid())),
+ 0400,
+ )
+}
+
+// acquireTemplate obtains exclusivity of a template.
+func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) {
+ return informTemplate(base, name, true)
+}
+
+// enterTemplate checks against exclusivity of a template.
+func enterTemplate(base *check.Absolute, name string) error {
+ _, err := informTemplate(base, name, false)
+ return err
+}
diff --git a/cmd/app/main.go b/cmd/app/main.go
index c9331e39..095863a8 100644
--- a/cmd/app/main.go
+++ b/cmd/app/main.go
@@ -128,7 +128,12 @@ func main() {
config.Container.Home = a
}
- return run(ctx, msg, &config)
+ remove, err := acquireTemplate(base, args[0])
+ if err != nil {
+ return err
+ }
+ err = run(ctx, msg, &config)
+ return errors.Join(err, remove())
},
).Flag(
&flagShell,
@@ -180,7 +185,8 @@ func main() {
r = io.MultiReader(f, common)
}
- config, err = parse(args[0], base, r)
+ var name string
+ config, err = parse(args[0], base, r, &name)
if closeErr := f.Close(); err == nil {
err = closeErr
}
@@ -197,6 +203,9 @@ func main() {
config.Container.Args[2] = flagCommand
}
+ if err = enterTemplate(base, name); err != nil {
+ return err
+ }
return run(ctx, msg, config, args[1:]...)
},
).