| index 74d2d4d1a..3fb553378 100644 |
| --- a/internal/release/ratchet.go |
| +++ b/internal/release/ratchet.go |
| @@ -3,12 +3,14 @@ package release |
| import ( |
| "errors" |
| "fmt" |
| + "io/fs" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| "sync/atomic" |
| + "syscall" |
| "time" |
| ) |
| |
| @@ -37,7 +39,9 @@ func RequirePointerDigest(getenv func(string) string) bool { |
| |
| // RootRatchetDir is root's marks (/opt/carlos/pointer-digests), and |
| // EdgeRatchetDir the sandboxed agent's and edge's, which cannot write /opt |
| -// (/data/pointer-digests). Each process writes its own and reads both. |
| +// (/data/pointer-digests). Each process writes its own and reads both. A mark |
| +// is one flat file per channel, <dir>/<acct>/<app>/<channel>.digests, with |
| +// every "/" in the channel written as "%2F" (see markPath). |
| func RootRatchetDir(optDir string) string { return filepath.Join(optDir, "pointer-digests") } |
| func EdgeRatchetDir(dataDir string) string { return filepath.Join(dataDir, "pointer-digests") } |
| |
| @@ -65,10 +69,13 @@ func NewDigestRatchet(own string, also []string, require bool) *DigestRatchet { |
| return r |
| } |
| |
| -// markPath is a channel's mark. The ".digests" suffix lets "canary" and |
| -// "canary/x" both be marked: the second needs a directory named canary. |
| +// markPath is a channel's mark: <dir>/<acct>/<app>/<channel>.digests, one |
| +// flat file, with every "/" in the channel escaped as "%2F". A channel name |
| +// never contains "%", so the encoding is collision-free: "canary" and |
| +// "canary/x", or "foo" and "foo.digests/bar", each get a file of their own |
| +// and never need the other's path to be a directory (Astra r1 #5). |
| func markPath(dir, acct, app, channel string) string { |
| - return filepath.Join(dir, acct, app, filepath.FromSlash(channel)+".digests") |
| + return filepath.Join(dir, acct, app, strings.ReplaceAll(channel, "/", "%2F")+".digests") |
| } |
| |
| // Admit is the ratchet's verdict on a verified pointer. A nil ratchet admits |
| @@ -77,7 +84,9 @@ func markPath(dir, acct, app, channel string) string { |
| // process refuses a digestless replay from that moment, and on disk before |
| // the pointer is admitted. A mark that cannot be persisted refuses the |
| // pointer (ErrRatchetUnpersisted): installing or extracting on it would leave |
| -// the box unprotected after a restart. |
| +// the box unprotected after a restart. A mark that cannot be read (anything |
| +// but a confirmed absence) refuses a pointer without digests: only a box that |
| +// knows it has never seen digests for the channel admits one (Astra r1 #1). |
| func (r *DigestRatchet) Admit(acct string, p *ChannelPointer) error { |
| if r == nil || p == nil { |
| return nil |
| @@ -88,30 +97,55 @@ func (r *DigestRatchet) Admit(acct string, p *ChannelPointer) error { |
| if r.require.Load() { |
| return fmt.Errorf("%w: %s/%s/%s, and %s is on", ErrPointerWithoutDigests, acct, p.App, p.Channel, RequirePointerDigestEnv) |
| } |
| - if r.marked(acct, p.App, p.Channel) { |
| + marked, err := r.marked(acct, p.App, p.Channel) |
| + if err != nil { |
| + return fmt.Errorf("%w: %s/%s/%s, and this box cannot tell whether it has already verified one with digests for that channel: %w", |
| + ErrPointerWithoutDigests, acct, p.App, p.Channel, err) |
| + } |
| + if marked { |
| return fmt.Errorf("%w: %s/%s/%s, and this box has already verified one with digests for that channel", |
| ErrPointerWithoutDigests, acct, p.App, p.Channel) |
| } |
| return nil |
| } |
| |
| -func (r *DigestRatchet) marked(acct, app, channel string) bool { |
| +// marked reports whether any of the ratchet's directories holds the channel's |
| +// mark. Only a confirmed absence (ENOENT, or ENOTDIR on a path component) |
| +// counts as "not marked"; a mark must be a regular file, and any other answer |
| +// (EACCES, EIO, a directory where the mark belongs) is an error, which the |
| +// caller treats as fail-closed. A mark found in any directory wins over an |
| +// error in another, since both refuse. |
| +func (r *DigestRatchet) marked(acct, app, channel string) (bool, error) { |
| k := acct + "|" + app + "|" + channel |
| r.mu.Lock() |
| defer r.mu.Unlock() |
| if r.seen[k] { |
| - return true |
| + return true, nil |
| } |
| + var firstErr error |
| for _, dir := range append([]string{r.own}, r.also...) { |
| if dir == "" { |
| continue |
| } |
| - if _, err := os.Stat(markPath(dir, acct, app, channel)); err == nil { |
| + path := markPath(dir, acct, app, channel) |
| + fi, err := os.Stat(path) |
| + switch { |
| + case err == nil && fi.Mode().IsRegular(): |
| r.seen[k] = true |
| - return true |
| + return true, nil |
| + case err == nil: |
| + if firstErr == nil { |
| + firstErr = fmt.Errorf("pointer digest mark %s is not a regular file", path) |
| + } |
| + case errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR): |
| + // confirmed absent |
| + default: |
| + if firstErr == nil { |
| + firstErr = err |
| + } |
| } |
| } |
| - return false |
| + return false, firstErr |
| } |
| |
| func (r *DigestRatchet) note(acct, app, channel string) error { |
| @@ -123,7 +157,7 @@ func (r *DigestRatchet) note(acct, app, channel string) error { |
| return nil |
| } |
| path := markPath(r.own, acct, app, channel) |
| - if _, err := os.Stat(path); err == nil { |
| + if fi, err := os.Stat(path); err == nil && fi.Mode().IsRegular() { |
| return nil |
| } |
| if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |