bogus handling of DAEMON_OPTS by chronyd-starter.sh

Bug #1898000 reported by Simon Déziel
8
This bug affects 1 person
Affects Status Importance Assigned to Milestone
chrony (Ubuntu)
Fix Released
Low
Christian Ehrhardt 

Bug Description

By default, chrony's DAEMON_OPTS is set to "-F -1" which means to enable seccomp but not in kill mode. To enable kill mode while also running in a container, one would use "-F 1 -x" but it seems to confuse getopts (from /usr/lib/systemd/scripts/chronyd-starter.sh) into thinking that no "-x" was provided and thus wrongly logs a warning about the CAP_SYS_TIME missing and also adds an extraneous "-x".

# Steps to reproduce:

1) create and enter into a test container:
lxc launch images:ubuntu/focal foo
lxc shell foo
2) install chrony:
apt update
apt install -y chrony
3) set DAEMON_OPTS="-F 1 -x" in /etc/default/chrony
4) restart chrony
systemctl restart chrony
5) check arguments passed to chronyd
ps aux| grep chrony

The last step should show that chronyd was invoked with 3 args: "-F 1 -x" but due to the bug, it shows 4 arguments:
_chrony 106 0.0 0.0 13212 2072 ? S 03:08 0:00 /usr/sbin/chronyd -F 1 -x -x
_chrony 107 0.0 0.0 5032 1728 ? S 03:08 0:00 /usr/sbin/chronyd -F 1 -x -x

# Workaround:

Simply setting DAEMON_OPTS to "-x -F 1" or "-F1 -x" will do.

# Simpler way to reproduce

Kkeep an eye on $X_SET and run:

sh -x /usr/lib/systemd/scripts/chronyd-starter.sh -F -1 -x
 or
sh -x /usr/lib/systemd/scripts/chronyd-starter.sh -F 1 -x

I realize this is an edge case that probably really few might run into but since I've lost a good chunk of time wondering was what going on, I felt I needed to report it. I would have preferred to send a patch but it's too late for me to try to tame getopts ;)

The bug does not affect Debian as /usr/lib/systemd/scripts/chronyd-starter.sh is an Ubuntu delta (carried to Groovy). Don't get me wrong, I appreciate the delta as I can easily run chrony inside a container, so thank you ;)

# Additional information

$ apt-cache policy chrony
chrony:
  Installed: 3.5-6ubuntu6.2
  Candidate: 3.5-6ubuntu6.2
  Version table:
 *** 3.5-6ubuntu6.2 500
        500 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu focal-security/main amd64 Packages
        100 /var/lib/dpkg/status
     3.5-6ubuntu6 500
        500 http://archive.ubuntu.com/ubuntu focal/main amd64 Packages
$ lsb_release -rd
Description: Ubuntu 20.04.1 LTS
Release: 20.04

Tags: server-next

Related branches

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Hi Simon,
you are right and this is a valid bug for sure.
Yet I have a very busy week ahead - it might take some time ... :-/

tags: added: server-next
Changed in chrony (Ubuntu):
assignee: nobody → Christian Ehrhardt  (paelzer)
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Hrm I got to test this now and I can't reproduce with:

Groovy
root@g:~# grep OPT /etc/default/chrony
DAEMON_OPTS="-F -1 -x"
root@g:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
             ├─118154 /usr/sbin/chronyd -F -1 -x
             └─118155 /usr/sbin/chronyd -F -1 -x

Focal
root@f:~# grep OPT /etc/default/chrony
DAEMON_OPTS="-F -1 -x"
root@f:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
             ├─80336 /usr/sbin/chronyd -F -1 -x
             └─80337 /usr/sbin/chronyd -F -1 -x

Hmm, maybe my containers I currently use are "too capable"

The script does two things:
1. it checks if it is running in a container
OR
2. it checks if it misses cap_sys_time

If 1 || 2 are true the service would (by default) fail to run at all.
The wrapper automatically adds -x in that case.

So if 1 || 2 you'll see:
  "Adding -x as fallback disabling control of the system clock, see
  /usr/share/doc/chrony/README.container to override this behavior"

