As I looked at it more and how php does environment variables; I discovered the thing that was different was the system $PATH variable.
In bash on linux; this is what sets the search order for trying to find executables like php and composer.
Run php -i
from an command prompt and bash is search for an executable "php" in every folder of PATH. Note it stops when it finds the first match so the order of things in PATH is important.
So my problem is really that when craft runs a shell command; php/composer aren't in PATH.
putenv() does exactly that; allows php to set an environment variable for the duration of scrip execution. The very excellent dotenv library that craft depends on makes use of that.
The scl command line tools (source and scl) change PATH. Once I saw the expected values it was as simple as popping those over into the .env file for my site.
I added a PATH="/opt/rh/rh-php73/root/usr/bin:/opt/rh/rh-mysql56/root/usr/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin"
to the top of my sites .env file. (usually in the SITEROOT/craft/ folder)
and now updates succeed and database backups work too.
If you're using mamp or any other awkward environment where php isn't in the system path; this would likely work for you too.
Take note that variables set in a .env file WILL NOT overwrite an existing variable.
https://github.com/vlucas/phpdotenv/tree/3.6#immutability
a small change to your craft/web/index.php file is needed to change that behavior.
Change
Dotenv\Dotenv::create(CRAFT_BASE_PATH)->load();
to
Dotenv\Dotenv::create(CRAFT_BASE_PATH)->overload();
and then any settings in your .env file will be honored even if they already existing in the environment.