While developing the luci package, I came across a strange feature of the demon rpcd.
To demonstrate this strange behavior, I asked ChatGPT to write a simple script to test the operation of rpcd daemon:
#!/bin/sh
EXE_DIR=$( cd "$(dirname "$0")" 2>/dev/null && pwd )
RPC_URL="http://127.0.0.1/ubus"
usage() {
echo "Usage:"
echo " $0 -p <password> -c <command> [-s]"
echo
echo "Options:"
echo " -p root password"
echo " -c command string (no quotes inside!)"
echo " -s run via 'sh -c'"
echo " -T run test with N iterations"
exit 1
}
PASSWORD=""
CMDLINE=""
USE_SH=0
TEST_MAX=0
while getopts "p:c:sT:" opt; do
case "$opt" in
p) PASSWORD="$OPTARG" ;;
c) CMDLINE="$OPTARG" ;;
s) USE_SH=1 ;;
T) TEST_MAX="$OPTARG" ;;
*) usage ;;
esac
done
[ "$PASSWORD" = "@" ] && PASSWORD=xxxxxxxxxxx
[ -z "$PASSWORD" ] && usage
[ -z "$CMDLINE" ] && usage
LOGIN_JSON=$( cat <<EOF
{
"jsonrpc": "2.0",
"id": 1,
"method": "call",
"params": [
"00000000000000000000000000000000",
"session",
"login",
{
"username": "root",
"password": "$PASSWORD"
}
]
}
EOF
)
LOGIN_REPLY=$( curl -s "$RPC_URL" -H 'Content-Type: application/json' -d "$LOGIN_JSON" )
SESSION=$( echo "$LOGIN_REPLY" | sed -n 's/.*"ubus_rpc_session"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' )
if [ -z "$SESSION" ]; then
echo "ERROR: login failed"
echo "$LOGIN_REPLY"
exit 2
fi
if [ "$USE_SH" -eq 1 ]; then
# sh -c "string"
CMD="/bin/sh"
PARAMS_JSON="\"-c\",\"$CMDLINE\""
else
# split by spaces
set -- $CMDLINE
CMD="$1"
shift
PARAMS_JSON=""
for a in "$@"; do
PARAMS_JSON="$PARAMS_JSON,\"$a\""
done
PARAMS_JSON="${PARAMS_JSON#,}"
fi
EXEC_JSON=$( cat <<EOF
{
"jsonrpc": "2.0",
"id": 2,
"method": "call",
"params": [
"$SESSION",
"file",
"exec",
{
"command": "$CMD",
"params": [ $PARAMS_JSON ]
}
]
}
EOF
)
TEST_ITER=0
while true; do
TEST_ITER=$((TEST_ITER + 1))
TEST_RESP=$( curl -s "$RPC_URL" -H 'Content-Type: application/json' -d "$EXEC_JSON" )
echo "$TEST_RESP"
if ! echo "$TEST_RESP" | grep -q '"result":\[0' ; then
echo "ERROR: response with error code. Iteration = $TEST_ITER"
exit 1
fi
[ $TEST_ITER -ge $TEST_MAX ] && break
done
echo ""
To test, you need to run the following command:
./rpctest.sh -p <password> -s -c 'start-stop-daemon -S -b -p /tmp/rpctest.pid -x sleep -- 1 '
So, on a 4-core mt7986A, this script always runs instantly and produces the following result:
{"jsonrpc":"2.0","id":2,"result":[0,{"code":0}]}
But on a single-core mt7621, in 30% of cases you have to wait more than 30 seconds for a response and get this response:
{"jsonrpc":"2.0","id":2,"result":[7]}
But this command always runs successfully:
> ./rpctest.sh -p @ -s -c 'start-stop-daemon -S -b -p /tmp/rpctest.pid -x sleep -- 1 ; awk -V '
{"jsonrpc":"2.0","id":2,"result":[0,{"code":0,"stdout":"GNU Awk 5.3.2, API 4.0\nCopyright (C) 1989, 1991-2025 Free Software Foundation.\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see http://www.gnu.org/licenses/.\n"}]}
But this command will no longer always be executed successfully:
> ./rpctest.sh -p @ -s -c 'start-stop-daemon -S -b -p /tmp/rpctest.pid -x sleep -- 1 ; echo 123 '
{"jsonrpc":"2.0","id":2,"result":[7]}
> ./rpctest.sh -p @ -s -c 'start-stop-daemon -S -b -p /tmp/rpctest.pid -x sleep -- 1 ; echo 123 '
{"jsonrpc":"2.0","id":2,"result":[0,{"code":0,"stdout":"123\n"}]}
Or same:
> ./rpctest.sh -p @ -s -c '( exec >/dev/null 2>&1 ; sleep 1 ) &'
{"jsonrpc":"2.0","id":2,"result":[7]}
> ./rpctest.sh -p @ -s -c '( exec >/dev/null 2>&1 ; sleep 1 ) &'
{"jsonrpc":"2.0","id":2,"result":[0,{"code":0}]}