C.W.K.
Stream
Lesson 05 of 05 · published

Revalidate at Apply Time

~12 min · revalidation, toctou, apply-time

Level 0Lost in Finder
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"What you saw is a memory. What you're about to change is the present. Never confuse the two."

The Gap Between Seeing and Doing

Every file operation has a gap: the moment the UI showed you the file, and the moment you actually act on it. It might be a second; it might be an hour if a menu sat open. In that gap the world can move — the file can be renamed, replaced, or deleted; the volume can unmount and remount; permissions can change; the destination folder can fill up or vanish; a collision that wasn't there can appear. Security people call this class of bug time-of-check to time-of-use, and it is where careless file tools lose data.

Check Late, Not Early

The fix is discipline about when you validate. Waygate does not trust what was true when it drew the row. At apply time — the instant before the engine touches disk — it re-resolves the reference and confirms the whole precondition set: the path resolves, the resource identity matches, the volume is the same one, permissions still allow it, the source metadata is unchanged, the destination is ready, and the collision state is what the plan assumed. Any mismatch stops the operation before a single byte moves.

Why This Ties the Track Together

This is the payoff of everything in this track. The filesystem is canonical (so revalidation reads the truth). A path isn't identity (so we check identity, not just existence). Refresh re-enumerates (so the view is honest). The database is a notebook (so there's no stale catalog to trust). Revalidate-at-apply is the moment all four rules get spent: they exist so that the last check before a mutation can be made against reality, not memory.

Revalidate the live world at apply time, never trust display time. Path, resource identity, volume, permissions, source metadata, destination, and collision state can all change after a row is drawn. The final gate before any mutation compares the plan's assumptions against the filesystem as it is right now — and stops on any mismatch.
I used to think validation was something you did up front — check the inputs, then run. Building Waygate taught me the opposite: for anything that touches shared state, the ONLY validation that counts is the one right before you act. Everything earlier is a courtesy to the user (grey out the button), but the real gate is late and unglamorous, and it's the one that saves the file.

Code

The apply-time gate: everything the plan assumed, re-checked now·swift
func gateBeforeMutation(_ plan: OperationPlan) throws {
    let src = try revalidate(plan.source)            // still the same file?
    let srcVals = try src.resourceValues(forKeys: [
        .contentModificationDateKey, .fileSizeKey])
    guard srcVals matches plan.expectedSourceMetadata
        else { throw OpError.sourceChanged }         // renamed/edited under us
    guard try volumeStillMounted(plan.source.volumeID)
        else { throw OpError.volumeGone }            // SMB dropped, USB pulled
    guard try destinationReady(plan.destination)
        else { throw OpError.destinationUnavailable }
    let collisionNow = try detectCollision(plan.destination)
    guard collisionNow == plan.assumedCollision
        else { throw OpError.collisionChanged }      // occupant appeared/changed
    // Only past ALL of these does the engine touch a single byte.
}
The TOCTOU gap, made concrete·text
  display time -------- (the gap) -------- apply time
  |                                        |
  row drawn:                               user hits Move
  report.pdf, 2.1 MB,                       <-- Waygate re-checks HERE
  modified 09:14                           is it still that file?
                                           same volume? same size/date?
     meanwhile, in the gap:                destination still ok?
     - file edited to 5.0 MB                collision unchanged?
     - or replaced entirely                any 'no' -> STOP, don't guess

External links

Exercise

Pick a destructive action in any app (empty trash, overwrite on save, delete selected). Identify its check-to-use gap: what is validated, when, and how long until the action fires? Then invent a scenario where the world changes inside that gap and the action does the wrong thing. What single late check would have caught it?
Hint
The classic is 'Save' overwriting a file that changed on disk after you opened it — checked at open, used at save, wrong in between. The late check that saves you is comparing the file's current modification state against what you assumed right before writing, and stopping if they differ. Check at use, not at check-time.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.