Send SIGXCPU and SIGKILL at the guest CPU limits
Intent: a guest RLIMIT_CPU limits the guest's CPU time as it does on Linux. This is follow-up 2 of #448, which stores the limit in the guest table and leaves it unenforced.
Failure fixed: on main, a guest that sets RLIMIT_CPU to 1 s and spins gets no signal and runs until the vCPU watchdog ends elfuse after 10 s with status 124. On Linux the guest gets SIGXCPU after 1 s of CPU time.
This PR follows check_process_timers (kernel/time/posix-cpu-timers.c). At the soft limit the process gets SIGXCPU and the soft limit rises by one second, so a process that catches or ignores SIGXCPU gets another one each CPU second. At the hard limit it gets SIGKILL; when soft equals hard, SIGKILL alone. A guest spinning at EL0 takes no exit where elfuse could check the limit, so rlimit_cpu_main samples the host process CPU time, the clock guest CLOCK_PROCESS_CPUTIME_ID reads, from a thread of its own. The thread starts on the first finite soft limit (boot seed, fork-child restore, or prlimit64) and sleeps for the remaining CPU time divided by the online CPU count, at least 10 ms. A limit change wakes it.
Review focus:
rlimit_lockis a leaf, so the sampler drops it beforesignal_queue_info, which takessig_lock.- The soft limit is raised before
SIGXCPUis queued, so a handler that callsgetrlimitreads the new value, as on Linux. - Both signals are queued as
SI_KERNELwithsi_pid0, matchingSEND_SIG_PRIVincheck_rlimit. - A fork child starts the sampler through
sys_rlimit_cpu_start, called right aftersignal_set_state, since that call replaces the shared pending set and would drop a signal queued earlier. - elfuse ignores host
SIGXCPUand lifts its ownRLIMIT_CPUsoft limit to the hard one, aseeada5bdoes forRLIMIT_FSIZE. XNU sends nothing at the hard limit, so the sampler sendsSIGKILLat the lower of the guest and host hard limits, which keeps a guest with euid 0 from raising its way past the host limit.
Known gaps: a fork child is a new host process, so its CPU time starts with what its elfuse process spent before the guest resumes (a fresh launch measured 6 to 17 ms), where a Linux child starts at 0. The signal can land up to 10 ms after the limit, the sampler's shortest sleep. A fatal default signal taken by a worker thread ends only that thread (#476), so the sampler sends SIGKILL again every 10 ms, and a default SIGXCPU taken by a worker is not sent again. Under a finite host RLIMIT_CPU hard limit, elfuse stalls before the guest runs, on main as well (#474), so the host hard limit is checked only with a build that reads it from the environment. A CLONE_VM child without CLONE_THREAD runs in its parent's host process, so the two share one CPU clock and the parent can be signalled for the child's CPU time (#478).
Verified on 8134b88, rebased onto 3347188: make check-format and make check pass; make test-matrix-elfuse-aarch64 gives 317 passed, 0 failed, 10 skipped.