How to Integrate Shell Scripts into the RC Framework

How to Integrate Shell Scripts into the RC Framework​

For an introduction a must read is https://docs.freebsd.org/en/articles/rc-scripting/.

Motivation​

The WLAN interface of my laptop can hang under some circumstances. One can easily launch a few commands to let it recover, but putting the commands in a script is by far more convenient.In order to run the WLAN supervisor script permanently the idea came up to integrate it into the rc framework.

First Implementation​

The very first implementation of /usr/local/etc/rc.d/wlan has been as below. . /etc/rc.subr is sourced first. Then the script can make use of its functionality or overwrite functions.
Code:
#!/bin/sh
# PROVIDE: wlan
# REQUIRE: netif
. /etc/rc.subr
name=wlan
rcvar=wlan_enable

# Make sure the pidfile matches what's in the config file.
: ${wlan_enable="NO"}
: ${wlan_pidfile="/var/run/wlan.pid"}

pidfile=${wlan_pidfile}
command=/usr/sbin/daemon
procname=/usr/home/chris/scripts/${name}
command_args="-p ${pidfile} ${procname}"

load_rc_config $name
run_rc_command "$1"
Unfortunately it did not work. I followed examples to overwrite the start and stop commands which are sourced by . /etc/rc.subr
in the first line of the script. The code below has been integrated in the script above. logger(1) writes informative lines into /var/log/messages.
Code:
start_cmd="${name}_start"
stop_cmd="${name}_stop"

wlan_start()
{
/usr/bin/logger "WLAN wlan start with $1 PID ${pidfile}"
/usr/sbin/daemon -p ${pidfile}  ${command}
}

wlan_stop()
{
/usr/bin/logger "WLAN wlan stop"test -r $pidfile && pid=$(cat ${pidfile}); kill $pid
}
Now starting and stopping of the WLAN supervisor script by wlan_enable in /etc/rc.conf or by service wlan start and service wlan stop worked. But other commands as service wlan status failed.There are many files in /usr/local/etc/rc.d/ which overwrite functions of /etc/rc.subr, may be to countermeasure issues in ancient versions of /etc/rc.subr. In contrast /usr/local/etc/rc.d//tinyproxy is almost as the first approach and works without overwriting functions of /etc/rc.subr. Therefore this should be possible somehow with the WLAN supervisor script.

Digging into rc.subr​

/etc/rc.subr is a huge script. In order to output debug information logger(1) is very useful. An example of its output to /var/log/messages is taken from the function check_pidfile.
Code:
...
logger "CBR check_pidfile _pid $_pid"
logger "CBR check_pidfile end"
...
Comparing the debug information of tinyproxy and wlan ended up in _find_processes().
The pid is already read from /var/run/tinyproxy.pid and /var/run/wlan.pid.
The difference of tinyproxy occurs in the last section of _find_processes as below.
Code:
    logger "PS $PS _fp_args $_fp_args _psargs $_psargs npid $_npid_jid JID $JID _jid $_jid jid $jid"
    logger "$PS 2>/dev/null -o pid= -o jid= -o command= $_psargs"
    _proccheck="\
        $PS 2>/dev/null -o pid= -o jid= -o command= $_psargs"' |
        while read _npid _jid '"$_fp_args"'; do
            '"$_fp_match"'
                logger "JID: $JID _jid: $_jid";
                if [ "$JID" -eq "$_jid" ];
                then echo -n "$_pref$_npid";
                _pref=" ";
                logger "IF Matches";
                fi
                ;;
            esac
        done'

#   debug "in _find_processes: proccheck is ($_proccheck)."
    logger "CBR _find_process _procheck $_procheck"
    eval $_proccheck
    _pref=" "
    _jid=0
    jid=0
    logger "CBR _find_process _procheck $_procheck"
    logger "CBR _find_process rc_pid $rc_pid"
    logger "CBR _find_process end"
}
_procheck assembles a command which feeds ps(1) and run by eval $_procheck. For success $procname must match with the name outputted by ps.The function _find_processes() takes care if the pid matches. But additionally itchecks if the service is running. Otherwise a stale /var/run/wlan.pid could be there but the service might have crashed without notice. The example of the current situation as on my system running the ps(1) command in a shell is as below.
Code:
/usr/home/chris/> /bin/ps -ww 2>/dev/null -o pid= -o jid= -o command= -p 705
705 0 /bin/sh /usr/home/chris/scripts/wlan
This means $procname must be /bin/sh.
Now /usr/local/etc/rc.d/wlan has been rewritten.
Code:
#!/bin/sh

