| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "io" |
| 9 | "os" |
| 10 | "reflect" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | // This observation is not filesystem or transport authority. The eventual |
| 18 | // transaction must retain Record and commit separate current-boot identities. |
| 19 | type servingParkRebootRecord struct { |
| 20 | Topology servingParkRebootObservation |
| 21 | Record servingParkRecord |
| 22 | Source, Event servingArtifactDirectory |
| 23 | } |
| 24 | |
| 25 | // The caller holds namespace.lock. Repeating the whole inspection rejects a |
| 26 | // source/name change during handle reads and a concurrent private-record change. |
| 27 | func (f *servingParkFS) inspectRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { |
| 28 | var zero servingParkRebootRecord |
| 29 | first, err := f.observeRebootRecord(ctx, id) |
| 30 | if err != nil { |
| 31 | return zero, err |
| 32 | } |
| 33 | again, err := f.observeRebootRecord(ctx, id) |
| 34 | if err != nil { |
| 35 | return zero, err |
| 36 | } |
| 37 | if !reflect.DeepEqual(first, again) { |
| 38 | return zero, hostbox.ErrChanged |
| 39 | } |
| 40 | if err := ctx.Err(); err != nil { |
| 41 | return zero, err |
| 42 | } |
| 43 | return again, nil |
| 44 | } |
| 45 | |
| 46 | func (f *servingParkFS) observeRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { |
| 47 | var out servingParkRebootRecord |
| 48 | topology, err := f.inspectRebootTopology(ctx) |
| 49 | if err != nil { |
| 50 | return out, err |
| 51 | } |
| 52 | r, err := (&servingParkLedger{state: f.state}).readHistorical(id) |
| 53 | if err != nil { |
| 54 | return out, err |
| 55 | } |
| 56 | n := topology.Previous |
| 57 | // A later retirement binds its parent to namespace history with root-private |
| 58 | // persistent evidence. Device/mount numbers constrain only the same boot. |
| 59 | if (r.Source.BootID != n.BootID || r.Source.Parent != n.Parent) && (!r.Source.ParentPersistent.valid() || r.Source.ParentPersistent != n.Topology.Parent) { |
| 60 | return out, hostbox.ErrChanged |
| 61 | } |
| 62 | if !validServingParkBootID(r.Source.BootID) || (r.Source.BootID == topology.BootID && r.Source.Parent.Device != topology.CurrentParent.Device) || !r.Source.Persistent.valid() || r.Source.Parent.Inode != n.Parent.Inode || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && (r.Namespace.Inode != n.Root.Inode || r.Namespace.Mount != r.Source.Parent.Mount) { |
| 63 | return out, hostbox.ErrChanged |
| 64 | } |
| 65 | if r.Source.BootID == topology.BootID && r.Source.Parent.Mount == topology.CurrentParent.Mount { |
| 66 | prior, err := servingParkReadBinding(f.state, "retirement-"+id) |
| 67 | if err != nil || prior == nil || prior.BeforeBoot != r.Source.BootID || prior.BeforeParent != r.Source.Parent { |
| 68 | return out, hostbox.ErrChanged |
| 69 | } |
| 70 | } |
| 71 | parent, pid, err := f.parent() |
| 72 | if err != nil { |
| 73 | return out, err |
| 74 | } |
| 75 | defer parent.Close() |
| 76 | root, rid, err := f.privateDir(parent, ".serving-parked") |
| 77 | if err != nil { |
| 78 | return out, err |
| 79 | } |
| 80 | defer root.Close() |
| 81 | if pid != topology.CurrentParent || rid != topology.CurrentRoot { |
| 82 | return out, hostbox.ErrChanged |
| 83 | } |
| 84 | current := servingParkNamespace{Version: 1, Parent: pid, Root: rid, BootID: topology.BootID} |
| 85 | handles, err := f.captureTopology(parent, root, current) |
| 86 | if err != nil { |
| 87 | return out, err |
| 88 | } |
| 89 | if *handles != *n.Topology { |
| 90 | return out, hostbox.ErrChanged |
| 91 | } |
| 92 | var source *os.File |
| 93 | // Completed parks are independent of the live installation, which may have |
| 94 | // restored a successor or been removed. Incomplete parks still require it. |
| 95 | if !r.Complete { |
| 96 | source, err = servingParkOpenDir(parent, provision.InstanceID(r.Authorization.Successor.Host)) |
| 97 | if err != nil { |
| 98 | return out, err |
| 99 | } |
| 100 | defer source.Close() |
| 101 | out.Source, err = f.rebootDirectory(source, r.Source.Directory, r.Source.Persistent, pid) |
| 102 | if err != nil { |
| 103 | return out, err |
| 104 | } |
| 105 | } |
| 106 | event, eid, err := f.privateDir(root, r.Authorization.ID) |
| 107 | if errors.Is(err, unix.ENOENT) { |
| 108 | if r.Prepared && !r.EventRemoveIntent { |
| 109 | return out, hostbox.ErrChanged |
| 110 | } |
| 111 | } else if err != nil { |
| 112 | return out, err |
| 113 | } else { |
| 114 | defer event.Close() |
| 115 | if !r.Prepared || r.EventRemoved { |
| 116 | return out, hostbox.ErrChanged |
| 117 | } |
| 118 | out.Event, err = f.rebootDirectory(event, r.Event, r.EventPersistent, pid) |
| 119 | if err != nil || out.Event != eid { |
| 120 | return out, hostbox.ErrChanged |
| 121 | } |
| 122 | // Only four fixed artifact names can be evidence. Unknown children remain |
| 123 | // untouched and block this event, including after an interrupted unlink. |
| 124 | names, e := event.Readdirnames(5) |
| 125 | if len(names) > 4 || e != nil && !errors.Is(e, io.EOF) { |
| 126 | return out, hostbox.ErrChanged |
| 127 | } |
| 128 | for _, name := range names { |
| 129 | known := false |
| 130 | for _, item := range r.Source.Files { |
| 131 | if item.Present && !item.Deleted && item.Name == name { |
| 132 | known = true |
| 133 | } |
| 134 | } |
| 135 | if !known { |
| 136 | return out, hostbox.ErrChanged |
| 137 | } |
| 138 | } |
| 139 | } |
| 140 | for _, item := range r.Source.Files { |
| 141 | src := servingParkFile{Name: item.Name} |
| 142 | dst := servingParkFile{Name: item.Name} |
| 143 | if source != nil { |
| 144 | src, err = servingParkStat(source, item.Name) |
| 145 | if err != nil { |
| 146 | return out, err |
| 147 | } |
| 148 | } |
| 149 | if event != nil { |
| 150 | dst, err = servingParkStat(event, item.Name) |
| 151 | if err != nil { |
| 152 | return out, err |
| 153 | } |
| 154 | } |
| 155 | if !servingParkRebootFileMatches(item, src, dst, pid, r) { |
| 156 | return out, hostbox.ErrChanged |
| 157 | } |
| 158 | } |
| 159 | if err := f.checkBoot(topology.BootID); err != nil { |
| 160 | return out, err |
| 161 | } |
| 162 | if err := ctx.Err(); err != nil { |
| 163 | return out, err |
| 164 | } |
| 165 | out.Topology, out.Record = topology, r |
| 166 | return out, nil |
| 167 | } |
| 168 | |
| 169 | func (f *servingParkFS) rebootDirectory(d *os.File, old servingArtifactDirectory, proof servingParkPersistentDirectory, parent servingArtifactDirectory) (servingArtifactDirectory, error) { |
| 170 | var zero servingArtifactDirectory |
| 171 | id, err := servingParkDirID(d) |
| 172 | if err != nil { |
| 173 | return zero, err |
| 174 | } |
| 175 | if !proof.valid() || id.Device != parent.Device || id.Inode != old.Inode || id.Mount != parent.Mount { |
| 176 | return zero, hostbox.ErrChanged |
| 177 | } |
| 178 | actual, err := f.handle(d) |
| 179 | if err != nil { |
| 180 | return zero, err |
| 181 | } |
| 182 | if actual != proof { |
| 183 | return zero, hostbox.ErrChanged |
| 184 | } |
| 185 | return id, nil |
| 186 | } |
| 187 | |
| 188 | func servingParkRebootFileMatches(old, src, dst servingParkFile, parent servingArtifactDirectory, r servingParkRecord) bool { |
| 189 | if !old.Present { |
| 190 | return !src.Present && !dst.Present |
| 191 | } |
| 192 | if !old.Persistent.valid() { |
| 193 | return false |
| 194 | } |
| 195 | expected := old |
| 196 | expected.Device, expected.Mount = parent.Device, parent.Mount |
| 197 | if old.Deleted || r.EventRemoved { |
| 198 | return !src.Present && !dst.Present |
| 199 | } |
| 200 | if r.Deleting { |
| 201 | return !src.Present && (sameServingParkFile(expected, dst) || !dst.Present && old.DeleteIntent) |
| 202 | } |
| 203 | if r.Parked || old.Moved { |
| 204 | return !src.Present && sameServingParkFile(expected, dst) |
| 205 | } |
| 206 | // A rename can be durable before its progress write. Accept exactly one |
| 207 | // recorded inode, with its handle; a second link or a replacement refuses. |
| 208 | return sameServingParkFile(expected, src) && !dst.Present || !src.Present && sameServingParkFile(expected, dst) |
| 209 | } |
| 210 | |