From bcfa90cc31cd230f49de1ef2e72d7e034f5a9f35 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Mon, 26 Oct 2026 20:19:21 -0500 Subject: [PATCH] Fixed hub upgrade PG migration failing with Permission denied on old binaries Ticket: ENT-14899 Changelog: Title --- packaging/common/cfengine-hub/postinstall.sh | 13 +++- packaging/common/cfengine-hub/preinstall.sh | 9 ++- packaging/common/cfengine-hub/preremove.sh | 3 + .../common/script-templates/script-common.sh | 78 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packaging/common/cfengine-hub/postinstall.sh b/packaging/common/cfengine-hub/postinstall.sh index 508080cf2..2022c422f 100644 --- a/packaging/common/cfengine-hub/postinstall.sh +++ b/packaging/common/cfengine-hub/postinstall.sh @@ -631,7 +631,9 @@ migrate_db_using_dump_file() { safe_mv "$PREFIX/share" postgresql "$PREFIX/share.new" # restore old backup safe_cp "$BACKUP_DIR" data "$PREFIX/state/pg" - safe_cp "$BACKUP_DIR" bin "$PREFIX" + safe_cp_nolink "$BACKUP_DIR" bin "$PREFIX" + # Ensure restored old binaries are executable by cfpostgres. + chmod 755 "$PREFIX/bin"/* 2>/dev/null || true on_files cp "$BACKUP_DIR/lib" "$PREFIX/lib" safe_cp "$BACKUP_DIR/lib" postgresql "$PREFIX/lib" safe_cp "$BACKUP_DIR/share" postgresql "$PREFIX/share" @@ -745,6 +747,14 @@ do_migration() { else DEBUG=0 fi + + # cfpostgres runs the old binaries during the migration (ENT-14899) + if ! su cfpostgres -c "test -x $BACKUP_DIR/bin/pg_ctl"; then + cf_console echo "Warning: cfpostgres can't execute the old PostgreSQL binaries, setting them to 755:" + cf_console ls -l "$BACKUP_DIR/bin" + chmod 755 "$BACKUP_DIR/bin"/* + fi + cd /tmp cf_console echo "Migrating database using pg_upgrade utility..." cf_console echo @@ -1150,6 +1160,7 @@ if ! [ -f "$PREFIX/UPGRADED_FROM.txt" ] || egrep '3\.([0-6]\.|7\.0)' "$PREFIX/UP # Versions <= 3.7.0 are unreliable in their daemon killing. Kill them one # more time now that we have upgraded. cf_console platform_service cfengine3 stop + terminate_cf_agents fi # Let's make sure all files and directories created above have correct SELinux diff --git a/packaging/common/cfengine-hub/preinstall.sh b/packaging/common/cfengine-hub/preinstall.sh index 5e2b81368..0b7e390e3 100644 --- a/packaging/common/cfengine-hub/preinstall.sh +++ b/packaging/common/cfengine-hub/preinstall.sh @@ -144,6 +144,7 @@ fi #stop the remaining services on upgrade if is_upgrade; then cf_console platform_service cfengine3 stop + terminate_cf_agents # CFE-2278: Migrate to split units if use_systemd && [ -e /usr/lib/systemd/system/cfengine3-web.service ]; then # When using systemd, the services are split in two, and although both will @@ -252,7 +253,13 @@ if migrating_postgres; then # the postgresql.conf file was modified, we should try to use it after migration cp -a "$BACKUP_DIR/data/postgresql.conf" "$BACKUP_DIR/data/postgresql.conf.modified" fi - safe_cp "$PREFIX" bin "$BACKUP_DIR" + # Copy, don't hardlink: policy managing perms under $PREFIX/state/pg (e.g. + # CFE-951 in masterfiles) would otherwise change the live binaries too. + safe_cp_nolink "$PREFIX" bin "$BACKUP_DIR" + # Normalize executable permissions so cfpostgres can run old PG binaries + # during migration, whatever mode damage the source already had. + chmod 755 "$BACKUP_DIR/bin"/* 2>/dev/null || true + chmod 711 "$BACKUP_DIR/bin" 2>/dev/null || true on_files cp "$PREFIX/lib" "$BACKUP_DIR/lib" safe_cp "$PREFIX/lib" postgresql "$BACKUP_DIR/lib" safe_cp "$PREFIX/share" postgresql "$BACKUP_DIR/share" diff --git a/packaging/common/cfengine-hub/preremove.sh b/packaging/common/cfengine-hub/preremove.sh index 46e6bfb0f..1d55682ef 100644 --- a/packaging/common/cfengine-hub/preremove.sh +++ b/packaging/common/cfengine-hub/preremove.sh @@ -1,4 +1,7 @@ cf_console platform_service cfengine3 stop +if ! terminate_cf_agents; then + cf_console echo "Warning: continuing removal with cf-agent still running" +fi if use_systemd && [ -e /usr/lib/systemd/system/cfengine3-web.service ]; then # When using systemd, the services are split in two, and although both will # stop due to the command above, the web part may only do so after some diff --git a/packaging/common/script-templates/script-common.sh b/packaging/common/script-templates/script-common.sh index 432102315..93c40a20b 100644 --- a/packaging/common/script-templates/script-common.sh +++ b/packaging/common/script-templates/script-common.sh @@ -104,6 +104,69 @@ wait_for_cf_postgres_down() { cd /tmp && ! su cfpostgres -c "$PREFIX/bin/psql -l" >/dev/null 2>&1 } +cf_agent_pids() { + # Prints the PIDs of running cf-agent processes, except our own ancestors + # (cf-agent may be running this package upgrade) and, where /proc tells + # us, processes in other mount namespaces (containers). + _ancestors=" " + _pid=$$ + while [ -n "$_pid" ] && [ "$_pid" -gt 1 ]; do + _ancestors="$_ancestors$_pid " + _pid=$(ps -o ppid= -p "$_pid" 2>/dev/null | tr -d ' ') + done + _ns=$(readlink /proc/self/ns/mnt 2>/dev/null || true) + for _pid in $(pgrep -x cf-agent); do + case "$_ancestors" in + *" $_pid "*) continue ;; + esac + if [ -z "$_ns" ] || [ "$(readlink /proc/$_pid/ns/mnt 2>/dev/null)" = "$_ns" ]; then + echo "$_pid" + fi + done +} + +terminate_cf_agents() { + # Call after stopping cf-execd. Stopping cf-execd doesn't stop an agent it + # started (systemd KillMode=process), and that agent can keep changing + # things, like permissions under state/pg (ENT-14899). Gives running agents + # up to 600s to finish, then kills them. Fails if it can't check for agents + # or can't kill them. + if ! command -v pgrep >/dev/null; then + cf_console echo "Error: pgrep not found, can't check for running cf-agent" + return 1 + fi + _pids=$(cf_agent_pids) + if [ -z "$_pids" ]; then + return 0 + fi + _timeout=600 + cf_console echo "Waiting up to ${_timeout}s for cf-agent to finish:" + cf_console ps -o pid,etime,args -p "$(echo $_pids | tr ' ' ',')" || true + _waited=0 + while [ -n "$(cf_agent_pids)" ] && [ "$_waited" -lt "$_timeout" ]; do + sleep 5 + _waited=$((_waited + 5)) + done + # Killing an agent can let the shell cf-execd started it from run the next + # one (update.cf, then promises.cf), so check a few times. + for _try in 1 2 3; do + _pids=$(cf_agent_pids) + if [ -z "$_pids" ]; then + return 0 + fi + cf_console echo "Warning: killing cf-agent(s) still running after ${_timeout}s:" + cf_console ps -o pid,etime,args -p "$(echo $_pids | tr ' ' ',')" || true + kill -KILL $_pids 2>/dev/null || true + sleep 1 + done + _pids=$(cf_agent_pids) + if [ -n "$_pids" ]; then + cf_console echo "Error: cf-agent(s) still running after being killed:" + cf_console ps -o pid,etime,args -p "$(echo $_pids | tr ' ' ',')" || true + return 1 + fi +} + safe_cp() { # "safe" alternative to `cp`. Tries `cp -al` first, and if it fails - `cp -a`. # Deletes partially-copied files if copy operation fails. @@ -136,6 +199,21 @@ safe_cp() { return 1 } +safe_cp_nolink() { + # Like `safe_cp`, but never creates hardlinks, so the copy does not share + # mode and owner with the source. Args are same as in safe_cp. + test "$#" -eq 3 || return 2 + from="$1" + name="$2" + to="$3" + if cp -a "$from/$name" "$to"; then + return 0 + fi + echo "Copy failed, so removing partially-copied data and aborting" + rm -rf "$to/$name" + return 1 +} + safe_mv() { # "safe" alternative to `mv`. Executes `safe_cp` and deletes source dir if # that succeeds.