# PROVIDE: wlan
# REQUIRE: netif

. /etc/rc.subr
name=wlan
rcvar=wlan_enable

load_rc_config $name

# Make sure the pidfile matches what's in the config file.
: ${wlan_enable="NO"}
: ${wlan_pidfile="/var/run/wlan.pid"}

pidfile=${wlan_pidfile}
command=/usr/sbin/daemon
procname=/bin/sh
procname_script=/usr/home/chris/scripts/${name}
command_args="-p ${pidfile} ${procname_script}"
run_rc_command "$1"
Overwriting of functions of /etc/rc.subr is not required anymore. service wlan status work as well. Others are not tested up to now. The similar approach should be possible for other interpreters. When everything work as expected the logger(1) commands can be removed.

Note: rc.subr(8) mentions interpreter to add information about any involved interpreter. But I have not found any file in /etc/rc.d/ and /usr/local/rc.d/ which makes use of that parameter. This can be a topic for further investigations.

I hope that this how to do is useful.

Kind regards,
Christoph
 

Update about $command_interpreter​

If the command interpreter is specified in /usr/local/etc/rc.d/wlan there is no need to hide the $procname.
The script /usr/local/etc/rc.d/wlan with the small modification works similar as in the original how to do.
Code:
#!/bin/sh

# PROVIDE: wlan
# REQUIRE: netif

. /etc/rc.subr
name=wlan 
rcvar=wlan_enable

load_rc_config $name

# Make sure the pidfile matches what's in the config file.
: ${wlan_enable="NO"}
: ${wlan_pidfile="/var/run/wlan.pid"}

pidfile=${wlan_pidfile}
command=/usr/sbin/daemon
command_interpreter=/bin/sh
procname=/usr/home/chris/scripts/${name}
command_args="-p ${pidfile} ${procname}"
run_rc_command "$1"
 
Code:
#!/bin/sh

# PROVIDE: wlan
# REQUIRE: netif

. /etc/rc.subr
name=wlan
rcvar=wlan_enable

load_rc_config $name

# Make sure the pidfile matches what's in the config file.
: ${wlan_enable="NO"}
: ${wlan_pidfile="/var/run/wlan.pid"}

pidfile=${wlan_pidfile}
command=/usr/sbin/daemon
procname=/bin/sh
procname_script=/usr/home/chris/scripts/${name}
command_args="-p ${pidfile} ${procname_script}"
run_rc_command "$1"
Executing user scripts during boot is insecure. For a laptop used by a single person, it's marginally acceptable, but generally it's a back door to root privileges. Many of my customers ask that some script they own be run at boot under root privileges. The answer is always, I'll vet and copy your script but you cannot write to it (or its parent directory).

However at one place I worked they did allow customers to have their shell scripts run at boot. One of them got the brainy idea to use the rc script (on Linux at the time) to grant one of their apps setuid root privileges. From there they managed to pretty much administer the systems (multiple computers), even rebooting them. Unfortunately that management decision was cast in stone and they maintained root privileges for years.

This of course this was over my objections. But people said, what harm would it do and told me I needed to trust a little more.

Generally, procname_script=/home/whatever is a bad idea.

I think I've lived long enough to have seen it all already.
 
Dear cy@,
thank you for your input. I am just a hobbyist but I try to learn from experts like you to improve my skills. Do you think it is ok to run the script as nobody or a new user without other privileges and place the script at /usr/bin/, /usr/local/bin/ or so?
Thank you for your help,
Christoph
 
  • Like
