sd-varlink: Return empty object for missing params

Previously, sd-varlink was inconsistent about what it did with
empty/missing/null parameters in a Varlink call or reply: when
dispatching to a callback, it would always construct an empty object and
hand over the empty object to the handler (good!). But when returning
the value directly (i.e. with sd_varlink_call), it would return NULL
when parameters were omitted and a JSON null when the paramerters were
null.

This inconsistency is a violation of UAPI.20, which states that
receivers of Varlink messages MUST treat omitted/null/empty parameters
on Varlink calls and replies equivalently, where senders only SHOULD
prefer to omit the parameters.

This commit makes sd-varlink normalize the parameters to an empty object
whenever it is null or omitted, which brings sd-varlink into compliance
with this rule in UAPI.20

(cherry picked from commit 34db99971e728e2c4f155c01d43d24f3fe313bef)
This commit is contained in:
Adrian Vovk
2026-10-16 17:24:52 +01:00
committed by Luca Boccassi
parent adc93a4ad5
commit ffcf226c7f
2 changed files with 38 additions and 20 deletions
+19 -20
View File
@@ -824,22 +824,30 @@ static int varlink_dispatch_disconnect(sd_varlink *v) {
return 1;
}
static int varlink_sanitize_incoming_parameters(sd_json_variant **v) {
static int varlink_normalize_incoming_parameters(sd_json_variant **framing, sd_json_variant **params) {
int r;
assert(v);
/* Convert NULL or JSON null to empty object for method handlers (backward compatibility) */
if (!*v || sd_json_variant_is_null(*v)) {
/* UAPI.20 specifies that receivers of Varlink messages MUST treat "parameters" identically no matter
* if it is missing, explicitly null, or an empty object. Here, we choose to normalize "parameters"
* to an empty object if it is missing or null, which makes sd_varlink behave predictably (i.e.
* sd_varlink_call and similar never return write NULL to ret_parameters if they return r >= 0) */
assert(params);
if (!*params || sd_json_variant_is_null(*params)) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *empty = NULL;
r = sd_json_variant_new_object(&empty, NULL, 0);
if (r < 0)
return r;
return json_variant_unref_and_replace(*v, empty);
}
/* Ensure we have an object */
if (!sd_json_variant_is_object(*v))
return -EINVAL;
if (framing) {
r = sd_json_variant_set_field(framing, "parameters", empty);
if (r < 0)
return r;
}
return json_variant_unref_and_replace(*params, empty);
}
return 0;
}
@@ -903,7 +911,7 @@ static int varlink_dispatch_reply(sd_varlink *v) {
if (error && FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES))
goto invalid;
r = varlink_sanitize_incoming_parameters(&parameters);
r = varlink_normalize_incoming_parameters(&v->current, &parameters);
if (r < 0)
goto invalid;
@@ -1269,7 +1277,7 @@ static int varlink_dispatch_method(sd_varlink *v) {
if (!method)
goto invalid;
r = varlink_sanitize_incoming_parameters(&parameters);
r = varlink_normalize_incoming_parameters(NULL, &parameters);
if (r < 0)
goto fail;
@@ -2355,15 +2363,6 @@ _public_ int sd_varlink_collect_full(
if (sd_json_variant_elements(collected) >= VARLINK_COLLECT_MAX)
return varlink_log_errno(v, SYNTHETIC_ERRNO(E2BIG), "Number of reply messages grew too large (%zu) while collecting.", sd_json_variant_elements(collected));
_cleanup_(sd_json_variant_unrefp) sd_json_variant *empty = NULL;
if (!p) {
r = sd_json_variant_new_array(&empty, /* array= */ NULL, /* n= */ 0);
if (r < 0)
return r;
p = empty;
}
r = sd_json_variant_append_array(&collected, p);
if (r < 0)
return varlink_log_errno(v, r, "Failed to append JSON object to array: %m");
@@ -380,5 +380,24 @@ echo "$result" | grep "untrusted data decompressed safely" >/dev/null
kill "$SERVE_PID" 2>/dev/null || true
wait "$SERVE_PID" 2>/dev/null || true
# Verify consistent handling of omitted/null/empty parameters
check_output_is_empty_object() {
REPLY="$1"
# This is a "Varlink server" that reads the incoming call, discards it, and
# directly sends the contents of $REPLY as the reply. This lets us exercise
# sd-varlink's client-side parsing of the Varlink call/reply framing
# shellcheck disable=SC2016
SERVE_PID=$(systemd-notify --fork -- \
systemd-socket-activate -l "$SERVE_SOCKET" --inetd -a -- \
bash -c 'read -r -d "" _; printf "%s\0" "$0"' "$REPLY")
varlinkctl call "unix:$SERVE_SOCKET" com.example.Test '{}' | grep "^{}$"
varlinkctl --exec call "unix:$SERVE_SOCKET" com.example.Test '{}' -- grep "^{}$"
kill "$SERVE_PID" 2>/dev/null || true
wait "$SERVE_PID" 2>/dev/null || true
}
check_output_is_empty_object '{}'
check_output_is_empty_object '{"parameters": null}'
check_output_is_empty_object '{"parameters": {}}'
rm -f "$SERVE_SOCKET"
rm -rf "$(dirname "$SERVE_SOCKET")" "$SERVE_TMPDIR"