Files
chrony/test/system/test.common
Miroslav Lichvar be7f5e8916 client: add support for dropping root privileges
To minimize the impact of potential attacks targeting chronyc started
under root (e.g. performed by a local chronyd process running without
root privileges, a remote chronyd process, or a MITM attacker on the
network), add support for changing the effective UID/GID in chronyc
after start.

The user can be specified by the -u option, similarly to chronyd. The
default chronyc user can be changed by the --with-chronyc-user
configure option. The default value of the default chronyc user is
"root", i.e. chronyc doesn't try to change the identity by default.

The default chronyc user does not follow the default chronyd user
set by the configure --with-user option to avoid errors on systems where
chronyc is not allowed to change its UID/GID (e.g. by a SELinux policy).
2025-08-07 10:18:31 +02:00

415 lines
9.8 KiB
Plaintext

# Copyright (C) Miroslav Lichvar 2009
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
export LC_ALL=C
export PATH=${CHRONY_PATH:-../..}:$PATH
TEST_DIR=${TEST_DIR:-$(pwd)/tmp}
TEST_LIBDIR=${TEST_LIBDIR:-$TEST_DIR}
TEST_LOGDIR=${TEST_LOGDIR:-$TEST_DIR}
TEST_RUNDIR=${TEST_RUNDIR:-$TEST_DIR}
TEST_SCFILTER=${TEST_SCFILTER:-0}
TEST_ROOT_USER=${TEST_ROOT_USER:-root}
TEST_PRIVDROP_USER=${TEST_PRIVDROP_USER:-nobody}
test_start() {
local user=$(get_user)
check_chronyd_features CMDMON || test_skip "CMDMON support disabled"
[ "${#TEST_DIR}" -ge 5 ] || test_skip "invalid TEST_DIR"
rm -rf "$TEST_DIR"
mkdir -p "$TEST_DIR" && chmod 700 "$TEST_DIR" || test_skip "could not create $TEST_DIR"
[ -d "$TEST_LIBDIR" ] || test_skip "missing $TEST_LIBDIR"
[ -d "$TEST_LOGDIR" ] || test_skip "missing $TEST_LOGDIR"
[ -d "$TEST_RUNDIR" ] || test_skip "missing $TEST_RUNDIR"
rm -f "$TEST_LIBDIR"/* "$TEST_LOGDIR"/* "$TEST_RUNDIR"/*
if [ "$user" != "$TEST_ROOT_USER" ]; then
id -u "$user" > /dev/null 2> /dev/null || test_skip "missing user $user"
chown "$user:$(id -g "$user")" "$TEST_DIR" || test_skip "could not chown $TEST_DIR"
su "$user" -s /bin/sh -c "touch $TEST_DIR/test" 2> /dev/null || \
test_skip "$user cannot access $TEST_DIR"
rm "$TEST_DIR/test"
else
chown 0:0 "$TEST_DIR" || test_skip "could not chown $TEST_DIR"
fi
echo "Testing $*:"
}
test_pass() {
echo "PASS"
exit 0
}
test_fail() {
echo "FAIL"
exit 1
}
test_skip() {
local msg=$1
[ -n "$msg" ] && echo "SKIP ($msg)" || echo "SKIP"
exit 9
}
test_ok() {
pad_line
echo -e "\tOK"
return 0
}
test_bad() {
pad_line
echo -e "\tBAD"
return 1
}
test_error() {
pad_line
echo -e "\tERROR"
return 1
}
chronyd=$(command -v chronyd)
chronyc=$(command -v chronyc)
[ $EUID -eq 0 ] || test_skip "not root"
[ -x "$chronyd" ] || test_skip "chronyd not found"
[ -x "$chronyc" ] || test_skip "chronyc not found"
if netstat -aln > /dev/null 2> /dev/null; then
port_list_command="netstat -aln"
elif ss -atun > /dev/null 2> /dev/null; then
port_list_command="ss -atun"
else
test_skip "missing netstat or ss"
fi
# Default test testings
default_minimal_config=0
default_extra_chronyd_directives=""
default_extra_chronyd_options=""
default_clock_control=0
default_server=127.0.0.1
default_server_name=127.0.0.1
default_server_options=""
default_priv_drop=0
# Initialize test settings from their defaults
for defoptname in ${!default_*}; do
optname=${defoptname#default_}
[ -z "${!optname}" ] && declare "$optname"="${!defoptname}"
done
msg_length=0
pad_line() {
local line_length=56
[ $msg_length -lt $line_length ] && \
printf "%$((line_length - msg_length))s" ""
msg_length=0
}
# Print aligned message
test_message() {
local level=$1 eol=$2
shift 2
local msg="$*"
while [ "$level" -gt 0 ]; do
echo -n " "
level=$((level - 1))
msg_length=$((msg_length + 2))
done
echo -n "$msg"
msg_length=$((msg_length + ${#msg}))
if [ "$eol" -ne 0 ]; then
echo
msg_length=0
fi
}
# Check if chronyd has specified features
check_chronyd_features() {
local feature features
features=$($chronyd -v | sed 's/.*(\(.*\)).*/\1/')
for feature; do
echo "$features" | grep -q "+$feature" || return 1
done
}
# Print test settings which differ from default value
print_nondefaults() {
local defoptname optname
test_message 1 1 "non-default settings:"
for defoptname in ${!default_*}; do
optname=${defoptname#default_}
[ "${!defoptname}" = "${!optname}" ] || \
test_message 2 1 "$optname"=${!optname}
done
}
get_conffile() {
echo "$TEST_DIR/chronyd.conf"
}
get_pidfile() {
echo "$TEST_RUNDIR/chronyd.pid"
}
get_logfile() {
echo "$TEST_LOGDIR/chronyd.log"
}
get_cmdsocket() {
echo "$TEST_RUNDIR/chronyd.sock"
}
get_user() {
if [ "$priv_drop" -ne 0 ]; then
echo "$TEST_PRIVDROP_USER"
else
echo "$TEST_ROOT_USER"
fi
}
# Find a free port in the 10000-20000 range (their use is racy)
get_free_port() {
local port
while true; do
port=$((RANDOM % 10000 + 10000))
$port_list_command | grep -q '^\(tcp\|udp\).*[:.]'"$port " && continue
break
done
echo $port
}
generate_chrony_conf() {
local user ntpport cmdport
user=$(get_user)
ntpport=$(get_free_port)
cmdport=$(get_free_port)
echo "0.0 10000" > "$TEST_LIBDIR/driftfile"
echo "1 MD5 abcdefghijklmnopq" > "$TEST_DIR/keys"
echo "0.0" > "$TEST_DIR/tempcomp"
chown "$user:$(id -g "$user")" "$TEST_LIBDIR/driftfile" "$TEST_DIR"/{keys,tempcomp}
(
echo "pidfile $(get_pidfile)"
echo "bindcmdaddress $(get_cmdsocket)"
echo "port $ntpport"
echo "cmdport $cmdport"
echo "$extra_chronyd_directives"
[ "$minimal_config" -ne 0 ] && exit 0
echo "allow"
echo "cmdallow"
echo "local"
echo "server $server_name port $ntpport minpoll -6 maxpoll -6 $server_options"
[ "$server" = "127.0.0.1" ] && echo "bindacqaddress $server"
echo "bindaddress 127.0.0.1"
echo "bindcmdaddress 127.0.0.1"
echo "dumpdir $TEST_RUNDIR"
echo "logdir $TEST_LOGDIR"
echo "log tempcomp rawmeasurements refclocks statistics tracking rtc"
echo "logbanner 0"
echo "smoothtime 100.0 0.001"
echo "leapsectz right/UTC"
echo "dscp 46"
echo "include /dev/null"
echo "keyfile $TEST_DIR/keys"
echo "driftfile $TEST_LIBDIR/driftfile"
echo "tempcomp $TEST_DIR/tempcomp 0.1 0 0 0 0"
) > "$(get_conffile)"
}
get_chronyd_options() {
[ "$clock_control" -eq 0 ] && echo "-x"
echo "-l $(get_logfile)"
echo "-f $(get_conffile)"
echo "-u $(get_user)"
echo "-F $TEST_SCFILTER"
echo "$extra_chronyd_options"
}
# Start a chronyd instance
start_chronyd() {
local pid pidfile=$(get_pidfile) wrapper_options=""
print_nondefaults
test_message 1 0 "starting chronyd"
generate_chrony_conf
trap stop_chronyd EXIT
rm -f "$TEST_LOGDIR"/*.log
if [[ $CHRONYD_WRAPPER == *valgrind* ]]; then
wrapper_options="--log-file=$TEST_DIR/chronyd.valgrind --enable-debuginfod=no"
fi
$CHRONYD_WRAPPER $wrapper_options \
"$chronyd" $(get_chronyd_options) > "$TEST_DIR/chronyd.out" 2>&1
[ $? -eq 0 ] && [ -f "$pidfile" ] && ps -p "$(cat "$pidfile")" > /dev/null && test_ok || test_error
}
wait_for_sync() {
local prev_length
test_message 1 0 "waiting for synchronization"
prev_length=$msg_length
for i in $(seq 1 10); do
run_chronyc "ntpdata $server" > /dev/null 2>&1 || break
if check_chronyc_output "Total RX +: [1-9]" > /dev/null 2>&1; then
msg_length=$prev_length
test_ok
return
fi
sleep 1
done
msg_length=$prev_length
test_error
}
# Stop the chronyd instance
stop_chronyd() {
local pid pidfile
pidfile=$(get_pidfile)
[ -f "$pidfile" ] || return 0
pid=$(cat "$pidfile")
test_message 1 0 "stopping chronyd"
if ! kill "$pid" 2> /dev/null; then
test_error
return
fi
# Wait for the process to terminate (we cannot use "wait")
while ps -p "$pid" > /dev/null; do
sleep 0.1
done
test_ok
if [ -f "$TEST_DIR/chronyd.valgrind" ]; then
test_message 2 0 "checking valgrind report"
! grep -q 'ERROR SUMMARY: [^0]' "$TEST_DIR/chronyd.valgrind" && \
test_ok || test_bad
fi
}
# Check chronyd log for expected and unexpected messages
check_chronyd_messages() {
local logfile=$(get_logfile)
test_message 1 0 "checking chronyd messages"
grep -q 'chronyd exiting' "$logfile" && \
([ "$clock_control" -eq 0 ] || ! grep -q 'Disabled control of system clock' "$logfile") && \
([ "$clock_control" -ne 0 ] || grep -q 'Disabled control of system clock' "$logfile") && \
([ "$minimal_config" -ne 0 ] || grep -q 'Frequency .* read from' "$logfile") && \
grep -q 'chronyd exiting' "$logfile" && \
! (grep -v '^.\{19\}Z D:' "$logfile" | grep -q 'Could not') && \
! grep -q 'Disabled command socket' "$logfile" && \
test_ok || test_bad
}
# Check the number of messages matching a pattern in a specified file
check_chronyd_message_count() {
local count pattern=$1 min=$2 max=$3 logfile=$(get_logfile)
test_message 1 0 "checking message \"$pattern\""
count=$(grep "$pattern" "$(get_logfile)" | wc -l)
[ "$min" -le "$count" ] && [ "$count" -le "$max" ] && test_ok || test_bad
}
# Check the logs and dump file for measurements and a clock update
check_chronyd_files() {
test_message 1 0 "checking chronyd files"
grep -q " $server .* 111 111 1110 " "$TEST_LOGDIR/measurements.log" && \
[ -f "$TEST_LOGDIR/tempcomp.log" ] && [ "$(wc -l < "$TEST_LOGDIR/tempcomp.log")" -ge 2 ] && \
test_ok || test_bad
}
# Run a chronyc command
run_chronyc() {
local host=$chronyc_host options="-n -m" wrapper_options="" ret=0
test_message 1 0 "running chronyc $([ -n "$host" ] && echo "@$host ")$*"
if [ -z "$host" ]; then
host="$(get_cmdsocket)"
else
options="$options -p $(grep cmdport "$(get_conffile)" | awk '{print $2}')"
fi
if [[ $CHRONYC_WRAPPER == *valgrind* ]]; then
wrapper_options="--log-file=$TEST_DIR/chronyc.valgrind --enable-debuginfod=no"
fi
$CHRONYC_WRAPPER $wrapper_options \
"$chronyc" -h "$host" -u "$(get_user)" $options "$@" > "$TEST_DIR/chronyc.out" && \
test_ok || test_error
[ $? -ne 0 ] && ret=1
if [ -f "$TEST_DIR/chronyc.valgrind" ]; then
test_message 2 0 "checking valgrind report"
! grep -q 'ERROR SUMMARY: [^0]' "$TEST_DIR/chronyc.valgrind" && \
test_ok || test_bad
[ $? -ne 0 ] && ret=1
fi
return $ret
}
# Compare chronyc output with specified pattern
check_chronyc_output() {
local pattern=$1
test_message 1 0 "checking chronyc output"
[[ "$(cat "$TEST_DIR/chronyc.out")" =~ $pattern ]] && test_ok || test_bad
}