But when you add -x in the OPTs yourself it should still be ok.
It runs a getopt loop if there is a -x.
If it is set it does not go into any of the later checks and runs chrony as you told it.

Test on non privileged container
default:
             ├─1459 /usr/sbin/chronyd -F -1 -x
             └─1460 /usr/sbin/chronyd -F -1 -x
...
Oct 05 11:27:20 g-chrony chronyd-starter.sh[1454]: Warning: Running in a container, likely impossible and unintended to sync system clock
Oct 05 11:27:20 g-chrony chronyd-starter.sh[1454]: Adding -x as fallback disabling control of the system clock, see /usr/share/doc/chrony/README.container to override this behavior

Adding -x to the options still does not break it ... hmm:

Groovy:
root@g-chrony:~# grep OPT /etc/default/chrony
DAEMON_OPTS="-F -1 -x"
root@g-chrony:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
             ├─1688 /usr/sbin/chronyd -F -1 -x
             └─1689 /usr/sbin/chronyd -F -1 -x

Focal:
root@f-chrony:~# grep OPT /etc/default/chrony
DAEMON_OPTS="-F -1 -x"
root@f-chrony:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
             ├─2221 /usr/sbin/chronyd -F -1 -x
             └─2222 /usr/sbin/chronyd -F -1 -x

Still ok.
Could you add set -x to /usr/lib/systemd/scripts/chronyd-starter.sh and let me know why the getopt loop might not work for you?

From /usr/lib/systemd/scripts/chronyd-starter.sh:
# Check if -x is already set manually, don't process further if that is the case
X_SET=0
while getopts ":x" opt; do
    case $opt in
        x)
            X_SET=1
            ;;
    esac
done

Changed in chrony (Ubuntu):
status: New → Incomplete
Revision history for this message
Simon Déziel (sdeziel) wrote : Re: [Bug 1898000] Re: bogus handling of DAEMON_OPTS by chronyd-starter.sh

Hello Christian,

One need to use "-F 1 -x" in order to reproduce. The "1" instead of "-1"
is what causes the bug to happen. Thanks!

Regards,
Simon

On 2020-10-05 7:33 a.m., Christian Ehrhardt  wrote:
> Hrm I got to test this now and I can't reproduce with:
>
> Groovy
> root@g:~# grep OPT /etc/default/chrony
> DAEMON_OPTS="-F -1 -x"
> root@g:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
> ├─118154 /usr/sbin/chronyd -F -1 -x
> └─118155 /usr/sbin/chronyd -F -1 -x
>
> Focal
> root@f:~# grep OPT /etc/default/chrony
> DAEMON_OPTS="-F -1 -x"
> root@f:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
> ├─80336 /usr/sbin/chronyd -F -1 -x
> └─80337 /usr/sbin/chronyd -F -1 -x
>
> Hmm, maybe my containers I currently use are "too capable"
>
> The script does two things:
> 1. it checks if it is running in a container
> OR
> 2. it checks if it misses cap_sys_time
>
> If 1 || 2 are true the service would (by default) fail to run at all.
> The wrapper automatically adds -x in that case.
>
> So if 1 || 2 you'll see:
> "Adding -x as fallback disabling control of the system clock, see
> /usr/share/doc/chrony/README.container to override this behavior"
>
>
> But when you add -x in the OPTs yourself it should still be ok.
> It runs a getopt loop if there is a -x.
> If it is set it does not go into any of the later checks and runs chrony as you told it.
>
> Test on non privileged container
> default:
> ├─1459 /usr/sbin/chronyd -F -1 -x
> └─1460 /usr/sbin/chronyd -F -1 -x
> ...
> Oct 05 11:27:20 g-chrony chronyd-starter.sh[1454]: Warning: Running in a container, likely impossible and unintended to sync system clock
> Oct 05 11:27:20 g-chrony chronyd-starter.sh[1454]: Adding -x as fallback disabling control of the system clock, see /usr/share/doc/chrony/README.container to override this behavior
>
>
> Adding -x to the options still does not break it ... hmm:
>
> Groovy:
> root@g-chrony:~# grep OPT /etc/default/chrony
> DAEMON_OPTS="-F -1 -x"
> root@g-chrony:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
> ├─1688 /usr/sbin/chronyd -F -1 -x
> └─1689 /usr/sbin/chronyd -F -1 -x
>
> Focal:
> root@f-chrony:~# grep OPT /etc/default/chrony
> DAEMON_OPTS="-F -1 -x"
> root@f-chrony:~# systemctl restart chrony; systemctl status chrony | grep 'sbin\/chronyd'
> ├─2221 /usr/sbin/chronyd -F -1 -x
> └─2222 /usr/sbin/chronyd -F -1 -x
>
>
> Still ok.
> Could you add set -x to /usr/lib/systemd/scripts/chronyd-starter.sh and let me know why the getopt loop might not work for you?
>
>
>>From /usr/lib/systemd/scripts/chronyd-starter.sh:
> # Check if -x is already set manually, don't process further if that is the case
> X_SET=0
> while getopts ":x" opt; do
> case $opt in
> x)
> X_SET=1
> ;;
> esac
> done
>
>
> ** Changed in: chrony (Ubuntu)
> Status: New => Incomplete
>

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Thanks Simon, I missed that little detail when retrying.
I now can confirm.

