From 5e71f86dffcd87a68ccb9e11a661d931e0e995e7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:22:12 +0200 Subject: [PATCH 01/18] alloc-util: add realloc0() helper than is like realloc() but zero-initializes appended space --- src/basic/alloc-util.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/basic/alloc-util.h b/src/basic/alloc-util.h index 156c973e69..3ef834acee 100644 --- a/src/basic/alloc-util.h +++ b/src/basic/alloc-util.h @@ -254,4 +254,24 @@ static inline void free_many_charp(char **c, size_t n) { free_many((void**) c, n); } +_alloc_(2) static inline void *realloc0(void *p, size_t new_size) { + size_t old_size; + void *q; + + /* Like realloc(), but initializes anything appended to zero */ + + old_size = MALLOC_SIZEOF_SAFE(p); + + q = realloc(p, new_size); + if (!q) + return NULL; + + new_size = MALLOC_SIZEOF_SAFE(q); /* Update with actually allocated space */ + + if (new_size > old_size) + memset((uint8_t*) q + old_size, 0, new_size - old_size); + + return q; +} + #include "memory-util.h" From 8240fa920d7ed8ab21fb344a463d9a38e5f34cd6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:23:58 +0200 Subject: [PATCH 02/18] varlink: add new helper that reports whether connection is idle This is useful to allow event loops to run exactly as long as there's something to do but not longer. --- src/shared/varlink.c | 9 +++++++++ src/shared/varlink.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index d2f649485b..5af2c2c363 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -1282,6 +1282,15 @@ int varlink_wait(Varlink *v, usec_t timeout) { return 1; } +int varlink_is_idle(Varlink *v) { + assert_return(v, -EINVAL); + + /* Returns true if there's nothing pending on the connection anymore, i.e. we processed all incoming + * or outgoing messages fully, or finished disconnection */ + + return IN_SET(v->state, VARLINK_DISCONNECTED, VARLINK_IDLE_CLIENT, VARLINK_IDLE_SERVER); +} + int varlink_get_fd(Varlink *v) { assert_return(v, -EINVAL); diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 0984222c96..5eb4676dc8 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -71,6 +71,8 @@ sd_event *varlink_get_event(Varlink *v); int varlink_process(Varlink *v); int varlink_wait(Varlink *v, usec_t timeout); +int varlink_is_idle(Varlink *v); + int varlink_flush(Varlink *v); int varlink_close(Varlink *v); From e50b2a93d66eff9b41999d38b8ed5eb647e2d74c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:29:41 +0200 Subject: [PATCH 03/18] varlink: add IDL/introspection logic --- src/shared/meson.build | 1 + src/shared/varlink-idl.c | 1582 ++++++++++++++++++++++++++++++++++++++ src/shared/varlink-idl.h | 158 ++++ 3 files changed, 1741 insertions(+) create mode 100644 src/shared/varlink-idl.c create mode 100644 src/shared/varlink-idl.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 3df178bfa1..da32e1435d 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -166,6 +166,7 @@ shared_sources = files( 'userdb-dropin.c', 'userdb.c', 'varlink.c', + 'varlink-idl.c', 'verb-log-control.c', 'verbs.c', 'vlan-util.c', diff --git a/src/shared/varlink-idl.c b/src/shared/varlink-idl.c new file mode 100644 index 0000000000..157474facf --- /dev/null +++ b/src/shared/varlink-idl.c @@ -0,0 +1,1582 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memstream-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "varlink-idl.h" +#include "set.h" + +enum { + COLOR_SYMBOL_TYPE, /* interface, method, type, error */ + COLOR_FIELD_TYPE, /* string, bool, … */ + COLOR_IDENTIFIER, + COLOR_MARKS, /* [], ->, ?, … */ + COLOR_RESET, + _COLOR_MAX, +}; + +static int varlink_idl_format_all_fields(FILE *f, const VarlinkSymbol *symbol, VarlinkFieldDirection direction, const char *indent, const char *const colors[static _COLOR_MAX]); + +static int varlink_idl_format_enum_values( + FILE *f, + const VarlinkSymbol *symbol, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + bool first = true; + + assert(f); + assert(symbol); + assert(symbol->symbol_type == VARLINK_ENUM_TYPE); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (first) { + first = false; + fputs("(\n", f); + } else + fputs(",\n", f); + + fputs(strempty(indent), f); + fputs("\t", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(field->name, f); + fputs(colors[COLOR_RESET], f); + } + + if (first) + fputs("()", f); + else { + fputs("\n", f); + fputs(strempty(indent), f); + fputs(")", f); + } + + return 0; +} + +static int varlink_idl_format_field( + FILE *f, + const VarlinkField *field, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + assert(f); + assert(field); + + fputs(strempty(indent), f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(field->name, f); + fputs(colors[COLOR_RESET], f); + fputs(": ", f); + + if (FLAGS_SET(field->field_flags, VARLINK_NULLABLE)) { + fputs(colors[COLOR_MARKS], f); + fputs("?", f); + fputs(colors[COLOR_RESET], f); + } + + switch (field->field_flags & (VARLINK_MAP|VARLINK_ARRAY)) { + + case VARLINK_MAP: + fputs(colors[COLOR_MARKS], f); + fputs("[", f); + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("string", f); + fputs(colors[COLOR_MARKS], f); + fputs("]", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_ARRAY: + fputs(colors[COLOR_MARKS], f); + fputs("[]", f); + fputs(colors[COLOR_RESET], f); + break; + + case 0: + break; + + default: + assert_not_reached(); + } + + switch (field->field_type) { + + case VARLINK_BOOL: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("bool", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_INT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("int", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_FLOAT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("float", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_STRING: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("string", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_OBJECT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("object", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_NAMED_TYPE: + fputs(colors[COLOR_IDENTIFIER], f); + fputs(ASSERT_PTR(field->named_type), f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_STRUCT: + return varlink_idl_format_all_fields(f, ASSERT_PTR(field->symbol), VARLINK_REGULAR, indent, colors); + + case VARLINK_ENUM: + return varlink_idl_format_enum_values(f, ASSERT_PTR(field->symbol), indent, colors); + + default: + assert_not_reached(); + } + + return 0; +} + +static int varlink_idl_format_all_fields( + FILE *f, + const VarlinkSymbol *symbol, + VarlinkFieldDirection filter_direction, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + _cleanup_free_ char *indent2 = NULL; + bool first = true; + int r; + + assert(f); + assert(symbol); + assert(IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_METHOD, VARLINK_ERROR)); + + indent2 = strjoin(strempty(indent), "\t"); + if (!indent2) + return -ENOMEM; + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (field->field_direction != filter_direction) + continue; + + if (first) { + first = false; + fputs("(\n", f); + } else + fputs(",\n", f); + + r = varlink_idl_format_field(f, field, indent2, colors); + if (r < 0) + return r; + } + + if (first) + fputs("()", f); + else { + fputs("\n", f); + fputs(strempty(indent), f); + fputs(")", f); + } + + return 0; +} + +static int varlink_idl_format_symbol( + FILE *f, + const VarlinkSymbol *symbol, + const char *const colors[static _COLOR_MAX]) { + int r; + + assert(f); + assert(symbol); + + switch (symbol->symbol_type) { + + case VARLINK_ENUM_TYPE: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("type ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_enum_values(f, symbol, /* indent= */ NULL, colors); + break; + + case VARLINK_STRUCT_TYPE: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("type ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors); + break; + + case VARLINK_METHOD: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("method ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_INPUT, /* indent= */ NULL, colors); + if (r < 0) + return r; + + fputs(colors[COLOR_MARKS], f); + fputs(" -> ", f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_OUTPUT, /* indent= */ NULL, colors); + break; + + case VARLINK_ERROR: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("error ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + fputs("\n", f); + return 0; +} + +static int varlink_idl_format_all_symbols( + FILE *f, + const VarlinkInterface *interface, + VarlinkSymbolType filter_type, + const char *const colors[static _COLOR_MAX]) { + + int r; + + assert(f); + assert(interface); + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + + if ((*symbol)->symbol_type != filter_type) + continue; + + fputs("\n", f); + + r = varlink_idl_format_symbol(f, *symbol, colors); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface) { + static const char* const color_table[_COLOR_MAX] = { + [COLOR_SYMBOL_TYPE] = ANSI_HIGHLIGHT_GREEN, + [COLOR_FIELD_TYPE] = ANSI_HIGHLIGHT_BLUE, + [COLOR_IDENTIFIER] = ANSI_NORMAL, + [COLOR_MARKS] = ANSI_HIGHLIGHT_MAGENTA, + [COLOR_RESET] = ANSI_NORMAL, + }; + + static const char* const color_off[_COLOR_MAX] = { + "", "", "", "", "", + }; + + int r; + + assert(interface); + + if (!f) + f = stdout; + + if (use_colors < 0) + use_colors = colors_enabled(); + + const char *const *colors = use_colors ? color_table : color_off; + + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("interface ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(ASSERT_PTR(interface->name), f); + fputs(colors[COLOR_RESET], f); + fputs("\n", f); + + for (VarlinkSymbolType t = 0; t < _VARLINK_SYMBOL_TYPE_MAX; t++) { + r = varlink_idl_format_all_symbols(f, interface, t, colors); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_format(const VarlinkInterface *interface, char **ret) { + _cleanup_(memstream_done) MemStream memstream = {}; + int r; + + if (!memstream_init(&memstream)) + return -errno; + + r = varlink_idl_dump(memstream.f, /* use_colors= */ false, interface); + if (r < 0) + return r; + + return memstream_finalize(&memstream, ret, NULL); +} + +static VarlinkSymbol *varlink_symbol_free(VarlinkSymbol *symbol) { + if (!symbol) + return NULL; + + /* See comment in varlink_interface_free() regarding the casting away of `const` */ + + free((char*) symbol->name); + + for (size_t i = 0; symbol->fields[i].field_type != _VARLINK_FIELD_TYPE_END_MARKER; i++) { + VarlinkField *field = symbol->fields + i; + + free((void*) field->name); + free((void*) field->named_type); + + /* The symbol pointer might either point to a named symbol, in which case that symbol is + * owned by the interface, or by an anomyous symbol, in which case it is owned by us, and we + * need to free it */ + if (field->symbol && field->field_type != VARLINK_NAMED_TYPE) + varlink_symbol_free((VarlinkSymbol*) field->symbol); + } + + return mfree(symbol); +} + +VarlinkInterface* varlink_interface_free(VarlinkInterface *interface) { + if (!interface) + return NULL; + + /* So here's the thing: in most cases we want that users of this define their interface descriptions + * in C code, and hence the definitions are constant and immutable during the life-cycle of the + * system. Because of that we define all structs with const* pointers. It makes it very nice and + * straight-forward to populate these structs with literal C strings. However, in some not so common + * cases we also want to allocate these structures dynamically on the heap, when parsing interface + * descriptions. But given this should be the exceptional and not the common case, we decided to + * simple cast away the 'const' where needed, even if it is ugly. */ + + free((char*) interface->name); + + for (size_t i = 0; interface->symbols[i]; i++) + varlink_symbol_free((VarlinkSymbol*) interface->symbols[i]); + + return mfree(interface); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkSymbol*, varlink_symbol_free); + +static int varlink_interface_realloc(VarlinkInterface **interface, size_t n_symbols) { + VarlinkInterface *n; + + assert(interface); + + n_symbols ++; /* Space for trailing NULL end marker symbol */ + + /* Overflow check */ + if (n_symbols > (SIZE_MAX - offsetof(VarlinkInterface, symbols)) / sizeof(VarlinkSymbol*)) + return -ENOMEM; + + n = realloc0(*interface, offsetof(VarlinkInterface, symbols) + sizeof(VarlinkSymbol*) * n_symbols); + if (!n) + return -ENOMEM; + + *interface = n; + return 0; +} + +static int varlink_symbol_realloc(VarlinkSymbol **symbol, size_t n_fields) { + VarlinkSymbol *n; + + assert(symbol); + + n_fields ++; /* Space for trailing end marker field */ + + /* Overflow check */ + if (n_fields > (SIZE_MAX - offsetof(VarlinkSymbol, fields)) / sizeof(VarlinkField)) + return -ENOMEM; + + n = realloc0(*symbol, offsetof(VarlinkSymbol, fields) + sizeof(VarlinkField) * n_fields); + if (!n) + return -ENOMEM; + + *symbol = n; + return 0; +} + +#define VALID_CHARS_IDENTIFIER ALPHANUMERICAL "_" +#define VALID_CHARS_RESERVED LOWERCASE_LETTERS +#define VALID_CHARS_INTERFACE_NAME ALPHANUMERICAL ".-" + +static void advance_line_column(const char *p, size_t n, unsigned *line, unsigned *column) { + + assert(p); + assert(line); + assert(column); + + for (; n > 0; p++, n--) { + + if (*p == '\n') { + (*line)++; + *column = 1; + } else + (*column)++; + } +} + +static size_t token_match( + const char *p, + const char *allowed_delimiters, + const char *allowed_chars) { + + /* Checks if the string p begins either with one of the token characters in allowed_delimiters or + * with a string consisting of allowed_chars. */ + + assert(p); + + if (allowed_delimiters && strchr(allowed_delimiters, *p)) + return 1; + + if (!allowed_chars) + return 0; + + return strspn(p, allowed_chars); +} + +static int varlink_idl_subparse_token( + const char **p, + unsigned *line, + unsigned *column, + const char *allowed_delimiters, + const char *allowed_chars, + char **ret_token) { + + _cleanup_free_ char *t = NULL; + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + assert(ret_token); + + if (**p == '\0') { /* eof */ + *ret_token = NULL; + return 0; + } + + l = token_match(*p, allowed_delimiters, allowed_chars); + + /* No token of the permitted character set found? Then let's try to skip over whitespace and try again */ + if (l == 0) { + size_t ll; + + ll = strspn(*p, WHITESPACE); + advance_line_column(*p, ll, line, column); + *p += ll; + + if (**p == '\0') { /* eof */ + *ret_token = NULL; + return 0; + } + + l = token_match(*p, allowed_delimiters, allowed_chars); + if (l == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find token of allowed chars '%s' or allowed delimiters '%s'.", strempty(allowed_chars), strempty(allowed_delimiters)); + } + + t = strndup(*p, l); + if (!t) + return -ENOMEM; + + advance_line_column(*p, l, line, column); + *p += l; + + *ret_token = TAKE_PTR(t); + return 1; +} + +static int varlink_idl_subparse_comment( + const char **p, + unsigned *line, + unsigned *column) { + + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + + l = strcspn(*p, NEWLINE); + advance_line_column(*p, l + 1, line, column); + *p += l; + + return 1; +} + +static int varlink_idl_subparse_whitespace( + const char **p, + unsigned *line, + unsigned *column) { + + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + + l = strspn(*p, WHITESPACE); + advance_line_column(*p, l, line, column); + *p += l; + + return 1; +} + +static int varlink_idl_subparse_struct_or_enum(const char **p, unsigned *line, unsigned *column, VarlinkSymbol **symbol, size_t *n_fields, VarlinkFieldDirection direction); + +static int varlink_idl_subparse_field_type( + const char **p, + unsigned *line, + unsigned *column, + VarlinkField *field) { + + size_t l; + int r; + + assert(p); + assert(*p); + assert(line); + assert(field); + + r = varlink_idl_subparse_whitespace(p, line, column); + if (r < 0) + return r; + + if (startswith(*p, "?")) { + field->field_flags |= VARLINK_NULLABLE; + l = 1; + } else { + field->field_flags &= ~VARLINK_NULLABLE; + l = 0; + } + + advance_line_column(*p, l, line, column); + *p += l; + + if (startswith(*p, "[]")) { + l = 2; + field->field_flags = (field->field_flags & ~VARLINK_MAP) | VARLINK_ARRAY; + } else if (startswith(*p, "[string]")) { + l = 8; + field->field_flags = (field->field_flags & ~VARLINK_ARRAY) | VARLINK_MAP; + } else { + l = 0; + field->field_flags = field->field_flags & ~(VARLINK_MAP | VARLINK_ARRAY); + } + + advance_line_column(*p, l, line, column); + *p += l; + + if (startswith(*p, "bool")) { + l = 4; + field->field_type = VARLINK_BOOL; + } else if (startswith(*p, "int")) { + l = 3; + field->field_type = VARLINK_INT; + } else if (startswith(*p, "float")) { + l = 5; + field->field_type = VARLINK_FLOAT; + } else if (startswith(*p, "string")) { + l = 6; + field->field_type = VARLINK_STRING; + } else if (startswith(*p, "object")) { + l = 6; + field->field_type = VARLINK_OBJECT; + } else if (**p == '(') { + _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL; + size_t n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID; + + r = varlink_idl_subparse_struct_or_enum( + p, + line, + column, + &symbol, + &n_fields, + VARLINK_REGULAR); + if (r < 0) + return r; + + if (symbol->symbol_type == VARLINK_STRUCT_TYPE) + field->field_type = VARLINK_STRUCT; + else { + assert(symbol->symbol_type == VARLINK_ENUM_TYPE); + field->field_type = VARLINK_ENUM; + } + + field->symbol = TAKE_PTR(symbol); + l = 0; + } else { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token(p, line, column, /* valid_tokens= */ NULL, VALID_CHARS_IDENTIFIER, &token); + if (r < 0) + return r; + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + field->named_type = TAKE_PTR(token); + field->field_type = VARLINK_NAMED_TYPE; + l = 0; + } + + advance_line_column(*p, l, line, column); + *p += l; + + return 0; +} + +static int varlink_idl_subparse_struct_or_enum( + const char **p, + unsigned *line, + unsigned *column, + VarlinkSymbol **symbol, + size_t *n_fields, + VarlinkFieldDirection direction) { + + enum { + STATE_OPEN, + STATE_NAME, + STATE_COLON, + STATE_COMMA, + STATE_DONE, + } state = STATE_OPEN; + _cleanup_free_ char *field_name = NULL; + const char *allowed_delimiters = "(", *allowed_chars = NULL; + int r; + + assert(p); + assert(*p); + assert(line); + assert(column); + assert(symbol); + assert(*symbol); + assert(n_fields); + + while (state != STATE_DONE) { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token( + p, + line, + column, + allowed_delimiters, + allowed_chars, + &token); + if (r < 0) + return r; + + switch (state) { + + case STATE_OPEN: + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (!streq(token, "(")) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + state = STATE_NAME; + allowed_delimiters = ")"; + allowed_chars = VALID_CHARS_IDENTIFIER; + break; + + case STATE_NAME: + assert(!field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(p, line, column); + if (r < 0) + return r; + } else if (streq(token, ")")) + state = STATE_DONE; + else { + field_name = TAKE_PTR(token); + state = STATE_COLON; + allowed_delimiters = ":,)"; + allowed_chars = NULL; + } + + break; + + case STATE_COLON: + assert(field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + if (streq(token, ":")) { + VarlinkField *field; + + if ((*symbol)->symbol_type < 0) + (*symbol)->symbol_type = VARLINK_STRUCT_TYPE; + if ((*symbol)->symbol_type == VARLINK_ENUM_TYPE) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Enum with struct fields, refusing: %m", *line, *column); + + r = varlink_symbol_realloc(symbol, *n_fields + 1); + if (r < 0) + return r; + + field = (*symbol)->fields + (*n_fields)++; + *field = (VarlinkField) { + .name = TAKE_PTR(field_name), + .field_type = _VARLINK_FIELD_TYPE_INVALID, + .field_direction = direction, + }; + + r = varlink_idl_subparse_field_type(p, line, column, field); + if (r < 0) + return r; + + state = STATE_COMMA; + allowed_delimiters = ",)"; + allowed_chars = NULL; + + } else if (STR_IN_SET(token, ",", ")")) { + VarlinkField *field; + + if ((*symbol)->symbol_type < 0) + (*symbol)->symbol_type = VARLINK_ENUM_TYPE; + if ((*symbol)->symbol_type != VARLINK_ENUM_TYPE) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Struct with enum fields, refusing: %m", *line, *column); + + r = varlink_symbol_realloc(symbol, *n_fields + 1); + if (r < 0) + return r; + + field = (*symbol)->fields + (*n_fields)++; + *field = (VarlinkField) { + .name = TAKE_PTR(field_name), + .field_type = VARLINK_ENUM_VALUE, + }; + + if (streq(token, ",")) { + state = STATE_NAME; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else { + assert(streq(token, ")")); + state = STATE_DONE; + } + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + break; + + case STATE_COMMA: + assert(!field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, ",")) { + state = STATE_NAME; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, ")")) + state = STATE_DONE; + else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + break; + + default: + assert_not_reached(); + } + } + + return 0; +} + +static int varlink_idl_resolve_symbol_types(VarlinkInterface *interface, VarlinkSymbol *symbol) { + assert(interface); + assert(symbol); + + for (VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + const VarlinkSymbol *found; + + if (field->field_type != VARLINK_NAMED_TYPE) + continue; + + if (field->symbol) /* Already resolved */ + continue; + + if (!field->named_type) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Named type field lacking a type name."); + + found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->named_type); + if (!found) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Failed to find type '%s'.", field->named_type); + + if (!IN_SET(found->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE)) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Symbol '%s' is referenced as type but is not a type.", field->named_type); + + field->symbol = found; + } + + return 0; +} + +static int varlink_idl_resolve_types(VarlinkInterface *interface) { + int r; + + assert(interface); + + for (VarlinkSymbol **symbol = (VarlinkSymbol**) interface->symbols; *symbol; symbol++) { + r = varlink_idl_resolve_symbol_types(interface, *symbol); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_parse( + const char *text, + unsigned *line, + unsigned *column, + VarlinkInterface **ret) { + + _cleanup_(varlink_interface_freep) VarlinkInterface *interface = NULL; + _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL; + enum { + STATE_PRE_INTERFACE, + STATE_INTERFACE, + STATE_PRE_SYMBOL, + STATE_METHOD, + STATE_METHOD_NAME, + STATE_METHOD_ARROW, + STATE_TYPE, + STATE_TYPE_NAME, + STATE_ERROR, + STATE_ERROR_NAME, + STATE_DONE, + } state = STATE_PRE_INTERFACE; + const char *allowed_delimiters = "#", *allowed_chars = VALID_CHARS_RESERVED; + size_t n_symbols = 0, n_fields = 1; + unsigned _line = 0, _column = 1; + const char **p = &text; + int r; + + if (!line) + line = &_line; + if (!column) + column = &_column; + + while (state != STATE_DONE) { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token( + p, + line, + column, + allowed_delimiters, + allowed_chars, + &token); + if (r < 0) + return r; + + switch (state) { + + case STATE_PRE_INTERFACE: + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(&text, line, column); + if (r < 0) + return r; + } else if (streq(token, "interface")) { + state = STATE_INTERFACE; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_INTERFACE_NAME; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + break; + + case STATE_INTERFACE: + assert(!interface); + assert(n_symbols == 0); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + r = varlink_interface_realloc(&interface, n_symbols); + if (r < 0) + return r; + + interface->name = TAKE_PTR(token); + state = STATE_PRE_SYMBOL; + allowed_delimiters = "#"; + allowed_chars = VALID_CHARS_RESERVED; + break; + + case STATE_PRE_SYMBOL: + if (!token) { + state = STATE_DONE; + break; + } + + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(&text, line, column); + if (r < 0) + return r; + } else if (streq(token, "method")) { + state = STATE_METHOD; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, "type")) { + state = STATE_TYPE; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, "error")) { + state = STATE_ERROR; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + break; + + case STATE_METHOD: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = VARLINK_METHOD; + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_INPUT); + if (r < 0) + return r; + + state = STATE_METHOD_ARROW; + allowed_chars = "->"; + break; + + case STATE_METHOD_ARROW: + assert(symbol); + + if (!streq(token, "->")) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_OUTPUT); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + case STATE_TYPE: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID; /* don't know yet if enum or struct, will be fileld in by varlink_idl_subparse_struct_or_enum() */ + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + case STATE_ERROR: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = VARLINK_ERROR; + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + default: + assert_not_reached(); + } + } + + r = varlink_idl_resolve_types(interface); + if (r < 0) + return r; + + *ret = TAKE_PTR(interface); + return 0; +} + +bool varlink_idl_field_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* Field names may start with lower or uppercase char, but no numerals or underscore */ + if (!strchr(LETTERS, name[0])) + return false; + + /* Otherwise fields may be alphanumerical or underscore, but no two underscore may immediately follow + * each other or be trailing */ + bool underscore = false; + for (const char *c = name + 1; *c; c++) { + if (*c == '_') { + if (underscore) + return false; + + underscore = true; + continue; + } + + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + underscore = false; + } + + if (underscore) + return false; + + return true; +} + +bool varlink_idl_symbol_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* We might want to reference VARLINK_STRUCT_TYPE and VARLINK_ENUM_TYPE symbols where we also + * reference native types, hence make sure the native type names are refused as symbol names. */ + if (STR_IN_SET(name, "bool", "int", "float", "string", "object")) + return false; + + /* Symbols must be named with an uppercase letter as first character */ + if (!strchr(UPPERCASE_LETTERS, name[0])) + return false; + + for (const char *c = name + 1; *c; c++) + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + return true; +} + +bool varlink_idl_interface_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* Interface names must start with a letter, uppercase or lower case, but nothing else */ + if (!strchr(LETTERS, name[0])) + return false; + + /* Otherwise it may be a series of non-empty dot separated labels, which are alphanumerical and may + * contain single dashes in the middle */ + bool dot = false, dash = false; + for (const char *c = name + 1; *c; c++) { + switch (*c) { + + case '.': + if (dot || dash) + return false; + + dot = true; + break; + + case '-': + if (dot || dash) + return false; + + dash = true; + break; + + default: + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + dot = dash = false; + } + } + + if (dot || dash) + return false; + + return true; +} + +static int varlink_idl_symbol_consistent(const VarlinkInterface *interface, const VarlinkSymbol *symbol, bool level); + +static int varlink_idl_field_consistent( + const VarlinkInterface *interface, + const VarlinkSymbol *symbol, + const VarlinkField *field, + bool level) { + + const char *symbol_name; + int r; + + assert(interface); + assert(symbol); + assert(field); + assert(field->name); + + symbol_name = symbol->name ?: ""; + + if (field->field_type <= 0 || field->field_type >= _VARLINK_FIELD_TYPE_MAX) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field type for '%s' in symbol '%s' is not valid, refusing.", field->name, symbol_name); + + if (field->field_type == VARLINK_ENUM_VALUE) { + + if (symbol->symbol_type != VARLINK_ENUM_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field type for '%s' in non-enum symbol '%s', refusing.", field->name, symbol_name); + + if (field->field_flags != 0) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field '%s' in symbol '%s' has non-zero flags set, refusing.", field->name, symbol_name); + } else { + if (symbol->symbol_type == VARLINK_ENUM_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Non-enum field type for '%s' in enum symbol '%s', refusing.", field->name, symbol_name); + + if (!IN_SET(field->field_flags & ~VARLINK_NULLABLE, 0, VARLINK_ARRAY, VARLINK_MAP)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Flags of field '%s' in symbol '%s' is invalid, refusing.", field->name, symbol_name); + } + + if (symbol->symbol_type != VARLINK_METHOD) { + if (field->field_direction != VARLINK_REGULAR) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in non-method symbol '%s' not regular, refusing.", field->name, symbol_name); + } else { + if (!IN_SET(field->field_direction, VARLINK_INPUT, VARLINK_OUTPUT)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in method symbol '%s' is not input or output, refusing.", field->name, symbol_name); + } + + if (field->symbol) { + if (!IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name); + + if (field->field_type == VARLINK_NAMED_TYPE) { + const VarlinkSymbol *found; + + if (!field->symbol->name || !field->named_type || !streq(field->symbol->name, field->named_type)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol name and named type of field '%s' in symbol '%s' do do not match, refusing.", field->name, symbol_name); + + /* If this is a named type, then check if it's properly part of the interface */ + found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->symbol->name); + if (!found) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not part of the interface, refusing.", field->name, symbol_name); + + if (!IN_SET(found->symbol_type, VARLINK_ENUM_TYPE, VARLINK_STRUCT_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not a type, refusing.", field->name, symbol_name); + } else { + /* If this is an anonymous type, then we recursively check if it's consistent, since + * it's not part of the interface, and hence we won't validate it from there. */ + + r = varlink_idl_symbol_consistent(interface, field->symbol, level); + if (r < 0) + return r; + } + + } else { + if (IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name); + + if (field->named_type) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Unresolved symbol in field '%s' in symbol '%s', refusing.", field->name, symbol_name); + } + + if (field->named_type) { + if (field->field_type != VARLINK_NAMED_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Named type set for field '%s' in symbol '%s' but not a named type field, refusing.", field->name, symbol_name); + } else { + if (field->field_type == VARLINK_NAMED_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No named type set for field '%s' in symbol '%s' but field is a named type field, refusing.", field->name, symbol_name); + } + + return 0; +} + +static bool varlink_symbol_is_empty(const VarlinkSymbol *symbol) { + assert(symbol); + + return symbol->fields[0].field_type == _VARLINK_FIELD_TYPE_END_MARKER; +} + +static int varlink_idl_symbol_consistent( + const VarlinkInterface *interface, + const VarlinkSymbol *symbol, + bool level) { + + _cleanup_(set_freep) Set *input_set = NULL, *output_set = NULL; + const char *symbol_name; + int r; + + assert(interface); + assert(symbol); + + symbol_name = symbol->name ?: ""; + + if (symbol->symbol_type < 0 || symbol->symbol_type >= _VARLINK_SYMBOL_TYPE_MAX) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol type for '%s' is not valid, refusing.", symbol_name); + + if (IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE) && varlink_symbol_is_empty(symbol)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol '%s' is empty, refusing.", symbol_name); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + Set **name_set = field->field_direction == VARLINK_OUTPUT ? &output_set : &input_set; /* for the method case we need two separate sets, otherwise we use the same */ + + if (!varlink_idl_field_name_is_valid(field->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field name '%s' in symbol '%s' not valid, refusing.", field->name, symbol_name); + + if (set_contains(*name_set, field->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Field '%s' defined twice in symbol '%s', refusing.", field->name, symbol_name); + + if (set_ensure_put(name_set, &string_hash_ops, field->name) < 0) + return log_oom(); + + r = varlink_idl_field_consistent(interface, symbol, field, level); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_consistent(const VarlinkInterface *interface, bool level) { + _cleanup_(set_freep) Set *name_set = NULL; + int r; + + assert(interface); + + if (!varlink_idl_interface_name_is_valid(interface->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Interface name '%s' is not valid, refusing.", interface->name); + + for (const VarlinkSymbol *const *symbol = interface->symbols; *symbol; symbol++) { + + if (!varlink_idl_symbol_name_is_valid((*symbol)->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol name '%s' is not valid, refusing.", strempty((*symbol)->name)); + + if (set_contains(name_set, (*symbol)->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Symbol '%s' defined twice in interface, refusing.", (*symbol)->name); + + if (set_ensure_put(&name_set, &string_hash_ops, (*symbol)->name) < 0) + return log_oom(); + + r = varlink_idl_symbol_consistent(interface, *symbol, level); + if (r < 0) + return r; + } + + return 0; +} + +static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field); + +static int varlink_idl_validate_field_element_type(const VarlinkField *field, JsonVariant *v) { + assert(field); + + switch (field->field_type) { + + case VARLINK_STRUCT: + case VARLINK_ENUM: + case VARLINK_NAMED_TYPE: + return varlink_idl_validate_symbol(field->symbol, v, VARLINK_REGULAR, NULL); + + case VARLINK_BOOL: + if (!json_variant_is_boolean(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a bool, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_INT: + if (!json_variant_is_integer(v) && !json_variant_is_unsigned(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an int, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_FLOAT: + if (!json_variant_is_number(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a float, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_STRING: + if (!json_variant_is_string(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a string, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_OBJECT: + if (!json_variant_is_object(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name)); + + break; + + default: + assert_not_reached(); + } + + return 0; +} + +static int varlink_idl_validate_field(const VarlinkField *field, JsonVariant *v) { + int r; + + assert(field); + + if (!v || json_variant_is_null(v)) { + + if (!FLAGS_SET(field->field_flags, VARLINK_NULLABLE)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), "Mandatory field '%s' is null or missing on object, refusing.", strna(field->name)); + + } else if (FLAGS_SET(field->field_flags, VARLINK_ARRAY)) { + JsonVariant *i; + + if (!json_variant_is_array(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an array, but it is not, refusing.", strna(field->name)); + + JSON_VARIANT_ARRAY_FOREACH(i, v) { + r = varlink_idl_validate_field_element_type(field, i); + if (r < 0) + return r; + } + + } else if (FLAGS_SET(field->field_flags, VARLINK_MAP)) { + _unused_ const char *k; + JsonVariant *e; + + if (!json_variant_is_object(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name)); + + JSON_VARIANT_OBJECT_FOREACH(k, e, v) { + r = varlink_idl_validate_field_element_type(field, e); + if (r < 0) + return r; + } + } else { + + r = varlink_idl_validate_field_element_type(field, v); + if (r < 0) + return r; + } + + return 0; +} + +static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) { + int r; + + assert(symbol); + + if (!v) { + if (bad_field) + *bad_field = NULL; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Null object passed, refusing."); + } + + switch (symbol->symbol_type) { + + case VARLINK_ENUM_TYPE: { + bool found = false; + const char *s; + + if (!json_variant_is_string(v)) { + if (bad_field) + *bad_field = symbol->name; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-string to enum field '%s', refusing.", strna(symbol->name)); + } + + assert_se(s = json_variant_string(v)); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + assert(field->field_type == VARLINK_ENUM_VALUE); + + if (streq_ptr(field->name, s)) { + found = true; + break; + } + } + + if (!found) { + if (bad_field) + *bad_field = s; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed unrecognized string '%s' to enum field '%s', refusing.", s, strna(symbol->name)); + } + + break; + } + + case VARLINK_STRUCT_TYPE: + case VARLINK_METHOD: + case VARLINK_ERROR: { + if (!json_variant_is_object(v)) { + if (bad_field) + *bad_field = symbol->name; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-object to field '%s', refusing.", strna(symbol->name)); + } + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (field->field_direction != direction) + continue; + + r = varlink_idl_validate_field(field, json_variant_by_key(v, field->name)); + if (r < 0) { + if (bad_field) + *bad_field = field->name; + return r; + } + } + + _unused_ JsonVariant *e; + const char *name; + JSON_VARIANT_OBJECT_FOREACH(name, e, v) { + if (!varlink_idl_find_field(symbol, name)) { + if (bad_field) + *bad_field = name; + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Field '%s' not defined for object, refusing.", name); + } + } + + break; + } + + default: + assert_not_reached(); + } + + return 1; /* validated */ +} + +static int varlink_idl_validate_method(const VarlinkSymbol *method, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) { + assert(IN_SET(direction, VARLINK_INPUT, VARLINK_OUTPUT)); + + if (!method) + return 0; /* Can't validate */ + if (method->symbol_type != VARLINK_METHOD) + return -EBADMSG; + + return varlink_idl_validate_symbol(method, v, direction, bad_field); +} + +int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) { + return varlink_idl_validate_method(method, v, VARLINK_INPUT, bad_field); +} + +int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) { + return varlink_idl_validate_method(method, v, VARLINK_OUTPUT, bad_field); +} + +int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field) { + if (!error) + return 0; /* Can't validate */ + if (error->symbol_type != VARLINK_ERROR) + return -EBADMSG; + + return varlink_idl_validate_symbol(error, v, VARLINK_REGULAR, bad_field); +} + +const VarlinkSymbol* varlink_idl_find_symbol( + const VarlinkInterface *interface, + VarlinkSymbolType type, + const char *name) { + + assert(interface); + assert(type < _VARLINK_SYMBOL_TYPE_MAX); + + if (isempty(name)) + return NULL; + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + if (type >= 0 && (*symbol)->symbol_type != type) + continue; + + if (streq_ptr((*symbol)->name, name)) + return *symbol; + } + + return NULL; +} + +const VarlinkField* varlink_idl_find_field( + const VarlinkSymbol *symbol, + const char *name) { + + assert(symbol); + + if (isempty(name)) + return NULL; + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) + if (streq_ptr(field->name, name)) + return field; + + return NULL; +} diff --git a/src/shared/varlink-idl.h b/src/shared/varlink-idl.h new file mode 100644 index 0000000000..3a7075e76f --- /dev/null +++ b/src/shared/varlink-idl.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include +#include + +#include "json.h" +#include "macro.h" + +/* This implements the Varlink Interface Definition Language ("Varlink IDL"), + * i.e. https://varlink.org/Interface-Definition + * + * Primarily allows encoding static interface definitions in C code, that can be converted to the textual IDL + * format on-the-fly. Can also parse the textual format back to C structures. Validates the interface + * definitions for internal consistency and validates JSON objects against the interface definitions. */ + +typedef enum VarlinkSymbolType { + VARLINK_ENUM_TYPE, + VARLINK_STRUCT_TYPE, + VARLINK_METHOD, + VARLINK_ERROR, + _VARLINK_SYMBOL_TYPE_MAX, + _VARLINK_SYMBOL_TYPE_INVALID = -EINVAL, +} VarlinkSymbolType; + +typedef enum VarlinkFieldType { + _VARLINK_FIELD_TYPE_END_MARKER = 0, /* zero type means: this is the last entry in the fields[] array of VarlinkSymbol */ + VARLINK_STRUCT, + VARLINK_ENUM, + VARLINK_NAMED_TYPE, + VARLINK_BOOL, + VARLINK_INT, + VARLINK_FLOAT, + VARLINK_STRING, + VARLINK_OBJECT, + VARLINK_ENUM_VALUE, + _VARLINK_FIELD_TYPE_MAX, + _VARLINK_FIELD_TYPE_INVALID = -EINVAL, +} VarlinkFieldType; + +typedef enum VarlinkFieldDirection { + VARLINK_REGULAR, + VARLINK_INPUT, + VARLINK_OUTPUT, + _VARLINK_FIELD_DIRECTION_MAX, + _VARLINK_FIELD_DIRECTION_INVALID = -EINVAL, +} VarlinkFieldDirection; + +typedef enum VarlinkFieldFlags { + VARLINK_ARRAY = 1 << 0, + VARLINK_MAP = 1 << 1, + VARLINK_NULLABLE = 1 << 2, + _VARLINK_FIELD_FLAGS_MAX = (1 << 3) - 1, + _VARLINK_FIELD_FLAGS_INVALID = -EINVAL, +} VarlinkFieldFlags; + +typedef struct VarlinkField VarlinkField; +typedef struct VarlinkSymbol VarlinkSymbol; +typedef struct VarlinkInterface VarlinkInterface; + +/* Fields are the components making up symbols */ +struct VarlinkField { + const char *name; + VarlinkFieldType field_type; + VarlinkFieldFlags field_flags; + VarlinkFieldDirection field_direction; /* in case of method call fields: whether input or output argument */ + const VarlinkSymbol *symbol; /* VARLINK_STRUCT, VARLINK_ENUM: anonymous symbol that carries the definitions, VARLINK_NAMED_TYPE: resolved symbol */ + const char *named_type; /* VARLINK_NAMED_TYPE */ +}; + +/* Symbols are primary named concepts in an interface, and are methods, errors or named types (either enum or struct). */ +struct VarlinkSymbol { + const char *name; /* most symbols have a name, but sometimes they are created on-the-fly for fields, in which case they are anonymous */ + VarlinkSymbolType symbol_type; + VarlinkField fields[]; +}; + +/* An interface definition has a name and consist of symbols */ +struct VarlinkInterface { + const char *name; + const VarlinkSymbol *symbols[]; +}; + +#define VARLINK_DEFINE_FIELD(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags) } + +#define VARLINK_DEFINE_FIELD_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags) } + +#define VARLINK_DEFINE_INPUT(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_INPUT } + +#define VARLINK_DEFINE_INPUT_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_INPUT } + +#define VARLINK_DEFINE_OUTPUT(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT } + +#define VARLINK_DEFINE_OUTPUT_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT } + +#define VARLINK_DEFINE_ENUM_VALUE(_name) \ + { .name = #_name, .field_type = VARLINK_ENUM_VALUE } + +#define VARLINK_DEFINE_METHOD(_name, ...) \ + const VarlinkSymbol vl_method_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_METHOD, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_ERROR(_name, ...) \ + const VarlinkSymbol vl_error_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_ERROR, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_STRUCT_TYPE(_name, ...) \ + const VarlinkSymbol vl_type_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_STRUCT_TYPE, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_ENUM_TYPE(_name, ...) \ + const VarlinkSymbol vl_type_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_ENUM_TYPE, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_INTERFACE(_name, _full_name, ...) \ + const VarlinkInterface vl_interface_ ## _name = { \ + .name = (_full_name), \ + .symbols = { __VA_ARGS__ __VA_OPT__(,) NULL}, \ + } + +int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface); +int varlink_idl_format(const VarlinkInterface *interface, char **ret); + +int varlink_idl_parse(const char *text, unsigned *ret_line, unsigned *ret_column, VarlinkInterface **ret); +VarlinkInterface* varlink_interface_free(VarlinkInterface *interface); +DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkInterface*, varlink_interface_free); + +bool varlink_idl_field_name_is_valid(const char *name); +bool varlink_idl_symbol_name_is_valid(const char *name); +bool varlink_idl_interface_name_is_valid(const char *name); + +int varlink_idl_consistent(const VarlinkInterface *interface, bool level); + +const VarlinkSymbol* varlink_idl_find_symbol(const VarlinkInterface *interface, VarlinkSymbolType type, const char *name); +const VarlinkField* varlink_idl_find_field(const VarlinkSymbol *symbol, const char *name); + +int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field); +int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field); +int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field); From 9fc843ed0ee9f605ceec96a1678e07a158920830 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:31:01 +0200 Subject: [PATCH 04/18] varlink: allow associating Varlink IDL information with a VarlinkServer The data is not used for anything yet, but this will be added in later commits. --- src/shared/varlink.c | 34 ++++++++++++++++++++++++++++++++++ src/shared/varlink.h | 6 ++++++ 2 files changed, 40 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 5af2c2c363..2295f46016 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -211,6 +211,7 @@ struct VarlinkServer { LIST_HEAD(VarlinkServerSocket, sockets); Hashmap *methods; + Hashmap *interfaces; VarlinkConnect connect_callback; VarlinkDisconnect disconnect_callback; @@ -2486,6 +2487,7 @@ static VarlinkServer* varlink_server_destroy(VarlinkServer *s) { free(m); hashmap_free(s->methods); + hashmap_free(s->interfaces); hashmap_free(s->by_uid); sd_event_unref(s->event); @@ -2961,6 +2963,38 @@ int varlink_server_bind_disconnect(VarlinkServer *s, VarlinkDisconnect callback) return 0; } +int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *interface) { + assert_return(s, -EINVAL); + assert_return(interface, -EINVAL); + assert_return(interface->name, -EINVAL); + + if (hashmap_contains(s->interfaces, interface->name)) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate registration of interface '%s'.", interface->name); + + return hashmap_ensure_put(&s->interfaces, &string_hash_ops, interface->name, (void*) interface); +} + +int varlink_server_add_interface_many_internal(VarlinkServer *s, ...) { + va_list ap; + int r = 0; + + assert_return(s, -EINVAL); + + va_start(ap, s); + for (;;) { + const VarlinkInterface *interface = va_arg(ap, const VarlinkInterface*); + if (!interface) + break; + + r = varlink_server_add_interface(s, interface); + if (r < 0) + break; + } + va_end(ap); + + return r; +} + unsigned varlink_server_connections_max(VarlinkServer *s) { int dts; diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 5eb4676dc8..457332eecc 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -5,6 +5,7 @@ #include "json.h" #include "time-util.h" +#include "varlink-idl.h" /* A minimal Varlink implementation. We only implement the minimal, obvious bits here though. No validation, * no introspection, no name service, just the stuff actually needed. @@ -153,6 +154,11 @@ int varlink_server_bind_method_many_internal(VarlinkServer *s, ...); int varlink_server_bind_connect(VarlinkServer *s, VarlinkConnect connect); int varlink_server_bind_disconnect(VarlinkServer *s, VarlinkDisconnect disconnect); +/* Add interface definition */ +int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *interface); +int varlink_server_add_interface_many_internal(VarlinkServer *s, ...); +#define varlink_server_add_interface_many(s, ...) varlink_server_add_interface_many_internal(s, __VA_ARGS__, NULL) + void* varlink_server_set_userdata(VarlinkServer *s, void *userdata); void* varlink_server_get_userdata(VarlinkServer *s); From f946d4c9eca403a503f49465ea48a95562fbc3a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:35:18 +0200 Subject: [PATCH 05/18] varlink: add introspection data for the org.varlink.service and io.systemd interfaces The official org.varlink.service interface definition, as per: https://varlink.org/Service And the io.systemd service where we carry some super generic errors our Varlink implementation generates. --- src/shared/meson.build | 2 ++ src/shared/varlink-io.systemd.c | 21 +++++++++++ src/shared/varlink-io.systemd.h | 6 ++++ src/shared/varlink-org.varlink.service.c | 45 ++++++++++++++++++++++++ src/shared/varlink-org.varlink.service.h | 6 ++++ src/shared/varlink.h | 2 ++ 6 files changed, 82 insertions(+) create mode 100644 src/shared/varlink-io.systemd.c create mode 100644 src/shared/varlink-io.systemd.h create mode 100644 src/shared/varlink-org.varlink.service.c create mode 100644 src/shared/varlink-org.varlink.service.h diff --git a/src/shared/meson.build b/src/shared/meson.build index da32e1435d..429789b045 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -167,6 +167,8 @@ shared_sources = files( 'userdb.c', 'varlink.c', 'varlink-idl.c', + 'varlink-io.systemd.c', + 'varlink-org.varlink.service.c', 'verb-log-control.c', 'verbs.c', 'vlan-util.c', diff --git a/src/shared/varlink-io.systemd.c b/src/shared/varlink-io.systemd.c new file mode 100644 index 0000000000..cdfe9ac263 --- /dev/null +++ b/src/shared/varlink-io.systemd.c @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.h" + +/* These are local errors that never cross the wire, and are our own invention */ +static VARLINK_DEFINE_ERROR(Disconnected); +static VARLINK_DEFINE_ERROR(TimedOut); +static VARLINK_DEFINE_ERROR(Protocol); + +/* This one we invented, and use for generically propagating system errors (errno) to clients */ +static VARLINK_DEFINE_ERROR( + System, + VARLINK_DEFINE_FIELD(errno, VARLINK_INT, 0)); + +VARLINK_DEFINE_INTERFACE( + io_systemd, + "io.systemd", + &vl_error_Disconnected, + &vl_error_TimedOut, + &vl_error_Protocol, + &vl_error_System); diff --git a/src/shared/varlink-io.systemd.h b/src/shared/varlink-io.systemd.h new file mode 100644 index 0000000000..6c17c6c4c7 --- /dev/null +++ b/src/shared/varlink-io.systemd.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd; diff --git a/src/shared/varlink-org.varlink.service.c b/src/shared/varlink-org.varlink.service.c new file mode 100644 index 0000000000..52ba99dd2b --- /dev/null +++ b/src/shared/varlink-org.varlink.service.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-org.varlink.service.h" + +static VARLINK_DEFINE_METHOD( + GetInfo, + VARLINK_DEFINE_OUTPUT(vendor, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(product, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(version, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(url, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(interfaces, VARLINK_STRING, VARLINK_ARRAY)); + +static VARLINK_DEFINE_METHOD( + GetInterfaceDescription, + VARLINK_DEFINE_OUTPUT(interface, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + InterfaceNotFound, + VARLINK_DEFINE_FIELD(interface, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + MethodNotFound, + VARLINK_DEFINE_FIELD(method, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + MethodNotImplemented, + VARLINK_DEFINE_FIELD(method, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + InvalidParameter, + VARLINK_DEFINE_FIELD(parameter, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(PermissionDenied); + +/* As per https://varlink.org/Service */ +VARLINK_DEFINE_INTERFACE( + org_varlink_service, + "org.varlink.service", + &vl_method_GetInfo, + &vl_method_GetInterfaceDescription, + &vl_error_InterfaceNotFound, + &vl_error_MethodNotFound, + &vl_error_MethodNotImplemented, + &vl_error_InvalidParameter, + &vl_error_PermissionDenied); diff --git a/src/shared/varlink-org.varlink.service.h b/src/shared/varlink-org.varlink.service.h new file mode 100644 index 0000000000..75c55e6e82 --- /dev/null +++ b/src/shared/varlink-org.varlink.service.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_org_varlink_service; diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 457332eecc..da7265312d 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -187,6 +187,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); #define VARLINK_ERROR_DISCONNECTED "io.systemd.Disconnected" #define VARLINK_ERROR_TIMEOUT "io.systemd.TimedOut" #define VARLINK_ERROR_PROTOCOL "io.systemd.Protocol" + +/* This one we invented, and use for generically propagating system errors (errno) to clients */ #define VARLINK_ERROR_SYSTEM "io.systemd.System" /* These are errors defined in the Varlink spec */ From 7e006b49ad4cf94eac45ee9770756763f7350292 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:39:25 +0200 Subject: [PATCH 06/18] varlink: implement the org.varlink.service introspection interface by default + hook up validator Fixes: #23874 --- src/shared/varlink.c | 233 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 202 insertions(+), 31 deletions(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 2295f46016..aac25bbea8 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -23,6 +23,9 @@ #include "user-util.h" #include "varlink.h" #include "varlink-internal.h" +#include "varlink-org.varlink.service.h" +#include "varlink-io.systemd.h" +#include "version.h" #define VARLINK_DEFAULT_CONNECTIONS_MAX 4096U #define VARLINK_DEFAULT_CONNECTIONS_PER_UID_MAX 1024U @@ -162,6 +165,7 @@ struct Varlink { VarlinkReply reply_callback; JsonVariant *current; + VarlinkSymbol *current_method; struct ucred ucred; bool ucred_acquired:1; @@ -210,8 +214,9 @@ struct VarlinkServer { LIST_HEAD(VarlinkServerSocket, sockets); - Hashmap *methods; - Hashmap *interfaces; + Hashmap *methods; /* Fully qualified symbol name of a method → VarlinkMethod */ + Hashmap *interfaces; /* Fully qualified interface name → VarlinkInterface* */ + Hashmap *symbols; /* Fully qualified symbol name of methord/error → VarlinkSymbol* */ VarlinkConnect connect_callback; VarlinkDisconnect disconnect_callback; @@ -219,7 +224,7 @@ struct VarlinkServer { int64_t event_priority; unsigned n_connections; - Hashmap *by_uid; + Hashmap *by_uid; /* UID_TO_PTR(uid) → UINT_TO_PTR(n_connections) */ void *userdata; char *description; @@ -443,6 +448,7 @@ static void varlink_clear_current(Varlink *v) { /* Clears the currently processed incoming message */ v->current = json_variant_unref(v->current); + v->current_method = NULL; close_many(v->input_fds, v->n_input_fds); v->input_fds = mfree(v->input_fds); @@ -988,10 +994,84 @@ invalid: return 1; } +static int generic_method_get_info( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + _cleanup_strv_free_ char **interfaces = NULL; + _cleanup_free_ char *product = NULL; + int r; + + assert(link); + + if (json_variant_elements(parameters) != 0) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_VARIANT("parameter", json_variant_by_index(parameters, 0)))); + + product = strjoin("systemd (", program_invocation_short_name, ")"); + if (!product) + return -ENOMEM; + + VarlinkInterface *interface; + HASHMAP_FOREACH(interface, ASSERT_PTR(link->server)->interfaces) { + r = strv_extend(&interfaces, interface->name); + if (r < 0) + return r; + } + + strv_sort(interfaces); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("vendor", "The systemd Project"), + JSON_BUILD_PAIR_STRING("product", product), + JSON_BUILD_PAIR_STRING("version", STRINGIFY(PROJECT_VERSION) " (" GIT_VERSION ")"), + JSON_BUILD_PAIR_STRING("url", "https://systemd.io/"), + JSON_BUILD_PAIR_STRV("interfaces", interfaces))); +} + +static int generic_method_get_interface_description( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + static const struct JsonDispatch dispatch_table[] = { + { "interface", JSON_VARIANT_STRING, json_dispatch_const_string, 0, JSON_MANDATORY }, + {} + }; + _cleanup_free_ char *text = NULL; + const VarlinkInterface *interface; + const char *name = NULL; + int r; + + assert(link); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &name); + if (r < 0) + return r; + + interface = hashmap_get(ASSERT_PTR(link->server)->interfaces, name); + if (!interface) + return varlink_errorb(link, VARLINK_ERROR_INTERFACE_NOT_FOUND, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("interface", name))); + + r = varlink_idl_format(interface, &text); + if (r < 0) + return r; + + return varlink_replyb(link, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("description", text))); +} + static int varlink_dispatch_method(Varlink *v) { _cleanup_(json_variant_unrefp) JsonVariant *parameters = NULL; VarlinkMethodFlags flags = 0; - const char *method = NULL, *error; + const char *method = NULL; JsonVariant *e; VarlinkMethod callback; const char *k; @@ -1064,37 +1144,51 @@ static int varlink_dispatch_method(Varlink *v) { assert(v->server); - if (STR_IN_SET(method, "org.varlink.service.GetInfo", "org.varlink.service.GetInterface")) { - /* For now, we don't implement a single of varlink's own methods */ - callback = NULL; - error = VARLINK_ERROR_METHOD_NOT_IMPLEMENTED; - } else if (startswith(method, "org.varlink.service.")) { - callback = NULL; - error = VARLINK_ERROR_METHOD_NOT_FOUND; - } else { - callback = hashmap_get(v->server->methods, method); - error = VARLINK_ERROR_METHOD_NOT_FOUND; + /* First consult user supplied method implementations */ + callback = hashmap_get(v->server->methods, method); + if (!callback) { + if (streq(method, "org.varlink.service.GetInfo")) + callback = generic_method_get_info; + else if (streq(method, "org.varlink.service.GetInterfaceDescription")) + callback = generic_method_get_interface_description; } if (callback) { - r = callback(v, parameters, flags, v->userdata); - if (r < 0) { - log_debug_errno(r, "Callback for %s returned error: %m", method); + bool invalid = false; - /* We got an error back from the callback. Propagate it to the client if the method call remains unanswered. */ - if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { - r = varlink_error_errno(v, r); - if (r < 0) - return r; + v->current_method = hashmap_get(v->server->symbols, method); + if (!v->current_method) + log_debug("No interface description defined for method '%s', not validating.", method); + else { + const char *bad_field; + + r = varlink_idl_validate_method_call(v->current_method, parameters, &bad_field); + if (r < 0) { + log_debug_errno(r, "Parameters for method %s() didn't pass validation on field '%s': %m", method, strna(bad_field)); + r = varlink_errorb(v, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", bad_field))); + invalid = true; + } + } + + if (!invalid) { + r = callback(v, parameters, flags, v->userdata); + if (r < 0) { + log_debug_errno(r, "Callback for %s returned error: %m", method); + + /* We got an error back from the callback. Propagate it to the client if the method call remains unanswered. */ + if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { + r = varlink_error_errno(v, r); + if (r < 0) + return r; + } } } } else if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { - assert(error); - - r = varlink_errorb(v, error, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)))); + r = varlink_errorb(v, VARLINK_ERROR_METHOD_NOT_FOUND, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)))); if (r < 0) return r; - } + } else + r = 0; switch (v->state) { @@ -1114,7 +1208,6 @@ static int varlink_dispatch_method(Varlink *v) { default: assert_not_reached(); - } return r; @@ -1859,6 +1952,14 @@ int varlink_reply(Varlink *v, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + if (v->current_method) { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(v->current_method, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Return parameters for method reply %s() didn't pass validation on field '%s', ignoring: %m", v->current_method->name, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -1925,6 +2026,17 @@ int varlink_error(Varlink *v, const char *error_id, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + VarlinkSymbol *symbol = hashmap_get(v->server->symbols, error_id); + if (!symbol) + log_debug("No interface description defined for error '%s', not validating.", error_id); + else { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(symbol, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Parameters for error %s didn't pass validation on field '%s', ignoring: %m", error_id, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -2026,6 +2138,14 @@ int varlink_notify(Varlink *v, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + if (v->current_method) { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(v->current_method, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Return parameters for method reply %s() didn't pass validation on field '%s', ignoring: %m", v->current_method->name, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -2455,7 +2575,8 @@ int varlink_set_allow_fd_passing_output(Varlink *v, bool b) { } int varlink_server_new(VarlinkServer **ret, VarlinkServerFlags flags) { - VarlinkServer *s; + _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL; + int r; assert_return(ret, -EINVAL); assert_return((flags & ~_VARLINK_SERVER_FLAGS_ALL) == 0, -EINVAL); @@ -2471,7 +2592,14 @@ int varlink_server_new(VarlinkServer **ret, VarlinkServerFlags flags) { .connections_per_uid_max = varlink_server_connections_per_uid_max(NULL), }; - *ret = s; + r = varlink_server_add_interface_many( + s, + &vl_interface_io_systemd, + &vl_interface_org_varlink_service); + if (r < 0) + return r; + + *ret = TAKE_PTR(s); return 0; } @@ -2488,6 +2616,7 @@ static VarlinkServer* varlink_server_destroy(VarlinkServer *s) { hashmap_free(s->methods); hashmap_free(s->interfaces); + hashmap_free(s->symbols); hashmap_free(s->by_uid); sd_event_unref(s->event); @@ -2891,6 +3020,22 @@ sd_event *varlink_server_get_event(VarlinkServer *s) { return s->event; } +static bool varlink_symbol_in_interface(const char *method, const char *interface) { + const char *p; + + assert(method); + assert(interface); + + p = startswith(method, interface); + if (!p) + return false; + + if (*p != '.') + return false; + + return !strchr(p+1, '.'); +} + int varlink_server_bind_method(VarlinkServer *s, const char *method, VarlinkMethod callback) { _cleanup_free_ char *m = NULL; int r; @@ -2899,7 +3044,8 @@ int varlink_server_bind_method(VarlinkServer *s, const char *method, VarlinkMeth assert_return(method, -EINVAL); assert_return(callback, -EINVAL); - if (startswith(method, "org.varlink.service.")) + if (varlink_symbol_in_interface(method, "org.varlink.service") || + varlink_symbol_in_interface(method, "io.systemd")) return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Cannot bind server to '%s'.", method); m = strdup(method); @@ -2964,6 +3110,8 @@ int varlink_server_bind_disconnect(VarlinkServer *s, VarlinkDisconnect callback) } int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *interface) { + int r; + assert_return(s, -EINVAL); assert_return(interface, -EINVAL); assert_return(interface->name, -EINVAL); @@ -2971,7 +3119,30 @@ int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *inter if (hashmap_contains(s->interfaces, interface->name)) return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate registration of interface '%s'.", interface->name); - return hashmap_ensure_put(&s->interfaces, &string_hash_ops, interface->name, (void*) interface); + r = hashmap_ensure_put(&s->interfaces, &string_hash_ops, interface->name, (void*) interface); + if (r < 0) + return r; + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + _cleanup_free_ char *j = NULL; + + /* We only ever want to validate method calls/replies and errors against the interface + * definitions, hence don't bother with the type symbols */ + if (!IN_SET((*symbol)->symbol_type, VARLINK_METHOD, VARLINK_ERROR)) + continue; + + j = strjoin(interface->name, ".", (*symbol)->name); + if (!j) + return -ENOMEM; + + r = hashmap_ensure_put(&s->symbols, &string_hash_ops_free, j, (void*) *symbol); + if (r < 0) + return r; + + TAKE_PTR(j); + } + + return 0; } int varlink_server_add_interface_many_internal(VarlinkServer *s, ...) { From 0f274a8790d335adb0a7fa5c55df4a2c16fbe1d3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:40:35 +0200 Subject: [PATCH 07/18] varlink: add varlink_connect_url() and varlink_connect_exec() calls --- src/shared/varlink.c | 145 +++++++++++++++++++++++++++++++++++++++++++ src/shared/varlink.h | 2 + 2 files changed, 147 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index aac25bbea8..f52c961ce5 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -10,6 +10,7 @@ #include "hashmap.h" #include "io-util.h" #include "list.h" +#include "path-util.h" #include "process-util.h" #include "selinux-util.h" #include "serialize.h" @@ -193,6 +194,8 @@ struct Varlink { sd_event_source *time_event_source; sd_event_source *quit_event_source; sd_event_source *defer_event_source; + + pid_t exec_pid; }; typedef struct VarlinkServerSocket VarlinkServerSocket; @@ -404,6 +407,143 @@ int varlink_connect_address(Varlink **ret, const char *address) { return 0; } +int varlink_connect_exec(Varlink **ret, const char *_command, char **_argv) { + _cleanup_close_pair_ int pair[2] = PIPE_EBADF; + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_free_ char *command = NULL; + _cleanup_strv_free_ char **argv = NULL; + int r; + + assert_return(ret, -EINVAL); + assert_return(_command, -EINVAL); + + /* Copy the strings, in case they point into our own argv[], which we'll invalidate shortly because + * we rename the child process */ + command = strdup(_command); + if (!command) + return -ENOMEM; + + if (strv_isempty(_argv)) + argv = strv_new(command); + else + argv = strv_copy(_argv); + if (!argv) + return -ENOMEM; + + log_debug("Forking off Varlink child process '%s'.", command); + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0) + return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m"); + + r = safe_fork_full( + "(sd-vlexec)", + /* stdio_fds= */ NULL, + /* except_fds= */ (int[]) { pair[1] }, + /* n_except_fds= */ 1, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_REOPEN_LOG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE, + &pid); + if (r < 0) + return log_debug_errno(r, "Failed to spawn process: %m"); + if (r == 0) { + char spid[DECIMAL_STR_MAX(pid_t)+1]; + const char *setenv_list[] = { + "LISTEN_FDS", "1", + "LISTEN_PID", spid, + "LISTEN_FDNAMES", "varlink", + NULL, NULL, + }; + /* Child */ + + pair[0] = -EBADF; + + r = move_fd(pair[1], 3, /* cloexec= */ false); + if (r < 0) { + log_debug_errno(r, "Failed to move file descriptor to 3: %m"); + _exit(EXIT_FAILURE); + } + + xsprintf(spid, PID_FMT, pid); + + STRV_FOREACH_PAIR(a, b, setenv_list) { + if (setenv(*a, *b, /* override= */ true) < 0) { + log_debug_errno(errno, "Failed to set environment variable '%s': %m", *a); + _exit(EXIT_FAILURE); + } + } + + execvp(command, argv); + log_debug_errno(r, "Failed to invoke process '%s': %m", command); + _exit(EXIT_FAILURE); + } + + pair[1] = safe_close(pair[1]); + + Varlink *v; + r = varlink_new(&v); + if (r < 0) + return log_debug_errno(r, "Failed to create varlink object: %m"); + + v->fd = TAKE_FD(pair[0]); + v->af = AF_UNIX; + v->exec_pid = TAKE_PID(pid); + varlink_set_state(v, VARLINK_IDLE_CLIENT); + + *ret = v; + return 0; +} + +int varlink_connect_url(Varlink **ret, const char *url) { + _cleanup_free_ char *c = NULL; + const char *p; + bool exec; + + assert_return(ret, -EINVAL); + assert_return(url, -EINVAL); + + // FIXME: Add support for vsock:, ssh-exec:, ssh-unix: URL schemes here. (The latter with OpenSSH + // 9.4's -W switch for referencing remote AF_UNIX sockets.) + + /* The Varlink URL scheme is a bit underdefined. We support only the unix: transport for now, plus an + * exec: transport we made up ourselves. Strictly speaking this shouldn't even be called URL, since + * it has nothing to do with Internet URLs by RFC. */ + + p = startswith(url, "unix:"); + if (p) + exec = false; + else { + p = startswith(url, "exec:"); + if (!p) + return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported."); + + exec = true; + } + + /* The varlink.org reference C library supports more than just file system paths. We might want to + * support that one day too. For now simply refuse that. */ + if (p[strcspn(p, ";?#")] != '\0') + return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL parameterization with ';', '?', '#' not supported."); + + if (exec || p[0] != '@') { /* no validity checks for abstract namespace */ + + if (!path_is_absolute(p)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path not absolute, refusing."); + + c = strdup(p); + if (!c) + return log_oom_debug(); + + path_simplify(c); + + if (!path_is_normalized(c)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing."); + } + + if (exec) + return varlink_connect_exec(ret, c ?: p, NULL); + + return varlink_connect_address(ret, c ?: p); +} + int varlink_connect_fd(Varlink **ret, int fd) { Varlink *v; int r; @@ -482,6 +622,11 @@ static void varlink_clear(Varlink *v) { v->output_queue_tail = NULL; v->event = sd_event_unref(v->event); + + if (v->exec_pid > 0) { + sigterm_wait(v->exec_pid); + v->exec_pid = 0; + } } static Varlink* varlink_destroy(Varlink *v) { diff --git a/src/shared/varlink.h b/src/shared/varlink.h index da7265312d..be4b328969 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -56,6 +56,8 @@ typedef int (*VarlinkConnect)(VarlinkServer *server, Varlink *link, void *userda typedef void (*VarlinkDisconnect)(VarlinkServer *server, Varlink *link, void *userdata); int varlink_connect_address(Varlink **ret, const char *address); +int varlink_connect_exec(Varlink **ret, const char *command, char **argv); +int varlink_connect_url(Varlink **ret, const char *url); int varlink_connect_fd(Varlink **ret, int fd); Varlink* varlink_ref(Varlink *link); From abef4a7b3c4625e85e4c4f8b5a17a04260b845b2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:41:56 +0200 Subject: [PATCH 08/18] tree-wide: make all our Varlink APIs introspectable --- src/core/core-varlink.c | 9 ++ src/home/homed-manager.c | 5 + src/journal/journald-server.c | 5 + src/machine/machined-varlink.c | 5 + src/oom/oomd-manager.c | 5 + src/resolve/resolved-varlink.c | 10 ++ src/shared/meson.build | 6 + src/shared/varlink-io.systemd.Journal.c | 19 +++ src/shared/varlink-io.systemd.Journal.h | 6 + src/shared/varlink-io.systemd.ManagedOOM.c | 20 +++ src/shared/varlink-io.systemd.ManagedOOM.h | 6 + .../varlink-io.systemd.Resolve.Monitor.c | 136 ++++++++++++++++++ .../varlink-io.systemd.Resolve.Monitor.h | 6 + src/shared/varlink-io.systemd.Resolve.c | 76 ++++++++++ src/shared/varlink-io.systemd.Resolve.h | 6 + src/shared/varlink-io.systemd.UserDatabase.c | 46 ++++++ src/shared/varlink-io.systemd.UserDatabase.h | 6 + src/shared/varlink-io.systemd.oom.c | 25 ++++ src/shared/varlink-io.systemd.oom.h | 7 + src/userdb/userwork.c | 5 + 20 files changed, 409 insertions(+) create mode 100644 src/shared/varlink-io.systemd.Journal.c create mode 100644 src/shared/varlink-io.systemd.Journal.h create mode 100644 src/shared/varlink-io.systemd.ManagedOOM.c create mode 100644 src/shared/varlink-io.systemd.ManagedOOM.h create mode 100644 src/shared/varlink-io.systemd.Resolve.Monitor.c create mode 100644 src/shared/varlink-io.systemd.Resolve.Monitor.h create mode 100644 src/shared/varlink-io.systemd.Resolve.c create mode 100644 src/shared/varlink-io.systemd.Resolve.h create mode 100644 src/shared/varlink-io.systemd.UserDatabase.c create mode 100644 src/shared/varlink-io.systemd.UserDatabase.h create mode 100644 src/shared/varlink-io.systemd.oom.c create mode 100644 src/shared/varlink-io.systemd.oom.h diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c index 1ff4a93cce..b0b15d242c 100644 --- a/src/core/core-varlink.c +++ b/src/core/core-varlink.c @@ -5,6 +5,8 @@ #include "strv.h" #include "user-util.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink-io.systemd.ManagedOOM.h" typedef struct LookupParameters { const char *user_name; @@ -600,6 +602,13 @@ int manager_setup_varlink_server(Manager *m, VarlinkServer **ret) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface_many( + s, + &vl_interface_io_systemd_UserDatabase, + &vl_interface_io_systemd_ManagedOOM); + if (r < 0) + return log_error_errno(r, "Failed to add interfaces to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index 903207b50f..c4525310fc 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -53,6 +53,7 @@ #include "user-record-util.h" #include "user-record.h" #include "user-util.h" +#include "varlink-io.systemd.UserDatabase.h" /* Where to look for private/public keys that are used to sign the user records. We are not using * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that @@ -1003,6 +1004,10 @@ static int manager_bind_varlink(Manager *m) { varlink_server_set_userdata(m->varlink_server, m); + r = varlink_server_add_interface(m->varlink_server, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( m->varlink_server, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/journal/journald-server.c b/src/journal/journald-server.c index 615b3c6c6b..371338211a 100644 --- a/src/journal/journald-server.c +++ b/src/journal/journald-server.c @@ -58,6 +58,7 @@ #include "syslog-util.h" #include "uid-alloc-range.h" #include "user-util.h" +#include "varlink-io.systemd.Journal.h" #define USER_JOURNALS_MAX 1024 @@ -2224,6 +2225,10 @@ static int server_open_varlink(Server *s, const char *socket, int fd) { varlink_server_set_userdata(s->varlink_server, s); + r = varlink_server_add_interface(s->varlink_server, &vl_interface_io_systemd_Journal); + if (r < 0) + return log_error_errno(r, "Failed to add Journal interface to varlink server: %m"); + r = varlink_server_bind_method_many( s->varlink_server, "io.systemd.Journal.Synchronize", vl_method_synchronize, diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c index ec625ad7b4..4b72ee9b0a 100644 --- a/src/machine/machined-varlink.c +++ b/src/machine/machined-varlink.c @@ -5,6 +5,7 @@ #include "mkdir.h" #include "user-util.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" typedef struct LookupParameters { const char *user_name; @@ -392,6 +393,10 @@ int manager_varlink_init(Manager *m) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/oom/oomd-manager.c b/src/oom/oomd-manager.c index 49896ea7f1..75918a5fe7 100644 --- a/src/oom/oomd-manager.c +++ b/src/oom/oomd-manager.c @@ -15,6 +15,7 @@ #include "oomd-manager.h" #include "path-util.h" #include "percent-util.h" +#include "varlink-io.systemd.oom.h" typedef struct ManagedOOMMessage { ManagedOOMMode mode; @@ -733,6 +734,10 @@ static int manager_varlink_init(Manager *m, int fd) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_oom); + if (r < 0) + return log_error_errno(r, "Failed to add oom interface to varlink server: %m"); + r = varlink_server_bind_method(s, "io.systemd.oom.ReportManagedOOMCGroups", process_managed_oom_request); if (r < 0) return log_error_errno(r, "Failed to register varlink method: %m"); diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index 83b8e52807..c9bb616acb 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -5,6 +5,8 @@ #include "resolved-dns-synthesize.h" #include "resolved-varlink.h" #include "socket-netlink.h" +#include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.Resolve.Monitor.h" typedef struct LookupParameters { int ifindex; @@ -699,6 +701,10 @@ static int varlink_monitor_server_init(Manager *m) { varlink_server_set_userdata(server, m); + r = varlink_server_add_interface(server, &vl_interface_io_systemd_Resolve_Monitor); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve.Monitor interface to varlink server: %m"); + r = varlink_server_bind_method_many( server, "io.systemd.Resolve.Monitor.SubscribeQueryResults", vl_method_subscribe_query_results, @@ -741,6 +747,10 @@ static int varlink_main_server_init(Manager *m) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_Resolve); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve interface to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname, diff --git a/src/shared/meson.build b/src/shared/meson.build index 429789b045..9d29c32dff 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -168,6 +168,12 @@ shared_sources = files( 'varlink.c', 'varlink-idl.c', 'varlink-io.systemd.c', + 'varlink-io.systemd.Journal.c', + 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.Resolve.Monitor.c', + 'varlink-io.systemd.Resolve.c', + 'varlink-io.systemd.UserDatabase.c', + 'varlink-io.systemd.oom.c', 'varlink-org.varlink.service.c', 'verb-log-control.c', 'verbs.c', diff --git a/src/shared/varlink-io.systemd.Journal.c b/src/shared/varlink-io.systemd.Journal.c new file mode 100644 index 0000000000..b93fb72aca --- /dev/null +++ b/src/shared/varlink-io.systemd.Journal.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Journal.h" + +static VARLINK_DEFINE_METHOD(Synchronize); +static VARLINK_DEFINE_METHOD(Rotate); +static VARLINK_DEFINE_METHOD(FlushToVar); +static VARLINK_DEFINE_METHOD(RelinquishVar); + +static VARLINK_DEFINE_ERROR(NotSupportedByNamespaces); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Journal, + "io.systemd.Journal", + &vl_method_Synchronize, + &vl_method_Rotate, + &vl_method_FlushToVar, + &vl_method_RelinquishVar, + &vl_error_NotSupportedByNamespaces); diff --git a/src/shared/varlink-io.systemd.Journal.h b/src/shared/varlink-io.systemd.Journal.h new file mode 100644 index 0000000000..0bc94a775c --- /dev/null +++ b/src/shared/varlink-io.systemd.Journal.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Journal; diff --git a/src/shared/varlink-io.systemd.ManagedOOM.c b/src/shared/varlink-io.systemd.ManagedOOM.c new file mode 100644 index 0000000000..7cc5721dc2 --- /dev/null +++ b/src/shared/varlink-io.systemd.ManagedOOM.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.ManagedOOM.h" + +/* Pull in vl_type_ControlGroup, since both interfaces need it */ +#include "varlink-io.systemd.oom.h" + +/* This is PID1's Varlink service, where PID 1 is the server and oomd is the client. + * + * Compare with io.systemd.oom where the client/server roles of oomd and the service manager are swapped! */ + +static VARLINK_DEFINE_METHOD( + SubscribeManagedOOMCGroups, + VARLINK_DEFINE_OUTPUT_BY_TYPE(cgroups, ControlGroup, VARLINK_ARRAY)); + +VARLINK_DEFINE_INTERFACE( + io_systemd_ManagedOOM, + "io.systemd.ManagedOOM", + &vl_method_SubscribeManagedOOMCGroups, + &vl_type_ControlGroup); diff --git a/src/shared/varlink-io.systemd.ManagedOOM.h b/src/shared/varlink-io.systemd.ManagedOOM.h new file mode 100644 index 0000000000..2c8bf54c4e --- /dev/null +++ b/src/shared/varlink-io.systemd.ManagedOOM.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_ManagedOOM; diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.c b/src/shared/varlink-io.systemd.Resolve.Monitor.c new file mode 100644 index 0000000000..179b9bcf1e --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Monitor.c @@ -0,0 +1,136 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Resolve.Monitor.h" + +VARLINK_DEFINE_STRUCT_TYPE( + ResourceKey, + VARLINK_DEFINE_FIELD(class, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(type, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + ResourceRecord, + VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0), + VARLINK_DEFINE_FIELD(priority, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(weight, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(port, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(cpu, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(os, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(items, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(address, VARLINK_INT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(mname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(rname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(serial, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(refresh, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(expire, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(minimum, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(exchange, VARLINK_STRING, VARLINK_NULLABLE)); + +VARLINK_DEFINE_STRUCT_TYPE( + Answer, + VARLINK_DEFINE_FIELD_BY_TYPE(rr, ResourceRecord, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(raw, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE)); + +VARLINK_DEFINE_METHOD( + SubscribeQueryResults, + /* First reply */ + VARLINK_DEFINE_OUTPUT(ready, VARLINK_BOOL, VARLINK_NULLABLE), + /* Subsequent replies */ + VARLINK_DEFINE_OUTPUT(state, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(rcode, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(errno, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(question, ResourceKey, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT_BY_TYPE(collectedQuestions, ResourceKey, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT_BY_TYPE(answer, Answer, VARLINK_NULLABLE|VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + CacheEntry, + VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(rrs, ResourceRecord, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(type, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(until, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + ScopeCache, + VARLINK_DEFINE_FIELD(protocol, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(family, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(ifname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(cache, CacheEntry, VARLINK_ARRAY)); + +VARLINK_DEFINE_METHOD( + DumpCache, + VARLINK_DEFINE_OUTPUT_BY_TYPE(dump, ScopeCache, VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + ServerState, + VARLINK_DEFINE_FIELD(Server, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(Type, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(Interface, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(InterfaceIndex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(VerifiedFeatureLevel, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(PossibleFeatureLevel, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(DNSSECMode, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(DNSSECSupported, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(ReceivedUDPFragmentMax, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(FailedUDPAttempts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(FailedTCPAttempts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(PacketTruncated, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketBadOpt, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketRRSIGMissing, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketInvalid, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketDoOff, VARLINK_BOOL, 0)); + +VARLINK_DEFINE_METHOD( + DumpServerState, + VARLINK_DEFINE_OUTPUT_BY_TYPE(dump, ServerState, VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + TransactionStatistics, + VARLINK_DEFINE_FIELD(currentTransactions, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTransactions, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTimeouts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTimeoutsServedStale, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalFailedResponses, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalFailedResponsesServedStale, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + CacheStatistics, + VARLINK_DEFINE_FIELD(size, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(hits, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(misses, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + DnssecStatistics, + VARLINK_DEFINE_FIELD(secure, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(insecure, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(bogus, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(indeterminate, VARLINK_INT, 0)); + +VARLINK_DEFINE_METHOD( + DumpStatistics, + VARLINK_DEFINE_OUTPUT_BY_TYPE(transactions, TransactionStatistics, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(cache, CacheStatistics, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(dnssec, DnssecStatistics, 0)); + +VARLINK_DEFINE_METHOD(ResetStatistics); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Resolve_Monitor, + "io.systemd.Resolve.Monitor", + &vl_method_SubscribeQueryResults, + &vl_method_DumpCache, + &vl_method_DumpServerState, + &vl_method_DumpStatistics, + &vl_method_ResetStatistics, + &vl_type_ResourceKey, + &vl_type_ResourceRecord, + &vl_type_Answer, + &vl_type_CacheEntry, + &vl_type_ScopeCache, + &vl_type_TransactionStatistics, + &vl_type_CacheStatistics, + &vl_type_DnssecStatistics, + &vl_type_ServerState); diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.h b/src/shared/varlink-io.systemd.Resolve.Monitor.h new file mode 100644 index 0000000000..a133ec30a5 --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Monitor.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Resolve_Monitor; diff --git a/src/shared/varlink-io.systemd.Resolve.c b/src/shared/varlink-io.systemd.Resolve.c new file mode 100644 index 0000000000..0d8ad281fa --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Resolve.h" + +static VARLINK_DEFINE_STRUCT_TYPE( + ResolvedAddress, + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(family, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(address, VARLINK_INT, VARLINK_ARRAY)); + +static VARLINK_DEFINE_METHOD( + ResolveHostname, + VARLINK_DEFINE_INPUT(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(family, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(flags, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(addresses, ResolvedAddress, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(flags, VARLINK_INT, 0)); + +static VARLINK_DEFINE_STRUCT_TYPE( + ResolvedName, + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_METHOD( + ResolveAddress, + VARLINK_DEFINE_INPUT(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(family, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(address, VARLINK_INT, VARLINK_ARRAY), + VARLINK_DEFINE_INPUT(flags, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(names, ResolvedName, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(flags, VARLINK_INT, 0)); + +static VARLINK_DEFINE_ERROR(NoNameServers); +static VARLINK_DEFINE_ERROR(NoSuchResourceRecord); +static VARLINK_DEFINE_ERROR(QueryTimedOut); +static VARLINK_DEFINE_ERROR(MaxAttemptsReached); +static VARLINK_DEFINE_ERROR(InvalidReply); +static VARLINK_DEFINE_ERROR(QueryAborted); +static VARLINK_DEFINE_ERROR( + DNSSECValidationFailed, + VARLINK_DEFINE_FIELD(result, VARLINK_STRING, 0)); +static VARLINK_DEFINE_ERROR(NoTrustAnchor); +static VARLINK_DEFINE_ERROR(ResourceRecordTypeUnsupported); +static VARLINK_DEFINE_ERROR(NetworkDown); +static VARLINK_DEFINE_ERROR(NoSource); +static VARLINK_DEFINE_ERROR(StubLoop); +static VARLINK_DEFINE_ERROR( + DNSError, + VARLINK_DEFINE_FIELD(rcode, VARLINK_INT, 0)); +static VARLINK_DEFINE_ERROR(CNAMELoop); +static VARLINK_DEFINE_ERROR(BadAddressSize); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Resolve, + "io.systemd.Resolve", + &vl_method_ResolveHostname, + &vl_method_ResolveAddress, + &vl_type_ResolvedAddress, + &vl_type_ResolvedName, + &vl_error_NoNameServers, + &vl_error_NoSuchResourceRecord, + &vl_error_QueryTimedOut, + &vl_error_MaxAttemptsReached, + &vl_error_InvalidReply, + &vl_error_QueryAborted, + &vl_error_DNSSECValidationFailed, + &vl_error_NoTrustAnchor, + &vl_error_ResourceRecordTypeUnsupported, + &vl_error_NetworkDown, + &vl_error_NoSource, + &vl_error_StubLoop, + &vl_error_DNSError, + &vl_error_CNAMELoop, + &vl_error_BadAddressSize); diff --git a/src/shared/varlink-io.systemd.Resolve.h b/src/shared/varlink-io.systemd.Resolve.h new file mode 100644 index 0000000000..5c7ed39d0d --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Resolve; diff --git a/src/shared/varlink-io.systemd.UserDatabase.c b/src/shared/varlink-io.systemd.UserDatabase.c new file mode 100644 index 0000000000..a640d2e29a --- /dev/null +++ b/src/shared/varlink-io.systemd.UserDatabase.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.UserDatabase.h" + +static VARLINK_DEFINE_METHOD( + GetUserRecord, + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(userName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(record, VARLINK_OBJECT, 0), + VARLINK_DEFINE_OUTPUT(incomplete, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_METHOD( + GetGroupRecord, + VARLINK_DEFINE_INPUT(gid, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(groupName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(record, VARLINK_OBJECT, 0), + VARLINK_DEFINE_OUTPUT(incomplete, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_METHOD( + GetMemberships, + VARLINK_DEFINE_INPUT(userName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(groupName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(userName, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(groupName, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(NoRecordFound); +static VARLINK_DEFINE_ERROR(BadService); +static VARLINK_DEFINE_ERROR(ServiceNotAvailable); +static VARLINK_DEFINE_ERROR(ConflictingRecordNotFound); +static VARLINK_DEFINE_ERROR(EnumerationNotSupported); + +/* As per https://systemd.io/USER_GROUP_API/ */ +VARLINK_DEFINE_INTERFACE( + io_systemd_UserDatabase, + "io.systemd.UserDatabase", + &vl_method_GetUserRecord, + &vl_method_GetGroupRecord, + &vl_method_GetMemberships, + &vl_error_NoRecordFound, + &vl_error_BadService, + &vl_error_ServiceNotAvailable, + &vl_error_ConflictingRecordNotFound, + &vl_error_EnumerationNotSupported); diff --git a/src/shared/varlink-io.systemd.UserDatabase.h b/src/shared/varlink-io.systemd.UserDatabase.h new file mode 100644 index 0000000000..346ca848ac --- /dev/null +++ b/src/shared/varlink-io.systemd.UserDatabase.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_UserDatabase; diff --git a/src/shared/varlink-io.systemd.oom.c b/src/shared/varlink-io.systemd.oom.c new file mode 100644 index 0000000000..e1da3fa527 --- /dev/null +++ b/src/shared/varlink-io.systemd.oom.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.oom.h" + +/* This is oomd's Varlink service, where oomd is server and systemd --user is the client. + * + * Compare with io.systemd.ManagedOOM where the client/server roles of the service manager and oomd are + * swapped! */ + +VARLINK_DEFINE_STRUCT_TYPE( + ControlGroup, + VARLINK_DEFINE_FIELD(mode, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(path, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(property, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(limit, VARLINK_INT, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_METHOD( + ReportManagedOOMCGroups, + VARLINK_DEFINE_INPUT_BY_TYPE(cgroups, ControlGroup, VARLINK_ARRAY)); + +VARLINK_DEFINE_INTERFACE( + io_systemd_oom, + "io.systemd.oom", + &vl_method_ReportManagedOOMCGroups, + &vl_type_ControlGroup); diff --git a/src/shared/varlink-io.systemd.oom.h b/src/shared/varlink-io.systemd.oom.h new file mode 100644 index 0000000000..911dbc29e6 --- /dev/null +++ b/src/shared/varlink-io.systemd.oom.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkSymbol vl_type_ControlGroup; +extern const VarlinkInterface vl_interface_io_systemd_oom; diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c index 9fccadc950..7413378bcb 100644 --- a/src/userdb/userwork.c +++ b/src/userdb/userwork.c @@ -18,6 +18,7 @@ #include "user-util.h" #include "userdb.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" #define ITERATIONS_MAX 64U #define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) @@ -483,6 +484,10 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to allocate server: %m"); + r = varlink_server_add_interface(server, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( server, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, From 7dce9ead5d92703480afc312d2eb30b441348bf6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 11:49:38 +0200 Subject: [PATCH 09/18] oomd: move SubscriptionTaken Varlink error out of generic Varlink code This error is a private error returned by PID 1 to oomd. It's internal, and very specific to the use-case. Hence it should not be part of the org.varlink.service interface (which isn't really our namespace anyway). Hence, let's clean this up and move it over to the ManagedOOM varlink interface of PID, where it belongs. Since this is a private protocol of our two daemons, and the client (i.e. oomd) doesn't explicitly test for this error anyway we can just move it over without ill effects. --- src/core/core-varlink.c | 2 +- src/shared/varlink-io.systemd.ManagedOOM.c | 5 ++++- src/shared/varlink.h | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c index b0b15d242c..71714e9473 100644 --- a/src/core/core-varlink.c +++ b/src/core/core-varlink.c @@ -234,7 +234,7 @@ static int vl_method_subscribe_managed_oom_cgroups( /* We only take one subscriber for this method so return an error if there's already an existing one. * This shouldn't happen since systemd-oomd is the only client of this method. */ if (FLAGS_SET(flags, VARLINK_METHOD_MORE) && m->managed_oom_varlink) - return varlink_error(link, VARLINK_ERROR_SUBSCRIPTION_TAKEN, NULL); + return varlink_error(link, "io.systemd.ManagedOOM.SubscriptionTaken", NULL); r = build_managed_oom_cgroups_json(m, &v); if (r < 0) diff --git a/src/shared/varlink-io.systemd.ManagedOOM.c b/src/shared/varlink-io.systemd.ManagedOOM.c index 7cc5721dc2..d6414b3718 100644 --- a/src/shared/varlink-io.systemd.ManagedOOM.c +++ b/src/shared/varlink-io.systemd.ManagedOOM.c @@ -13,8 +13,11 @@ static VARLINK_DEFINE_METHOD( SubscribeManagedOOMCGroups, VARLINK_DEFINE_OUTPUT_BY_TYPE(cgroups, ControlGroup, VARLINK_ARRAY)); +static VARLINK_DEFINE_ERROR(SubscriptionTaken); + VARLINK_DEFINE_INTERFACE( io_systemd_ManagedOOM, "io.systemd.ManagedOOM", &vl_method_SubscribeManagedOOMCGroups, - &vl_type_ControlGroup); + &vl_type_ControlGroup, + &vl_error_SubscriptionTaken); diff --git a/src/shared/varlink.h b/src/shared/varlink.h index be4b328969..d49a8510f0 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -200,5 +200,4 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); #define VARLINK_ERROR_INVALID_PARAMETER "org.varlink.service.InvalidParameter" /* These are errors we came up with and squatted the namespace with */ -#define VARLINK_ERROR_SUBSCRIPTION_TAKEN "org.varlink.service.SubscriptionTaken" #define VARLINK_ERROR_PERMISSION_DENIED "org.varlink.service.PermissionDenied" From faec54ad3491c68f6aa69257333733f3ba9c4cff Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:44:09 +0200 Subject: [PATCH 10/18] test: add test for varlink introspection logic --- src/test/meson.build | 4 + src/test/test-varlink-idl.c | 341 ++++++++++++++++++++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 src/test/test-varlink-idl.c diff --git a/src/test/meson.build b/src/test/meson.build index 7e25fcdbaa..3f1b7afbf7 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -417,6 +417,10 @@ executables += [ 'sources' : files('test-varlink.c'), 'dependencies' : threads, }, + test_template + { + 'sources' : files('test-varlink-idl.c'), + 'dependencies' : threads, + }, test_template + { 'sources' : files('test-watchdog.c'), 'type' : 'unsafe', diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c new file mode 100644 index 0000000000..cd85e2c106 --- /dev/null +++ b/src/test/test-varlink-idl.c @@ -0,0 +1,341 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "pretty-print.h" +#include "tests.h" +#include "varlink.h" +#include "varlink-idl.h" +#include "varlink-io.systemd.h" +#include "varlink-io.systemd.Journal.h" +#include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.Resolve.Monitor.h" +#include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink-io.systemd.oom.h" +#include "varlink-org.varlink.service.h" + +static VARLINK_DEFINE_ENUM_TYPE( + EnumTest, + VARLINK_DEFINE_ENUM_VALUE(foo), + VARLINK_DEFINE_ENUM_VALUE(bar), + VARLINK_DEFINE_ENUM_VALUE(baz)); + +static VARLINK_DEFINE_STRUCT_TYPE( + NestedStructTest, + VARLINK_DEFINE_FIELD(x, VARLINK_INT, 0)); + +static VARLINK_DEFINE_STRUCT_TYPE( + StructTest, + + VARLINK_DEFINE_FIELD(bbb, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(bbbn, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(bbba, VARLINK_BOOL, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(bbbna, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(bbbm, VARLINK_BOOL, VARLINK_MAP), + VARLINK_DEFINE_FIELD(bbbnm, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(iii, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(iiin, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(iiia, VARLINK_INT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(iiina, VARLINK_INT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(iiim, VARLINK_INT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(iiinm, VARLINK_INT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(fff, VARLINK_FLOAT, 0), + VARLINK_DEFINE_FIELD(fffn, VARLINK_FLOAT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(fffa, VARLINK_FLOAT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(fffna, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(fffm, VARLINK_FLOAT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(fffnm, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(sss, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(sssn, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(sssa, VARLINK_STRING, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(sssna, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(sssm, VARLINK_STRING, VARLINK_MAP), + VARLINK_DEFINE_FIELD(sssnm, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(ooo, VARLINK_OBJECT, 0), + VARLINK_DEFINE_FIELD(ooon, VARLINK_OBJECT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(oooa, VARLINK_OBJECT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(ooona, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(ooom, VARLINK_OBJECT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(ooonm, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD_BY_TYPE(eee, EnumTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(eeen, EnumTest, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(eeea, EnumTest, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(eeena, EnumTest, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(eeem, EnumTest, VARLINK_MAP), + VARLINK_DEFINE_FIELD_BY_TYPE(eeenm, EnumTest, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD_BY_TYPE(nnn, NestedStructTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnn, NestedStructTest, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(nnna, NestedStructTest, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnna, NestedStructTest, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnm, NestedStructTest, VARLINK_MAP), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnnm, NestedStructTest, VARLINK_NULLABLE|VARLINK_MAP)); + +static VARLINK_DEFINE_METHOD( + MethodTest, + VARLINK_DEFINE_INPUT(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_INPUT_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_INPUT_BY_TYPE(z, StructTest, 0), + VARLINK_DEFINE_OUTPUT(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(z, StructTest, 0)); + +static VARLINK_DEFINE_ERROR( + ErrorTest, + VARLINK_DEFINE_FIELD(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(z, StructTest, 0)); + +static VARLINK_DEFINE_INTERFACE( + xyz_test, + "xyz.test", + &vl_type_EnumTest, + &vl_type_NestedStructTest, + &vl_type_StructTest, + &vl_method_MethodTest, + &vl_error_ErrorTest); + +static void test_parse_format_one(const VarlinkInterface *iface) { + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + _cleanup_free_ char *text = NULL, *text2 = NULL; + + assert_se(iface); + + assert_se(varlink_idl_dump(stdout, /* use_colors=*/ true, iface) >= 0); + assert_se(varlink_idl_consistent(iface, LOG_ERR) >= 0); + assert_se(varlink_idl_format(iface, &text) >= 0); + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + assert_se(varlink_idl_consistent(parsed, LOG_ERR) >= 0); + assert_se(varlink_idl_format(parsed, &text2) >= 0); + assert_se(streq(text, text2)); +} + +TEST(parse_format) { + test_parse_format_one(&vl_interface_org_varlink_service); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_UserDatabase); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Journal); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Resolve); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Resolve_Monitor); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_ManagedOOM); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_oom); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd); + print_separator(); + test_parse_format_one(&vl_interface_xyz_test); +} + +TEST(parse) { + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + + /* This one has (nested) enonymous enums and structs */ + static const char text[] = + "interface quu.waa\n" + "type Fooenum ( a, b, c )\n" + "type Barstruct ( a : (x, y, z), b : (x : int), c: (f, ff, fff), d: object, e : (sub : (subsub: (subsubsub: string, subsubsub2: (iii, ooo)))))" + ; + + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + test_parse_format_one(parsed); +} + +TEST(interface_name_is_valid) { + assert_se(!varlink_idl_interface_name_is_valid(NULL)); + assert_se(!varlink_idl_interface_name_is_valid("")); + assert_se(!varlink_idl_interface_name_is_valid(",")); + assert_se(!varlink_idl_interface_name_is_valid(".")); + assert_se(!varlink_idl_interface_name_is_valid("-")); + assert_se(varlink_idl_interface_name_is_valid("a")); + assert_se(varlink_idl_interface_name_is_valid("a.a")); + assert_se(!varlink_idl_interface_name_is_valid("-.a")); + assert_se(!varlink_idl_interface_name_is_valid("-a.a")); + assert_se(!varlink_idl_interface_name_is_valid("a-.a")); + assert_se(varlink_idl_interface_name_is_valid("a-a.a")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.a-")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.-a")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.-")); + assert_se(varlink_idl_interface_name_is_valid("a-a.a-a")); + assert_se(varlink_idl_interface_name_is_valid("io.systemd.Foobar")); +} + +TEST(symbol_name_is_valid) { + assert_se(!varlink_idl_symbol_name_is_valid(NULL)); + assert_se(!varlink_idl_symbol_name_is_valid("")); + assert_se(!varlink_idl_symbol_name_is_valid("_")); + assert_se(!varlink_idl_symbol_name_is_valid("_foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foofoo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo0")); + assert_se(!varlink_idl_symbol_name_is_valid("0Foo")); + assert_se(!varlink_idl_symbol_name_is_valid("foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo0foo")); + assert_se(!varlink_idl_symbol_name_is_valid("bool")); + assert_se(!varlink_idl_symbol_name_is_valid("int")); + assert_se(!varlink_idl_symbol_name_is_valid("float")); + assert_se(!varlink_idl_symbol_name_is_valid("string")); + assert_se(!varlink_idl_symbol_name_is_valid("object")); +} + +TEST(field_name_is_valid) { + assert_se(!varlink_idl_field_name_is_valid(NULL)); + assert_se(!varlink_idl_field_name_is_valid("")); + assert_se(!varlink_idl_field_name_is_valid("_")); + assert_se(!varlink_idl_field_name_is_valid("_foo")); + assert_se(!varlink_idl_field_name_is_valid("_foo_")); + assert_se(!varlink_idl_field_name_is_valid("foo_")); + assert_se(varlink_idl_field_name_is_valid("foo_foo")); + assert_se(varlink_idl_field_name_is_valid("f_o_o_f_o_o")); + assert_se(!varlink_idl_field_name_is_valid("foo__foo")); + assert_se(varlink_idl_field_name_is_valid("Foofoo")); + assert_se(varlink_idl_field_name_is_valid("Foo")); + assert_se(varlink_idl_field_name_is_valid("Foo0")); + assert_se(!varlink_idl_field_name_is_valid("0Foo")); + assert_se(varlink_idl_field_name_is_valid("foo")); + assert_se(varlink_idl_field_name_is_valid("Foo0foo")); + assert_se(varlink_idl_field_name_is_valid("foo0foo")); +} + +TEST(validate_json) { + + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + + /* This one has (nested) enonymous enums and structs */ + static const char text[] = + "interface validate.test\n" + "method Mymethod ( a:string, b:int, c:?bool, d:[]int, e:?[string]bool, f:?(piff, paff), g:(f:float) ) -> ()\n"; + + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + test_parse_format_one(parsed); + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + assert_se(json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("a", JSON_BUILD_STRING("x")), + JSON_BUILD_PAIR("b", JSON_BUILD_UNSIGNED(44)), + JSON_BUILD_PAIR("d", JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(5), JSON_BUILD_UNSIGNED(7), JSON_BUILD_UNSIGNED(107))), + JSON_BUILD_PAIR("g", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("f", JSON_BUILD_REAL(0.5f)))))) >= 0); + + json_variant_dump(v, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, stdout, NULL); + + const VarlinkSymbol* symbol = ASSERT_PTR(varlink_idl_find_symbol(parsed, VARLINK_METHOD, "Mymethod")); + + assert_se(varlink_idl_validate_method_call(symbol, v, NULL) >= 0); +} + +static int test_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + JsonVariant *foo = json_variant_by_key(parameters, "foo"), *bar = json_variant_by_key(parameters, "bar"); + + return varlink_replyb(link, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("waldo", json_variant_unsigned(foo) * json_variant_unsigned(bar)), + JSON_BUILD_PAIR_UNSIGNED("quux", json_variant_unsigned(foo) + json_variant_unsigned(bar)))); +} + +static int done_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + assert_se(sd_event_exit(varlink_get_event(link), 0) >= 0); + return 0; +} + +static VARLINK_DEFINE_METHOD( + TestMethod, + VARLINK_DEFINE_INPUT(foo, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(bar, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(optional, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(waldo, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(quux, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD(Done); + +static VARLINK_DEFINE_INTERFACE( + xyz, + "xyz", + &vl_method_TestMethod, + &vl_method_Done); + + +static void* server_thread(void *userdata) { + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + + assert_se(varlink_server_new(&server, 0) >= 0); + assert_se(varlink_server_add_interface(server, &vl_interface_xyz) >= 0); + assert_se(varlink_server_bind_method(server, "xyz.TestMethod", test_method) >= 0); + assert_se(varlink_server_bind_method(server, "xyz.Done", done_method) >= 0); + + assert_se(sd_event_new(&event) >= 0); + assert_se(varlink_server_attach_event(server, event, 0) >= 0); + + assert_se(varlink_server_add_connection(server, PTR_TO_FD(userdata), NULL) >= 0); + + assert_se(sd_event_loop(event) >= 0); + return NULL; +} + +TEST(validate_method_call) { + _cleanup_close_pair_ int fd[2] = PIPE_EBADF; + _cleanup_(varlink_unrefp) Varlink *v = NULL; + pthread_t t; + + assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, fd) >= 0); + assert_se(pthread_create(&t, NULL, server_thread, FD_TO_PTR(TAKE_FD(fd[1]))) == 0); + assert_se(varlink_connect_fd(&v, TAKE_FD(fd[0])) >= 0); + + JsonVariant *reply = NULL; + const char *error_id = NULL; + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 8), + JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0); + + _cleanup_(json_variant_unrefp) JsonVariant *expected_reply = NULL; + assert_se(json_build(&expected_reply, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("waldo", 8*9), + JSON_BUILD_PAIR_UNSIGNED("quux", 8+9))) >= 0); + + assert_se(!error_id); + + json_variant_dump(reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + json_variant_dump(expected_reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + assert_se(json_variant_equal(reply, expected_reply)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 9), + JSON_BUILD_PAIR_UNSIGNED("bar", 8), + JSON_BUILD_PAIR_STRING("optional", "pfft"))) >= 0); + + assert_se(!error_id); + assert_se(json_variant_equal(reply, expected_reply)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 8), + JSON_BUILD_PAIR_UNSIGNED("bar", 9), + JSON_BUILD_PAIR_STRING("zzz", "pfft"))) >= 0); + assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("foo", "wuff"), + JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0); + assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER)); + + assert_se(varlink_send(v, "xyz.Done", NULL) >= 0); + assert_se(varlink_flush(v) >= 0); + assert_se(pthread_join(t, NULL) == 0); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); From 47723340135f7d3fee0440647d4c3767fa3b4bf6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 15:49:06 +0200 Subject: [PATCH 11/18] varlink: add exit-on-idle logic for Varlink server This adds a logic that if enabled ensures sd_event_exit() is called whenever the varlink connection count hits zero. This is useful for implementing pure Varlink services (i.e. services whose only job is to serve Varlink requests), that shall run only as long as needed, i.e. as long as at least one request is being served. --- src/shared/varlink.c | 20 +++++++++++++++++++- src/shared/varlink.h | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index f52c961ce5..6015aed4cf 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -234,6 +234,8 @@ struct VarlinkServer { unsigned connections_max; unsigned connections_per_uid_max; + + bool exit_on_idle; }; static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { @@ -273,6 +275,7 @@ DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(varlink_state, VarlinkState); log_debug("%s: " fmt, varlink_server_description(s), ##__VA_ARGS__) static int varlink_format_queue(Varlink *v); +static void varlink_server_test_exit_on_idle(VarlinkServer *s); static const char *varlink_description(Varlink *v) { return (v ? v->description : NULL) ?: "varlink"; @@ -1654,6 +1657,7 @@ static void varlink_detach_server(Varlink *v) { if (saved_server->disconnect_callback) saved_server->disconnect_callback(saved_server, v, saved_server->userdata); + varlink_server_test_exit_on_idle(saved_server); varlink_server_unref(saved_server); varlink_unref(v); } @@ -3098,9 +3102,23 @@ int varlink_server_shutdown(VarlinkServer *s) { return 0; } +static void varlink_server_test_exit_on_idle(VarlinkServer *s) { + assert(s); + + if (s->exit_on_idle && s->event && s->n_connections == 0) + (void) sd_event_exit(s->event, 0); +} + +int varlink_server_set_exit_on_idle(VarlinkServer *s, bool b) { + assert_return(s, -EINVAL); + + s->exit_on_idle = b; + varlink_server_test_exit_on_idle(s); + return 0; +} + static int varlink_server_add_socket_event_source(VarlinkServer *s, VarlinkServerSocket *ss, int64_t priority) { _cleanup_(sd_event_source_unrefp) sd_event_source *es = NULL; - int r; assert(s); diff --git a/src/shared/varlink.h b/src/shared/varlink.h index d49a8510f0..143d82cabc 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -170,6 +170,8 @@ sd_event *varlink_server_get_event(VarlinkServer *v); int varlink_server_shutdown(VarlinkServer *server); +int varlink_server_set_exit_on_idle(VarlinkServer *s, bool b); + unsigned varlink_server_connections_max(VarlinkServer *s); unsigned varlink_server_connections_per_uid_max(VarlinkServer *s); From 206504a594492f4b97259bbe345dde4e520df7dc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 15:51:26 +0200 Subject: [PATCH 12/18] varlink: add varlink_server_listen_auto() helper This new helper will automatically take listening fds passed in from the service manager and processes varlink on them. It's useful for Varlink services that shall be socket activatable. --- src/shared/varlink.c | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/shared/varlink.h | 1 + 2 files changed, 46 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 6015aed4cf..229d97febf 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -3,6 +3,8 @@ #include #include +#include + #include "alloc-util.h" #include "errno-util.h" #include "fd-util.h" @@ -3061,6 +3063,49 @@ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t return 0; } +int varlink_server_listen_auto(VarlinkServer *s) { + _cleanup_strv_free_ char **names = NULL; + int r, n = 0; + + assert_return(s, -EINVAL); + + /* Adds all passed fds marked as "varlink" to our varlink server. These fds can either refer to a + * listening socket or to a connection socket. + * + * See https://varlink.org/#activation for the environment variables this is backed by and the + * recommended "varlink" identifier in $LISTEN_FDNAMES. */ + + r = sd_listen_fds_with_names(/* unset_environment= */ false, &names); + if (r < 0) + return r; + + for (int i = 0; i < r; i++) { + int b, fd; + socklen_t l = sizeof(b); + + if (!streq(names[i], "varlink")) + continue; + + fd = SD_LISTEN_FDS_START + i; + + if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &b, &l) < 0) + return -errno; + + assert(l == sizeof(b)); + + if (b) /* Listening socket? */ + r = varlink_server_listen_fd(s, fd); + else /* Otherwise assume connection socket */ + r = varlink_server_add_connection(s, fd, NULL); + if (r < 0) + return r; + + n++; + } + + return n; +} + void* varlink_server_set_userdata(VarlinkServer *s, void *userdata) { void *ret; diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 143d82cabc..776904890d 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -147,6 +147,7 @@ VarlinkServer *varlink_server_unref(VarlinkServer *s); /* Add addresses or fds to listen on */ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t mode); int varlink_server_listen_fd(VarlinkServer *s, int fd); +int varlink_server_listen_auto(VarlinkServer *s); int varlink_server_add_connection(VarlinkServer *s, int fd, Varlink **ret); /* Bind callbacks */ From 2e5edb93b7e55de6465954d6fa03a384e52b00ae Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 15:57:39 +0200 Subject: [PATCH 13/18] varlink: add new call varlink_server_loop_auto() This is a helper call that runs the specified VarlinkServer object in an event loop, and exits once no more connections exist. This is useful for pure varlink servers (i.e. those which only server varlink requests and do nothing else), to run as long as there's something to do and exit right after. --- src/shared/varlink.c | 28 ++++++++++++++++++++++++++++ src/shared/varlink.h | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 229d97febf..e97127838a 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -3123,6 +3123,34 @@ void* varlink_server_get_userdata(VarlinkServer *s) { return s->userdata; } +int varlink_server_loop_auto(VarlinkServer *server) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + assert_return(server, -EINVAL); + assert_return(!server->event, -EBUSY); + + /* Runs a Varlink service event loop populated with a passed fd. Exits on the last connection. */ + + r = sd_event_new(&event); + if (r < 0) + return r; + + r = varlink_server_set_exit_on_idle(server, true); + if (r < 0) + return r; + + r = varlink_server_attach_event(server, event, 0); + if (r < 0) + return r; + + r = varlink_server_listen_auto(server); + if (r < 0) + return r; + + return sd_event_loop(event); +} + static VarlinkServerSocket* varlink_server_socket_destroy(VarlinkServerSocket *ss) { if (!ss) return NULL; diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 776904890d..cfb7f7ed8b 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -169,6 +169,8 @@ int varlink_server_attach_event(VarlinkServer *v, sd_event *e, int64_t priority) int varlink_server_detach_event(VarlinkServer *v); sd_event *varlink_server_get_event(VarlinkServer *v); +int varlink_server_loop_auto(VarlinkServer *server); + int varlink_server_shutdown(VarlinkServer *server); int varlink_server_set_exit_on_idle(VarlinkServer *s, bool b); From 02b0d24d1e30331004ee15a4aaccc20ac1d87553 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 16:00:00 +0200 Subject: [PATCH 14/18] varlink: add varlink_invocation() call This call checks if we are invoked in a socket-activation Varlink server context. It's useful for commands that can be run from the command line or as Varlink service and then either serve commands from the cmdline or those from Varlink. --- src/shared/varlink.c | 35 +++++++++++++++++++++++++++++++++++ src/shared/varlink.h | 10 +++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index e97127838a..f061024785 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -3540,3 +3540,38 @@ int varlink_server_deserialize_one(VarlinkServer *s, const char *value, FDSet *f LIST_PREPEND(sockets, s->sockets, TAKE_PTR(ss)); return 0; } + +int varlink_invocation(VarlinkInvocationFlags flags) { + _cleanup_strv_free_ char **names = NULL; + int r, b; + socklen_t l = sizeof(b); + + /* Returns true if this is a "pure" varlink server invocation, i.e. with one fd passed. */ + + r = sd_listen_fds_with_names(/* unset_environment= */ false, &names); + if (r < 0) + return r; + if (r == 0) + return false; + if (r > 1) + return -ETOOMANYREFS; + + if (!strv_equal(names, STRV_MAKE("varlink"))) + return false; + + if (FLAGS_SET(flags, VARLINK_ALLOW_LISTEN|VARLINK_ALLOW_ACCEPT)) /* Both flags set? Then allow everything */ + return true; + + if ((flags & (VARLINK_ALLOW_LISTEN|VARLINK_ALLOW_ACCEPT)) == 0) /* Neither is set, then fail */ + return -EISCONN; + + if (getsockopt(SD_LISTEN_FDS_START, SOL_SOCKET, SO_ACCEPTCONN, &b, &l) < 0) + return -errno; + + assert(l == sizeof(b)); + + if (!FLAGS_SET(flags, b ? VARLINK_ALLOW_LISTEN : VARLINK_ALLOW_ACCEPT)) + return -EISCONN; + + return true; +} diff --git a/src/shared/varlink.h b/src/shared/varlink.h index cfb7f7ed8b..5fe529c853 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -46,7 +46,6 @@ typedef enum VarlinkServerFlags { VARLINK_SERVER_MYSELF_ONLY = 1 << 1, /* Only accessible by our own UID */ VARLINK_SERVER_ACCOUNT_UID = 1 << 2, /* Do per user accounting */ VARLINK_SERVER_INHERIT_USERDATA = 1 << 3, /* Initialize Varlink connection userdata from VarlinkServer userdata */ - _VARLINK_SERVER_FLAGS_ALL = (1 << 4) - 1, } VarlinkServerFlags; @@ -185,6 +184,15 @@ unsigned varlink_server_current_connections(VarlinkServer *s); int varlink_server_set_description(VarlinkServer *s, const char *description); +typedef enum VarlinkInvocationFlags { + VARLINK_ALLOW_LISTEN = 1 << 0, + VARLINK_ALLOW_ACCEPT = 1 << 1, + _VARLINK_SERVER_INVOCATION_FLAGS_MAX = (1 << 2) - 1, + _VARLINK_SERVER_INVOCATION_FLAGS_INVALID = -EINVAL, +} VarlinkInvocationFlags; + +int varlink_invocation(VarlinkInvocationFlags flags); + DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_flush_close_unref); From d408a53f78b04a2f4a0a460e147626ae59118f01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 22 Sep 2023 22:44:28 +0200 Subject: [PATCH 15/18] varlinkctl: add new varlinkctl tool --- man/busctl.xml | 1 + man/rules/meson.build | 1 + man/varlinkctl.xml | 315 ++++++++++++++++++++++ meson.build | 1 + src/varlinkctl/meson.build | 13 + src/varlinkctl/varlinkctl.c | 520 ++++++++++++++++++++++++++++++++++++ 6 files changed, 851 insertions(+) create mode 100644 man/varlinkctl.xml create mode 100644 src/varlinkctl/meson.build create mode 100644 src/varlinkctl/varlinkctl.c diff --git a/man/busctl.xml b/man/busctl.xml index ce23dd1b33..cc404529ae 100644 --- a/man/busctl.xml +++ b/man/busctl.xml @@ -582,6 +582,7 @@ o "/org/freedesktop/systemd1/job/42684" dbus-daemon1, D-Bus, sd-bus3, + varlinkctl1, systemd1, machinectl1, wireshark1 diff --git a/man/rules/meson.build b/man/rules/meson.build index 3454668cab..3265db9c2d 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1249,6 +1249,7 @@ manpages = [ ['systemd-user-runtime-dir', 'user-runtime-dir@.service'], ''], ['userdbctl', '1', [], 'ENABLE_USERDB'], + ['varlinkctl', '1', [], ''], ['vconsole.conf', '5', [], 'ENABLE_VCONSOLE'], ['veritytab', '5', [], 'HAVE_LIBCRYPTSETUP'] ] diff --git a/man/varlinkctl.xml b/man/varlinkctl.xml new file mode 100644 index 0000000000..5d0ec6ace7 --- /dev/null +++ b/man/varlinkctl.xml @@ -0,0 +1,315 @@ + + + + + + + + varlinkctl + systemd + + + + varlinkctl + 1 + + + + varlinkctl + Introspect with and invoke Varlink services + + + + + varlinkctl + OPTIONS + info + ADDRESS + + + + varlinkctl + OPTIONS + list-interfaces + ADDRESS + + + + varlinkctl + OPTIONS + introspect + ADDRESS + INTERFACE + + + + varlinkctl + OPTIONS + call + ADDRESS + METHOD + PARAMETERS + + + + varlinkctl + OPTIONS + validate-idl + FILE + + + + + Description + + varlinkctl may be used to introspect and invoke Varlink services. + + Services are referenced by one of the following: + + + A Varlink service reference starting with the unix: string, followed + by an absolute AF_UNIX path, or by @ and an arbitrary string + (the latter for referencing sockets in the abstract namespace). + + A Varlink service reference starting with the exec: string, followed + by an absolute path of a binary to execute. + + + For convenience these two simpler (redundant) service address syntaxes are also supported: + + + A file system path to an AF_UNIX socket, either absolute + (i.e. begins with /) or relative (in which case it must begin with + ./). + + A file system path to an executable, either absolute or relative (as above, must begin + with /, resp. ./). + + + + + Commands + + The following commands are understood: + + + + info ADDRESS + + Show brief information about the specified service, including vendor name and list of + implemented interfaces. Expects a service address in the formats described above. + + + + + + list-interfaces ADDRESS + + Show list of interfaces implemented by the specified service. Expects a service + address in the formats described above. + + + + + + introspect ADDRESS INTERFACE + + Show interface definition of the specified interface provided by the specified + service. Expects a service address in the formats described above and a Varlink interface + name. + + + + + + call ADDRESS METHOD [ARGUMENTS] + + Call the specified method of the specified service. Expects a service address in the + format described above, a fully qualified Varlink method name, and a JSON arguments object. If the + arguments object is not specified, it is read from STDIN instead. To pass an empty list of + parameters, specify the empty object {}. + + The reply parameters are written as JSON object to STDOUT. + + + + + + validate-idl [FILE] + + Reads a Varlink interface definition file, parses and validates it, then outputs it + with syntax highlighting. This checks for syntax and internal consistency of the interface. Expects a + file name to read the interface definition from. If omitted reads the interface definition from + STDIN. + + + + + + help + + Show command syntax help. + + + + + + + + Options + + The following options are understood: + + + + + + When used with call: expect multiple method replies. If this flag is + set the method call is sent with the more flag set, which tells the service to + generate multiple replies, if needed. The command remains running until the service sends a reply + message that indicates it is the last in the series. This flag should be set only for method calls + that support this mechanism. + + If this mode is enabled output is automatically switched to JSON-SEQ mode, so that individual + reply objects can be easily discerned. + + + + + + + + When used with call: do not expect a method reply. If this flag + is set the method call is sent with the oneway flag set (the command exits + immediately after), which tells the service not to generate a reply. + + + + + + MODE + + + Selects the JSON output formatting, one of pretty (for nicely indented, + colorized output) or short (for terse output with minimal whitespace and no + newlines), defaults to short. + + + + + + + + + + Equivalent to when invoked interactively from a terminal. Otherwise + equivalent to , in particular when the output is piped to some other + program. + + + + + + + + + + + + + Examples + + + Investigating a Service + + The following three commands inspect the io.systemd.Resolve service + implemented by + systemd-resolved.service8, + listing general service information and implemented interfaces, and then displaying the interface + definition of its primary interface: + + $ varlinkctl info /run/systemd/resolve/io.systemd.Resolve + Vendor: The systemd Project + Product: systemd (systemd-resolved) + Version: 254 (254-1522-g4790521^) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.Resolve + org.varlink.service +$ varlinkctl list-interfaces /run/systemd/resolve/io.systemd.Resolve +io.systemd +io.systemd.Resolve +org.varlink.service +$ varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve +interface io.systemd.Resolve +type ResolvedAddress( + ifindex: ?int, + … + + (Interface definition has been truncated in the example above, in the interest of brevity.) + + + + Invoking a Method + + The following command resolves a hostname via systemd-resolved.service8's ResolveHostname method call. + + $ varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name":"systemd.io","family":2}' -j +{ + "addresses" : [ + { + "ifindex" : 2, + "family" : 2, + "address" : [ + 185, + 199, + 111, + 153 + ] + } + ], + "name" : "systemd.io", + "flags" : 1048577 +} + + + + Investigating a Service Executable + + The following comand inspects the /usr/lib/systemd/systemd-pcrextend + executable and the IPC APIs it provides. It then invokes a method on it: + + # varlinkctl info /usr/lib/systemd/systemd-pcrextend + Vendor: The systemd Project + Product: systemd (systemd-pcrextend) + Version: 254 (254-1536-g97734fb) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.PCRExtend + org.varlink.service +# varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend +interface io.systemd.PCRExtend + +method Extend( + pcr: int, + text: ?string, + data: ?string +) -> () +# varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"pcr":15,"text":"foobar"}' +{} + + + + + + See Also + + + busctl1, + Varlink + + + diff --git a/meson.build b/meson.build index 88bc1367e0..5e9ca285ac 100644 --- a/meson.build +++ b/meson.build @@ -2193,6 +2193,7 @@ subdir('src/update-done') subdir('src/update-utmp') subdir('src/user-sessions') subdir('src/userdb') +subdir('src/varlinkctl') subdir('src/vconsole') subdir('src/veritysetup') subdir('src/volatile-root') diff --git a/src/varlinkctl/meson.build b/src/varlinkctl/meson.build new file mode 100644 index 0000000000..c1074dc50a --- /dev/null +++ b/src/varlinkctl/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +varlinkctl_sources = files( + 'varlinkctl.c', +) + +executables += [ + executable_template + { + 'name' : 'varlinkctl', + 'public' : true, + 'sources' : varlinkctl_sources, + }, +] diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c new file mode 100644 index 0000000000..a656bfcaf0 --- /dev/null +++ b/src/varlinkctl/varlinkctl.c @@ -0,0 +1,520 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "build.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "main-func.h" +#include "pager.h" +#include "parse-argument.h" +#include "path-util.h" +#include "pretty-print.h" +#include "terminal-util.h" +#include "varlink.h" +#include "verbs.h" +#include "version.h" + +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static VarlinkMethodFlags arg_method_flags = 0; + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("varlinkctl", "1", &link); + if (r < 0) + return log_oom(); + + pager_open(arg_pager_flags); + + printf("%1$s [OPTIONS...] COMMAND ...\n\n" + "%5$sIntrospect Varlink Services.%6$s\n" + "\n%3$sCommands:%4$s\n" + " info ADDRESS Show service information\n" + " list-interfaces ADDRESS\n" + " List interfaces implemented by service\n" + " introspect ADDRESS INTERFACE\n" + " Show interface definition\n" + " call ADDRESS METHOD [PARAMS]\n" + " Invoke method\n" + " validate-idl [FILE] Validate interface description\n" + " help Show this help\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --more Request multiple responses\n" + " --oneway Do not request response\n" + " --json=MODE Output as JSON\n" + " -j Same as --json=pretty on tty, --json=short otherwise\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + return help(); +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_MORE, + ARG_ONEWAY, + ARG_JSON, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "more", no_argument, NULL, ARG_MORE }, + { "oneway", no_argument, NULL, ARG_ONEWAY }, + { "json", required_argument, NULL, ARG_JSON }, + {}, + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_MORE: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_ONEWAY) | VARLINK_METHOD_MORE; + break; + + case ARG_ONEWAY: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_MORE) | VARLINK_METHOD_ONEWAY; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'j': + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + /* If more than one reply is expected, imply JSON-SEQ output */ + if (FLAGS_SET(arg_method_flags, VARLINK_METHOD_MORE)) + arg_json_format_flags |= JSON_FORMAT_SEQ; + + return 1; +} + +static int varlink_connect_auto(Varlink **ret, const char *where) { + int r; + + assert(ret); + assert(where); + + if (STARTSWITH_SET(where, "/", "./")) { /* If the string starts with a slash or dot slash we use it as a file system path */ + _cleanup_close_ int fd = -EBADF; + struct stat st; + + fd = open(where, O_PATH|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", where); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", where); + + /* Is this a socket in the fs? Then connect() to it. */ + if (S_ISSOCK(st.st_mode)) { + r = varlink_connect_address(ret, FORMAT_PROC_FD_PATH(fd)); + if (r < 0) + return log_error_errno(r, "Failed to connect to '%s': %m", where); + + return 0; + } + + /* Is this an executable binary? Then fork it off. */ + if (S_ISREG(st.st_mode) && (st.st_mode & 0111)) { + r = varlink_connect_exec(ret, where, STRV_MAKE(where)); /* Ideally we'd use FORMAT_PROC_FD_PATH(fd) here too, but that breaks the #! logic */ + if (r < 0) + return log_error_errno(r, "Failed to spawn '%s' process: %m", where); + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unrecognized path '%s' is neither an AF_UNIX socket, nor an executable binary.", where); + } + + /* Otherwise assume this is an URL */ + r = varlink_connect_url(ret, where); + if (r < 0) + return log_error_errno(r, "Failed to connect to URL '%s': %m", where); + + return 0; +} + +typedef struct GetInfoData { + const char *vendor; + const char *product; + const char *version; + const char *url; + char **interfaces; +} GetInfoData; + +static void get_info_data_done(GetInfoData *d) { + assert(d); + + d->interfaces = strv_free(d->interfaces); +} + +static int verb_info(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url; + int r; + + assert(argc == 2); + url = argv[1]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_call(vl, "org.varlink.service.GetInfo", NULL, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInfo() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInfo() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "vendor", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, vendor), JSON_MANDATORY }, + { "product", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, product), JSON_MANDATORY }, + { "version", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, version), JSON_MANDATORY }, + { "url", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, url), JSON_MANDATORY }, + { "interfaces", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(GetInfoData, interfaces), JSON_MANDATORY }, + {} + }; + _cleanup_(get_info_data_done) GetInfoData data = {}; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &data); + if (r < 0) + return r; + + strv_sort(data.interfaces); + + if (streq_ptr(argv[0], "list-interfaces")) { + STRV_FOREACH(i, data.interfaces) + puts(*i); + } else { + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new_vertical(); + if (!t) + return log_oom(); + + r = table_add_many( + t, + TABLE_FIELD, "Vendor", + TABLE_STRING, data.vendor, + TABLE_FIELD, "Product", + TABLE_STRING, data.product, + TABLE_FIELD, "Version", + TABLE_STRING, data.version, + TABLE_FIELD, "URL", + TABLE_STRING, data.url, + TABLE_SET_URL, data.url, + TABLE_FIELD, "Interfaces", + TABLE_STRV, data.interfaces); + if (r < 0) + return table_log_add_error(r); + + r = table_print(t, NULL); + if (r < 0) + return table_log_print_error(r); + } + } else { + JsonVariant *v; + + v = streq_ptr(argv[0], "list-interfaces") ? + json_variant_by_key(reply, "interfaces") : reply; + + json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; +} + +typedef struct GetInterfaceDescriptionData { + const char *description; +} GetInterfaceDescriptionData; + +static int verb_introspect(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *interface; + int r; + + assert(argc == 3); + url = argv[1]; + interface = argv[2]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_callb(vl, "org.varlink.service.GetInterfaceDescription", &reply, &error, NULL, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("interface", interface))); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInterfaceDescription() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInterfaceDescription() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "description", JSON_VARIANT_STRING, json_dispatch_const_string, 0, JSON_MANDATORY }, + {} + }; + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + const char *description = NULL; + unsigned line = 0, column = 0; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &description); + if (r < 0) + return r; + + /* Try to parse the returned description, so that we can add syntax highlighting */ + r = varlink_idl_parse(ASSERT_PTR(description), &line, &column, &vi); + if (r < 0) { + log_warning_errno(r, "Failed to parse returned interface description at %u:%u, showing raw interface description: %m", line, column); + + fputs(description, stdout); + if (!endswith(description, "\n")) + fputs("\n", stdout); + } else { + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + } + } else + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + + return 0; +} + +static int reply_callback( + Varlink *link, + JsonVariant *parameters, + const char *error, + VarlinkReplyFlags flags, + void *userdata) { + + int r; + + assert(link); + + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call failed: %s", error); + } else + r = 0; + + json_variant_dump(parameters, arg_json_format_flags, stdout, NULL); + return r; +} + +static int verb_call(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *jp = NULL; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *method, *parameter; + unsigned line = 0, column = 0; + int r; + + assert(argc >= 3); + assert(argc <= 4); + url = argv[1]; + method = argv[2]; + parameter = argc > 3 && !streq(argv[3], "-") ? argv[3] : NULL; + + arg_json_format_flags &= ~JSON_FORMAT_OFF; + + if (parameter) { + r = json_parse_with_source(parameter, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } else { + r = json_parse_file_at(stdin, AT_FDCWD, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + if (arg_method_flags & VARLINK_METHOD_ONEWAY) { + r = varlink_send(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + r = varlink_flush(vl); + if (r < 0) + return log_error_errno(r, "Failed to flush Varlink connection: %m"); + + } else if (arg_method_flags & VARLINK_METHOD_MORE) { + + varlink_set_userdata(vl, (void*) method); + + r = varlink_bind_reply(vl, reply_callback); + if (r < 0) + return log_error_errno(r, "Failed to bind reply callback: %m"); + + r = varlink_observe(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + for (;;) { + r = varlink_is_idle(vl); + if (r < 0) + return log_error_errno(r, "Failed to check if varlink connection is idle: %m"); + if (r > 0) + break; + + r = varlink_process(vl); + if (r < 0) + return log_error_errno(r, "Failed to process varlink connection: %m"); + if (r != 0) + continue; + + r = varlink_wait(vl, USEC_INFINITY); + if (r < 0) + return log_error_errno(r, "Failed to wait for varlink connection events: %m"); + } + } else { + JsonVariant *reply = NULL; + const char *error = NULL; + + r = varlink_call(vl, method, jp, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + /* If the server returned an error to us, then fail, but first output the associated parameters */ + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call %s() failed: %s", method, error); + } else + r = 0; + + pager_open(arg_pager_flags); + + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + return r; + } + + return 0; +} + +static int verb_validate_idl(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + _cleanup_free_ char *text = NULL; + const char *fname; + unsigned line = 1, column = 1; + int r; + + fname = argc > 1 ? argv[1] : NULL; + + if (fname) { + r = read_full_file(fname, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description file '%s': %m", fname); + } else { + r = read_full_stream(stdin, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description from stdin: %m"); + + fname = ""; + } + + r = varlink_idl_parse(text, &line, &column, &vi); + if (r < 0) + return log_error_errno(r, "%s:%u:%u: Failed to parse interface description: %m", fname, line, column); + + r = varlink_idl_consistent(vi, LOG_ERR); + if (r < 0) + return r; + + pager_open(arg_pager_flags); + + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + + return 0; +} + +static int varlinkctl_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "info", 2, 2, 0, verb_info }, + { "list-interfaces", 2, 2, 0, verb_info }, + { "introspect", 3, 3, 0, verb_introspect }, + { "call", 3, 4, 0, verb_call }, + { "validate-idl", 1, 2, 0, verb_validate_idl }, + { "help", VERB_ANY, VERB_ANY, 0, verb_help }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return varlinkctl_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); From 4e16d5c69e2b735a408675a42134a7bf9620756a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 25 Sep 2023 16:28:24 +0200 Subject: [PATCH 16/18] pcrextend: make pcrextend tool acccessible via varlink This is primarily supposed to be a 1st step with varlinkifying our various command line tools, and excercise in how this might look like across our codebase one day. However, at AllSystemsGo! 2023 it was requested that we provide an API to do a PCR measurement along with a matching event log record, and this provides that. --- src/pcrextend/pcrextend.c | 177 ++++++++++++++++++---- src/shared/meson.build | 1 + src/shared/varlink-io.systemd.PCRExtend.c | 14 ++ src/shared/varlink-io.systemd.PCRExtend.h | 6 + src/test/test-varlink-idl.c | 3 + units/meson.build | 9 ++ units/systemd-pcrextend.socket | 24 +++ units/systemd-pcrextend@.service.in | 19 +++ 8 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 src/shared/varlink-io.systemd.PCRExtend.c create mode 100644 src/shared/varlink-io.systemd.PCRExtend.h create mode 100644 units/systemd-pcrextend.socket create mode 100644 units/systemd-pcrextend@.service.in diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 378d1b0e8f..c44e78c4ca 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -21,6 +21,8 @@ #include "pretty-print.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "varlink.h" +#include "varlink-io.systemd.PCRExtend.h" static bool arg_graceful = false; static char *arg_tpm2_device = NULL; @@ -28,11 +30,14 @@ static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; static unsigned arg_pcr_index = UINT_MAX; +static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); +#define EXTENSION_STRING_SAFE_LIMIT 1024 + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -165,7 +170,12 @@ static int parse_argv(int argc, char *argv[]) { if (arg_file_system && arg_machine_id) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined."); - if (arg_pcr_index == UINT_MAX) + r = varlink_invocation(VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + arg_varlink = true; + else if (arg_pcr_index == UINT_MAX) arg_pcr_index = (arg_file_system || arg_machine_id) ? TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */ @@ -257,10 +267,119 @@ static int get_file_system_word( return 0; } +static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2UserspaceEventType event) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + int r; + + r = tpm2_context_new(arg_tpm2_device, &c); + if (r < 0) + return r; + + r = determine_banks(c, pcr); + if (r < 0) + return r; + if (strv_isempty(arg_banks)) /* Still none? */ + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); + + _cleanup_free_ char *joined_banks = NULL; + joined_banks = strv_join(arg_banks, ", "); + if (!joined_banks) + return log_oom(); + + _cleanup_free_ char *safe = NULL; + if (size > EXTENSION_STRING_SAFE_LIMIT) { + safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT); + if (!safe) + return log_oom(); + + if (!strextend(&safe, "...")) + return log_oom(); + } else { + safe = cescape_length(data, size); + if (!safe) + return log_oom(); + } + + log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); + + r = tpm2_extend_bytes(c, arg_banks, pcr, data, size, /* secret= */ NULL, /* secret_size= */ 0, event, safe); + if (r < 0) + return log_error_errno(r, "Could not extend PCR: %m"); + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, + LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", pcr, safe, joined_banks), + "MEASURING=%s", safe, + "PCR=%u", pcr, + "BANKS=%s", joined_banks); + + return 0; +} + +typedef struct MethodExtendParameters { + unsigned pcr; + const char *text; + void *data; + size_t data_size; +} MethodExtendParameters; + +static int json_dispatch_binary_data(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + MethodExtendParameters *p = ASSERT_PTR(userdata); + _cleanup_free_ void *d = NULL; + size_t l; + int r; + + r = json_variant_unbase64(variant, &d, &l); + if (r < 0) + return json_log(variant, flags, r, "JSON variant is not a base64 string."); + + free_and_replace(p->data, d); + p->data_size = l; + + return 0; +} + +static int vl_method_extend(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "pcr", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MethodExtendParameters, pcr), JSON_MANDATORY }, + { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, + { "data", JSON_VARIANT_STRING, json_dispatch_binary_data, 0, 0 }, + {} + }; + MethodExtendParameters p = { + .pcr = UINT_MAX, + }; + int r; + + assert(link); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!TPM2_PCR_INDEX_VALID(p.pcr)) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "pcr"))); + + if (p.text) { + /* Specifying both the text string and the binary data is not allowed */ + if (p.data) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "data"))); + + r = extend_now(p.pcr, p.text, strlen(p.text), _TPM2_USERSPACE_EVENT_TYPE_INVALID); + } else if (p.data) + r = extend_now(p.pcr, p.data, p.data_size, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + else + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "text"))); + if (r < 0) + return r; + + return varlink_reply(link, NULL); +} + static int run(int argc, char *argv[]) { - _cleanup_free_ char *joined = NULL, *word = NULL; + _cleanup_free_ char *word = NULL; Tpm2UserspaceEventType event; - size_t length; int r; log_setup(); @@ -269,6 +388,30 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) { + _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; + + /* Invocation as Varlink service */ + + r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRExtend); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = varlink_server_bind_method(varlink_server, "io.systemd.PCRExtend.Extend", vl_method_extend); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink method: %m"); + + r = varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return EXIT_SUCCESS; + } + if (arg_file_system) { _cleanup_free_ char *normalized = NULL, *normalized_escaped = NULL; _cleanup_(sd_device_unrefp) sd_device *d = NULL; @@ -348,8 +491,6 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - length = strlen(word); - /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */ r = efi_measured_uki(LOG_ERR); if (r < 0) @@ -359,34 +500,10 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; - r = tpm2_context_new(arg_tpm2_device, &c); + r = extend_now(arg_pcr_index, word, strlen(word), event); if (r < 0) return log_error_errno(r, "Failed to create TPM2 context: %m"); - r = determine_banks(c, arg_pcr_index); - if (r < 0) - return r; - if (strv_isempty(arg_banks)) /* Still none? */ - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); - - joined = strv_join(arg_banks, ", "); - if (!joined) - return log_oom(); - - log_debug("Measuring '%s' into PCR index %u, banks %s.", word, arg_pcr_index, joined); - - r = tpm2_extend_bytes(c, arg_banks, arg_pcr_index, word, length, NULL, 0, event, word); - if (r < 0) - return log_error_errno(r, "Could not extend PCR: %m"); - - log_struct(LOG_INFO, - "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, - LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", arg_pcr_index, word, joined), - "MEASURING=%s", word, - "PCR=%u", arg_pcr_index, - "BANKS=%s", joined); - return EXIT_SUCCESS; } diff --git a/src/shared/meson.build b/src/shared/meson.build index 9d29c32dff..569b865636 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -170,6 +170,7 @@ shared_sources = files( 'varlink-io.systemd.c', 'varlink-io.systemd.Journal.c', 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.Resolve.Monitor.c', 'varlink-io.systemd.Resolve.c', 'varlink-io.systemd.UserDatabase.c', diff --git a/src/shared/varlink-io.systemd.PCRExtend.c b/src/shared/varlink-io.systemd.PCRExtend.c new file mode 100644 index 0000000000..37d403f0ee --- /dev/null +++ b/src/shared/varlink-io.systemd.PCRExtend.c @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.PCRExtend.h" + +static VARLINK_DEFINE_METHOD( + Extend, + VARLINK_DEFINE_INPUT(pcr, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(text, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(data, VARLINK_STRING, VARLINK_NULLABLE)); + +VARLINK_DEFINE_INTERFACE( + io_systemd_PCRExtend, + "io.systemd.PCRExtend", + &vl_method_Extend); diff --git a/src/shared/varlink-io.systemd.PCRExtend.h b/src/shared/varlink-io.systemd.PCRExtend.h new file mode 100644 index 0000000000..ffc075af2c --- /dev/null +++ b/src/shared/varlink-io.systemd.PCRExtend.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_PCRExtend; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index cd85e2c106..a93d717f3f 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -10,6 +10,7 @@ #include "varlink-io.systemd.h" #include "varlink-io.systemd.Journal.h" #include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.PCRExtend.h" #include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.Resolve.h" #include "varlink-io.systemd.UserDatabase.h" @@ -134,6 +135,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_PCRExtend); + print_separator(); test_parse_format_one(&vl_interface_xyz_test); } diff --git a/units/meson.build b/units/meson.build index e1de2b4a6d..b089cde811 100644 --- a/units/meson.build +++ b/units/meson.build @@ -430,6 +430,15 @@ units = [ 'file' : 'systemd-oomd.socket', 'conditions' : ['ENABLE_OOMD'], }, + { + 'file' : 'systemd-pcrextend@.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrextend.socket', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + 'symlinks' : ['sockets.target.wants/'], + }, { 'file' : 'systemd-pcrfs-root.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], diff --git a/units/systemd-pcrextend.socket b/units/systemd-pcrextend.socket new file mode 100644 index 0000000000..6d7b8ff84c --- /dev/null +++ b/units/systemd-pcrextend.socket @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM2 PCR Extension (Varlink) +Documentation=man:systemd-pcrextend(8) +DefaultDependencies=no +Before=sockets.target +ConditionSecurity=measured-uki + +[Socket] +ListenStream=/run/systemd/io.systemd.PCRExtend +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-pcrextend@.service.in b/units/systemd-pcrextend@.service.in new file mode 100644 index 0000000000..2305b1cd4c --- /dev/null +++ b/units/systemd-pcrextend@.service.in @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM2 PCR Extension (Varlink) +Documentation=man:systemd-pcrphase.service(8) +DefaultDependencies=no +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target + +[Service] +Environment=LISTEN_FDNAMES=varlink +ExecStart=-{{LIBEXECDIR}}/systemd-pcrextend From 8525de13651362800a94849e03386e40c41d9404 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 26 Sep 2023 11:44:28 +0200 Subject: [PATCH 17/18] test: extend some existing tests with varlinkctl lines, to test varlinkctl + new PCRExtend service --- test/units/testsuite-04.journal.sh | 7 ++++++- test/units/testsuite-46.sh | 8 ++++++++ test/units/testsuite-70.sh | 11 ++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/test/units/testsuite-04.journal.sh b/test/units/testsuite-04.journal.sh index 1e105a0566..3efac616e0 100755 --- a/test/units/testsuite-04.journal.sh +++ b/test/units/testsuite-04.journal.sh @@ -10,8 +10,12 @@ for ((i = 0; i < ITERATIONS; i++)); do dd if=/dev/urandom bs=1M count=1 | base64 | systemd-cat done journalctl --rotate +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --rotate' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Rotate '{}' journalctl --flush +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.FlushToVar '{}' journalctl --sync +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' journalctl --rotate --vacuum-size=8M # Reset the ratelimit buckets for the subsequent tests below. @@ -42,7 +46,8 @@ write_and_match "<5> \t Leading spaces\n" " \t Leading spaces\n" --level-prefix # --output-fields restricts output ID="$(systemd-id128 new)" echo -ne "foo" | systemd-cat -t "$ID" --level-prefix false -journalctl --sync +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --sync' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' journalctl -b -o export --output-fields=MESSAGE,FOO --output-fields=PRIORITY,MESSAGE -t "$ID" >/tmp/output [[ $(wc -l /run/systemd/system/systemd-pcrextend.socket.d/50-no-condition.conf < Date: Fri, 22 Sep 2023 22:47:45 +0200 Subject: [PATCH 18/18] update TODO --- TODO | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/TODO b/TODO index 8111703d16..fde2013c83 100644 --- a/TODO +++ b/TODO @@ -148,6 +148,24 @@ Features: hostname pattern and has a ProxyCommand set that allows connecting to any local VM/container that way without any networking configured. +* Varlinkification of the following command line tools, to open them up to + other programs via IPC: + - bootctl + - journalctl (allowing journal read access via IPC) + - coredumpcl + - systemd-bless-boot + - systemd-measure + - systemd-creds (allowing clients to encrypt credentials locally) + - systemd-cryptenroll (to allow UIs to enroll FIDO2 keys and such) + - systemd-dissect + - systemd-sysext + - systemd-sysupdate + - systemd-analyze + - kernel-install + +* Varlink: add glue code to allow varlink clients to be authenticated via + Polkit by passing client pidfd over. + * in the service manager, pick up ERRNO= + BUSERROR= + VARLINKERROR= error identifiers, and store them along with the exit status of a server and report via "systemctl status". @@ -159,32 +177,15 @@ Features: * automatically mount one virtiofs during early boot phase to /run/host/, similar to how we do that for nspawn, based on some clear tag. -* make systemd-pcrextend accessible via varlink so that clients can measure - things reasonably freely, and get a proper entry in the TPM event log we - maintain, correctly synchronized - * add some service that makes an atomic snapshot of PCR state and event log up to that point available, possibly even with quote by the TPM. * encode type1 entries in some UKI section to add additional entries to the menu. -* extend the various CLI tools we have that output JSON already to also read - their command to execute from JSON in varlink format, so that you can fork - them off and talk to them fully via varlink. - -* add a "varlinkctl" tool that allows interacting with varlink services from - the shell. In particular add a "--exec=" switch, which allows specifying a - binary to invoke to which to talk via stdin/stdout - -* make tools that speak varlink over stdin/stdout trivially sockect - activatable. i.e. once bootctl, kernel-install, systemd-measure and similar - speak varlink make them available via a .socket unit with Accept=yes, so that - they can be talked to via IPC out-of-process - -* beef up .service units that are socket activated with Accept=yes with options - AllowPeerUser= + AllowPeerGroup= to allow trivially simple access control - when invoked via socket as IPC services +* Add ACL-based access management to .socket units. i.e. add AllowPeerUser= + + AllowPeerGroup= that installs additional user/group ACL entries on AF_UNIX + sockets. * systemd-tpm2-setup should probably have a factory reset logic, i.e. when some kernel command line option is set we reset the TPM (equivalent of tpm2_clear @@ -929,8 +930,6 @@ Features: 2. systemd-gpt-auto-generator should auto-mount /var if it finds it on disk 3. mount.x-initrd mount option in fstab should be implied for /var -* implement varlink introspection - * make persistent restarts easier by adding a new setting OpenPersistentFile= or so, which allows opening one or more files that is "persistent" across service restarts, hot reboot, cold reboots (depending on configuration): the