Reactions: cy@
Dear cy@,
thank you for your input. I am just a hobbyist but I try to learn from experts like you to improve my skills. Do you think it is ok to run the script as nobody or a new user without other privileges and place the script at /usr/bin/, /usr/local/bin/ or so?
Thank you for your help,
Christoph
No problem.

For scripts like that you can copy them to /usr/local/etc/rc.d, or wherever else in /usr/local, change their ownership to root:wheel, with only root with write privileges to it.

In an environment like $JOB, we tell our customers tell us where the file is. We will review and vet it, and if it's ok, copy it to /usr/local/somewhere, owned and only writable by root. If they want to change it they submit a ticket, we review the changes, using diff(1), and copy it over the old script, that is if it's ok. If not we work with them to make it ok.

Customers think it's a pain but what is more painful is an auditor pointing this out during an audit or even worse, dealing with some sort of system compromise caused by the customer and both of which we are 100% responsible for. Neither of those are fun.

And then there's the sudo mine field. That's another can of worms. Especially with a crafty end-user with more knowledge than our new guy with only one year of experience. That's for another thread.

As a side note, while working as a IBM MVS systems programmer at a site, there was bug fix for a system crash. The customer refused to approve applying the patch. They would periodically force a system crash because each crash would result in our company paying a penalty of thousands of dollars back to them. They would exercise the known bug which they refused to sign of fixing just to reduce what their company paid to us for our services. This is an extreme example but shows what end-users and customers are willing to do. I think I've seen it all during my 45 year career.

I have other stories to tell which are just as chilling and better to be told in person with beer in hand.
 

Security Improvements​

Due to the concerns and helpful advises by cy@ some modifications have been made. First a user named wlan with low privileges has been created.
Code:
# grep wlan /etc/passwd 
wlan:*:1002:1002:wlan supervisor:/var/empty:/usr/sbin/nologin
The location and permissions of the pid file has been modified that only wlan and root can access the content. Nevertheless ps(1) can be used to get the pid. The output below is with running wlan. Otherwise there would be no pid file.
Code:
# ll /var/run | grep wlan && ll /var/run/wlan/wlan.pid
drwx------  2 wlan       wlan           3 10 Okt. 12:26 wlan/
-rw-------  1 wlan  wlan  5 10 Okt. 12:26 /var/run/wlan/wlan.pid
The content of /usr/local/etc/rc.d/wlan is write protected even for root by setting the schg flag.
Code:
# ll -o /usr/local/etc/rc.d/wlan
-rwxr-xr-x  1 root  wheel  schg,uarch 430 10 Okt. 12:49 /usr/local/etc/rc.d/wlan*
The script itself has been moved to /usr/local/bin/ and write protected, too. The ownership has been set to the user wlan.
Code:
# ll -o /usr/local/bin/wlan
-rwx------  1 wlan  wlan  schg,uarch 361 10 Okt. 13:08 /usr/local/bin/wlan*
Finally the user has had to be specified in /usr/local/etc/rc.d/wlan. Its content is as below.
Code:
#!/bin/sh

# PROVIDE: wlan
# REQUIRE: netif

. /etc/rc.subr
name=wlan 
rcvar=wlan_enable

load_rc_config $name

# Make sure the pidfile matches what's in the config file.
: ${wlan_enable="NO"}
: ${wlan_pidfile="/var/run/wlan/wlan.pid"}
: ${wlan_user="wlan"}

pidfile=${wlan_pidfile}
command=/usr/sbin/daemon
command_interpreter=/bin/sh
procname=/usr/local/bin/${name}
command_args="-p ${pidfile} ${procname}"
run_rc_command "$1"
Now the script runs as the user wlan.
Code:
> ps -aux|grep wlan
...
wlan       42588   0,0  0,1    10852   2324  -  Is   12:26     0:00,00 daemon: /usr/local/bin/wlan[42589] (daemon)
wlan       42589   0,0  0,1    11820   3208  -  S    12:26     0:01,51 /bin/sh /usr/local/bin/wlan
Many thanks again to cy@ for the kind help,
Christoph
 
Back
Top