From ffcf226c7f7bf0f4732f02a6540cbd1406b6fb7e Mon Sep 17 00:00:00 2001 From: Adrian Vovk Date: Fri, 25 Sep 2026 12:33:25 -0400 Subject: [PATCH] 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) --- src/libsystemd/sd-varlink/sd-varlink.c | 39 +++++++++++----------- test/units/TEST-74-AUX-UTILS.varlinkctl.sh | 19 +++++++++++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/libsystemd/sd-varlink/sd-varlink.c b/src/libsystemd/sd-varlink/sd-varlink.c index ebf881459a..574fb57b5d 100644 --- a/src/libsystemd/sd-varlink/sd-varlink.c +++ b/src/libsystemd/sd-varlink/sd-varlink.c @@ -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(¶meters); + r = varlink_normalize_incoming_parameters(&v->current, ¶meters); 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(¶meters); + r = varlink_normalize_incoming_parameters(NULL, ¶meters); 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"); diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh index 9b807810ee..bf4d4c66f6 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh @@ -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"