Skip to content

CLOS-7025: reinstall the CageFS hooks on the first boot - #73

Open
prilr wants to merge 3 commits into
cloudlinuxfrom
CLOS-7025-reinstall-cagefs-hooks-on-first-boot
Open

CLOS-7025: reinstall the CageFS hooks on the first boot#73
prilr wants to merge 3 commits into
cloudlinuxfrom
CLOS-7025-reinstall-cagefs-hooks-on-first-boot

Conversation

@prilr

@prilr prilr commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

After a CloudLinux 8 to 9 elevation on Plesk, /etc/pam.d/su has no pam_lve.so line, CageFS users silently stop entering the cage through su, and cldiag --all fails on "pam_lve configuration is not found in /etc/pam.d/su config file".

Why it happens

CageFS installs its hooks from its own %posttrans scriptlet in two steps:

  1. install-cagefs-plugin.py --install runs first and, on Plesk >= 10, deliberately strips the pam_lve.so and pam_sulve.so lines from /etc/pam.d/su with sed.
  2. cagefsctl --hook-remove + cagefsctl --hook-install further down (CAG-377) put the pam_lve.so line back.

On a booted system both halves run and the file settles at session required pam_lve.so 500 1.

Inside leapp's upgrade transaction cagefsctl cannot run at all: the system is not booted with systemd as PID 1 and the process cannot look itself up in /proc, so clcommon logging setup raises psutil.NoSuchProcess and cagefsctl exits having done nothing. Only the stripping half takes effect.

Plesk only: cPanel, DirectAdmin and nopanel keep the line, because the strip sits in the plugin's Plesk branch.

The change

The update_cagefs FirstBoot actor now runs cagefsctl --hook-install before cagefsctl --force-update, performing on the first boot the repair the transaction could not. Idempotent, a failure does not skip the cage rebuild, and both failure paths log the command to rerun.

The actor's logic moves into libraries/updatecagefs.py with tests, following the repository's actor convention. Six unit tests cover ordering, the cagefsctl-absent guard, failure isolation and failure reporting; the ordering assertion was mutation-checked by swapping the two calls.

prilr and others added 3 commits September 8, 2026 04:38
CageFS installs its hooks from its own %posttrans scriptlet in two steps:
install-cagefs-plugin.py --install runs first and, on Plesk >= 10,
deliberately strips the pam_lve.so and pam_sulve.so lines from
/etc/pam.d/su, and the cagefsctl --hook-remove + --hook-install pair
further down (CAG-377) is what puts the pam_lve.so line back.

cagefsctl cannot run inside leapp's upgrade transaction: the system is
not booted with systemd as PID 1 and the process cannot look itself up in
/proc, so clcommon's logging setup raises psutil.NoSuchProcess and
cagefsctl exits having done nothing - one converted box's
leapp-upgrade.log records 171 cagefsctl invocations dying that way. Only
the stripping half of the pair takes effect, so after a CloudLinux 8 to 9 elevation on Plesk
/etc/pam.d/su has no pam_lve.so line, CageFS users silently stop entering
the cage through 'su', and cldiag fails on "pam_lve configuration is not
found in /etc/pam.d/su config file".

util-linux, which owns the file, is not involved: it ships /etc/pam.d/su
as %config(noreplace) and its el8 and el9 copies are byte-identical, so
rpm leaves the file alone and does not even write an .rpmnew.

The first boot is a fully booted system, where cagefsctl works, so the
update_cagefs actor runs 'cagefsctl --hook-install' there - before the cage rebuild, so that the
cages get the corrected /etc/pam.d files. It is idempotent, and its
failure does not skip the rebuild.

Verified on a CloudLinux 9 + Plesk VM: with the hooks intact a CageFS
user is caged, running the real CageFS plugin strips the line and the
user reaches the host binary instead, and 'cagefsctl --hook-install'
cages them again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Worked-On: cl-aiworkspaces
Review findings on the previous commit.

'cagefsctl --hook-install' cannot report failure through its exit code:
cagefsctl calls HooksInstall() and then exits zero unconditionally, and
configure_pam_lve() swallows IOError and OSError into a printed message.
So an immutable or read-only /etc/pam.d/su left CageFS users uncaged
while this actor logged "cagefs hooks were reinstalled successfully" -
checked=True could not detect the one failure its error message was
written for. Check the resulting configuration instead, parsing the same
non-comment 'pam_lve.so' line cldiag looks for, and parse it here rather
than importing clcommon so that a first-boot actor does not depend on
the CloudLinux venv being healthy.

A silent loss of confinement on an otherwise successful upgrade does not
belong in a debug log nobody reads afterwards, so that outcome now also
emits a report. The actor already declared produces = (Report,); this
makes the declaration true.

Second finding: leapp's run() raises OSError, not CalledProcessError,
when the executable is missing or has wrong permissions, and the guard
in process() is os.path.exists rather than os.access(X_OK). A cagefsctl
that exists but is zero-byte or non-executable - the shape of leapp's
own /sbin/new-kernel-pkg stub - therefore aborted the actor and cost the
cage rebuild that follows. Both cagefsctl calls now go through one
helper that catches either exception, so neither call can take the
other's step with it.

Tests: the two failure modes are pinned, along with a commented-out
pam_lve line and an unreadable config, both of which must count as not
configured. Before this change five of the nine tests fail - two on the
OSError escaping, three on the silent failure going unreported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Worked-On: cl-aiworkspaces
CageFS's %posttrans makes 25 cagefsctl calls; this actor restores two.
--force-update sets force-update/update/build_jail_flag and checks the
skeleton, so it rebuilds the skeleton and the jails and covers none of the
others: --setup-cl-selector, --update-wrappers, --reconfigure-cagefs,
--isolates-regenerate, --sync-proxy-commands, --create-virt-mp-all and the
rest have no references in cagefsctl.py outside their own option handlers,
so nothing in the update path reaches them. They are lost to the same
crash, which on a Plesk or cPanel box with site isolation can leave PHP
Selector broken inside the cages and the per-domain lsphp includes absent.

Widening this actor is not the answer. The fix belongs in clcommon, where
one guard restores all 25 at once, and the sequencing makes that sufficient
on its own: the transaction upgrades the CloudLinux venv and cllib around
step 1.5k of 7k while CageFS's %posttrans runs at 7077/7077, so the copy
that crashes is the target system's, and a fixed cllib in the target
repositories covers every subsequent upgrade - the source system's copy
never runs. Replaying more of a vendor scriptlet from a first-boot actor
would trade a measured narrow gap for unmeasured breadth: --remount-all on
a live box, for one.

Comment only, so the narrow scope reads as a decision rather than an
oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Worked-On: cl-aiworkspaces
@prilr
prilr force-pushed the CLOS-7025-reinstall-cagefs-hooks-on-first-boot branch from a09b13a to edb95a9 Compare September 8, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant