`startup`, `status` and the every-minute check each read the modules, read
the crontab and replace it. Two at once could interleave: the one that had
read the older module list replaced the crontab after the other had written
the newer one, and the module's lines were gone until the next check.
installRootCrontab() now holds a lock from before it reads the modules
until the crontab is replaced, so a list read before another process's
write cannot land after it. `startup` and `status` wait for the writer at
work; the every-minute check does not queue, it comes back a minute later.
The lock is taken on the class file, read-only: no lock file for root to
create in a directory xc_vm can write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
root_signals remembered the modules it had last written for as an MD5 of
config/modules.php in config/crontab.modules. Three things were wrong with
that:
- root wrote the marker with file_put_contents() in a directory xc_vm can
write: a link planted there had root overwrite the file it pointed to;
- a `startup` or `status` holding an older module list could write it over
a newer crontab while the marker still matched, and nothing put it right;
- the hash was MD5.
installRootCrontab() already compares the crontab with the list it wants
and writes only on a difference, so root_signals now simply calls it every
minute: an older list written by another process is corrected a minute
later, and there is no marker left to link or to hash.
The every-minute call is quiet when nothing changes, and writes nothing
when the crontab cannot be read: it runs from that crontab, so an
unreadable one is not an empty one, and writing over it would drop the
administrator's own lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A module's cron lines (CronProviderInterface) reach root's crontab
through StartupCommand::installRootCrontab(), which only startup and
status call. A module installed, enabled, disabled or removed from the
panel changed nothing there until the next restart: the anti-abuse
shield, for one, never ran after its install, so its honeypot ports
were never opened.
cron:root_signals, root's every-minute run, now writes the crontab again
when config/modules.php has changed since it was last written from it
(its hash is kept in config/crontab.modules). installRootCrontab() says
whether the crontab holds the list, and a write that failed is tried
again the next minute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>