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>