Spinlocks and interrupt masking
A spinlock protects data from code running on another CPU: a thread that finds the lock taken loops until the holder releases it. That works because the holder keeps running on its own CPU while the other one spins. It does not work against an interrupt handler on the same CPU. If the handler fires while the code it interrupted holds the lock, and tries to take the lock itself, it spins, and the holder cannot release the lock until the handler returns, so the CPU spins forever.
Code that shares data with an interrupt handler therefore masks interrupts on its CPU around each access, so the handler cannot fire in the middle of it. Masking is the only protection a CPU needs against its own interrupts, but a spinlock is still needed against other CPUs, so kernels combine the two in an interrupt-safe lock: mask, then take the lock; release, then restore. Masking saves the previous interrupt state and restores it, rather than turning interrupts back on, so one masked section can sit inside another.
The trap is code that takes no lock. A lock only excludes code that takes the same lock, and an interrupt handler that touches the data without taking it, often because it is not allowed to wait, is not kept out by any lock the thread side holds. Only masking keeps that handler out. Masking also makes exactly one section atomic: a caller that reads a count in one masked call and then walks the items in further calls can see the structure change between them.
In the Cosmos scheduler, the tick path takes no lock at all: OnTick runs inside the timer interrupt and PickNext inside whichever interrupt exit reschedules, and both rely on interrupts being masked there. The manager calls the thread lifecycle hooks and the two per-CPU hooks with interrupts masked, so the tick cannot land inside them. SchedulerManager.SetPriority holds a per-CPU spinlock taken without masking, which keeps out a second caller but not the tick, and GetPriority, GetRunQueueCount and GetRunQueueThread are called with no guard at all. Those four hooks, and any entry point a policy adds of its own, mask with SchedulerManager.MaskInterrupts(), whose InterruptMaskScope restores the previous state when it is disposed, so masking inside an already masked hook is harmless. See SpinLock and the IRQ-safe scope and Kernel constraints.