The impact is gladly rather low as it makes one "-x" to two "-x -x" which should not change the behavior of chronyd.

For the time being anyone affected that sets "-x" manually in /etc/default/chrony and want to avoid the double -x you can just set
   SYNC_IN_CONTAINER="yes"
That will also stop the wrapper from adding -x here.

Changed in chrony (Ubuntu):
importance: Undecided → Low
status: Incomplete → Confirmed
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

As mentioned we use getopt ignoring all but -x to select.

opstring so far was ":x"
1. leading ":" => silence errors
2. look for -x

We had hoped that we don't have to "keep track" of the official chronyd arguments that way.

But what happens is that the "1" as argument to -F with an optstring that misses "F:" will make getopt consider it the end of the options.
Therefore the getopt loop ends and won't reach the -x :-/

For some hardening I wonder if for our purpose "is a -x set" I wonder if we should switch away from getopt entirely or improve on it.

The following is already much more robust ignoring any bad args and odd arguments in between
for idx in $(seq 1 $#); do
    OPTIND=$idx
    echo PROCESS OPTIND $OPTIND
    if getopts ":x" opt; then
        echo OPT is $opt
        if [ "$opt" = "x" ]; then
            echo -x is set
        fi
    fi
done

But it would still fails at combined short args like -qdx to recognize -x out of these.

The following is even better, but depends on bash
for arg in $@; do
    echo ARG = $arg
    if [[ "$arg" =~ ^-[a-zA-Z0-9]*x ]]; then
        X_SET=1
    fi
done

Or we could do the same with grep

for arg in $@; do
    echo ARG = $arg
    if echo "$arg" | grep -q -e '^-[a-zA-Z0-9]*x'; then
         X_SET=1
    fi
done

The last two versions catch:
a) -F -1 -x
b) -F 1 -x
c) -dx
d) not confused by --longopts (non existing in chronyd today, but who knows)
e) any combination of a/b/c/d

bash and grep both already have
  Essential: yes
So we'd not need a dependency on it when introducing this change.

Generally people like grep more than bash regex, so I'm going with that.

Changed in chrony (Ubuntu):
status: Confirmed → In Progress
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Uploaded to groovy and not requiring an FFe (since it is a small and pure fix).
Never the less it will wait a bit until the release Team approves it since we are in Freeze for 20.10.

Revision history for this message
Launchpad Janitor (janitor) wrote :

This bug was fixed in the package chrony - 3.5.1-1ubuntu2

---------------
chrony (3.5.1-1ubuntu2) groovy; urgency=medium

  * d/chronyd-starter.sh: fix commandline argument parsing (LP: #1898000)

 -- Christian Ehrhardt <email address hidden> Tue, 06 Oct 2020 12:20:40 +0200

Changed in chrony (Ubuntu):
status: In Progress → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.