summary refs log tree commit diff
path: root/src/libnm-glib-aux
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2023-08-09 21:55:35 +0200
committerMichael Biebl <biebl@debian.org>2023-08-09 21:55:35 +0200
commit05e4a733f2141995181a551854d5df929f084adf (patch)
tree83bb937740a6667525ba0df046748ecaa829c269 /src/libnm-glib-aux
parent14b0f3a9dc9ea90d60a3b057350fd4d637dc021a (diff)
New upstream version 1.44.0 upstream/1.44.0
Diffstat (limited to 'src/libnm-glib-aux')
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.c12
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.h2
-rw-r--r--src/libnm-glib-aux/nm-dedup-multi.c6
-rw-r--r--src/libnm-glib-aux/nm-glib.h2
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.h8
-rw-r--r--src/libnm-glib-aux/nm-inet-utils.c151
-rw-r--r--src/libnm-glib-aux/nm-inet-utils.h75
-rw-r--r--src/libnm-glib-aux/nm-io-utils.c100
-rw-r--r--src/libnm-glib-aux/nm-io-utils.h10
-rw-r--r--src/libnm-glib-aux/nm-logging-fwd.h18
-rw-r--r--src/libnm-glib-aux/nm-macros-internal.h212
-rw-r--r--src/libnm-glib-aux/nm-prioq.c192
-rw-r--r--src/libnm-glib-aux/nm-prioq.h4
-rw-r--r--src/libnm-glib-aux/nm-random-utils.c2
-rw-r--r--src/libnm-glib-aux/nm-ref-string.h2
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c363
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h56
-rw-r--r--src/libnm-glib-aux/nm-str-buf.h4
-rw-r--r--src/libnm-glib-aux/nm-test-utils.h53
-rw-r--r--src/libnm-glib-aux/nm-uuid.c2
-rw-r--r--src/libnm-glib-aux/tests/test-shared-general.c180
21 files changed, 1150 insertions, 304 deletions
diff --git a/src/libnm-glib-aux/nm-dbus-aux.c b/src/libnm-glib-aux/nm-dbus-aux.c
index 3925da55..5c4dbc49 100644
--- a/src/libnm-glib-aux/nm-dbus-aux.c
+++ b/src/libnm-glib-aux/nm-dbus-aux.c
@@ -454,8 +454,8 @@ _bus_get_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
 /**
  * nm_g_bus_get_blocking:
- * @cancellable: (allow-none): a #GCancellable to abort the operation.
- * @error: (allow-none): the error.
+ * @cancellable: a #GCancellable to abort the operation.
+ * @error: the error.
  *
  * This calls g_bus_get(), but iterates the current (thread-default) GMainContext
  * until the response is ready. As such, it's similar to g_bus_get_sync(),
@@ -605,10 +605,10 @@ dbus_signal_meta_marshal(GClosure     *closure,
  * _nm_dbus_proxy_signal_connect_data:
  * @proxy: a #GDBusProxy
  * @signal_name: the D-Bus signal to connect to
- * @signature: (allow-none): the signal's type signature (must be a tuple)
+ * @signature: (nullable): the signal's type signature (must be a tuple)
  * @c_handler: the signal handler function
- * @data: (allow-none): data to pass to @c_handler
- * @destroy_data: (allow-none): closure destroy notify for @data
+ * @data: (nullable): data to pass to @c_handler
+ * @destroy_data: (nullable): closure destroy notify for @data
  * @connect_flags: connection flags
  *
  * Connects to the D-Bus signal @signal_name on @proxy. @c_handler must be a
@@ -690,7 +690,7 @@ _nm_dbus_typecheck_response(GVariant *response, const GVariantType *reply_type,
  * @proxy: A #GDBusProxy.
  * @res: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to
  *   g_dbus_proxy_call().
- * @reply_type: (allow-none): the expected type of the reply, or %NULL
+ * @reply_type: (nullable): the expected type of the reply, or %NULL
  * @error: Return location for error or %NULL.
  *
  * Finishes an operation started with g_dbus_proxy_call(), as with
diff --git a/src/libnm-glib-aux/nm-dbus-aux.h b/src/libnm-glib-aux/nm-dbus-aux.h
index 5db79c08..5bbb01e9 100644
--- a/src/libnm-glib-aux/nm-dbus-aux.h
+++ b/src/libnm-glib-aux/nm-dbus-aux.h
@@ -290,7 +290,7 @@ gulong _nm_dbus_proxy_signal_connect_data(GDBusProxy         *proxy,
  * @signal_name: the D-Bus signal to connect to
  * @signature: the signal's type signature (must be a tuple)
  * @c_handler: the signal handler function
- * @data: (allow-none): data to pass to @c_handler
+ * @data: (nullable): data to pass to @c_handler
  *
  * Simplified version of _nm_dbus_proxy_signal_connect_data() with fewer arguments.
  *
diff --git a/src/libnm-glib-aux/nm-dedup-multi.c b/src/libnm-glib-aux/nm-dedup-multi.c
index 625c4ef3..cf2dba90 100644
--- a/src/libnm-glib-aux/nm-dedup-multi.c
+++ b/src/libnm-glib-aux/nm-dedup-multi.c
@@ -97,7 +97,7 @@ _entry_lookup_head(const NMDedupMultiIndex   *self,
         if (c_list_is_empty(&idx_type->lst_idx_head))
             head_entry = NULL;
         else {
-            nm_assert(c_list_length(&idx_type->lst_idx_head) == 1);
+            nm_assert(c_list_length_is(&idx_type->lst_idx_head, 1));
             head_entry = c_list_entry(idx_type->lst_idx_head.next, NMDedupMultiHeadEntry, lst_idx);
         }
         nm_assert(head_entry == g_hash_table_lookup(self->idx_entries, &stack_entry));
@@ -420,7 +420,7 @@ nm_dedup_multi_index_add(NMDedupMultiIndex                         *self,
  *
  * Adds and object to the index.
  *
- * Return: %TRUE if anything changed, %FALSE if nothing changed.
+ * Returns: %TRUE if anything changed, %FALSE if nothing changed.
  */
 gboolean
 nm_dedup_multi_index_add_full(NMDedupMultiIndex                         *self,
@@ -951,7 +951,7 @@ nm_dedup_multi_objs_to_ptr_array_head(const NMDedupMultiHeadEntry   *head_entry,
 /**
  * nm_dedup_multi_entry_reorder:
  * @entry: the entry to reorder. It must not be NULL (and tracked in an index).
- * @entry_order: (allow-none): an optional other entry. It MUST be in the same
+ * @entry_order: (nullable): an optional other entry. It MUST be in the same
  *   list as entry. If given, @entry will be ordered after/before @entry_order.
  *   If left at %NULL, @entry will be moved to the front/end of the list.
  * @order_after: if @entry_order is given, %TRUE means to move @entry after
diff --git a/src/libnm-glib-aux/nm-glib.h b/src/libnm-glib-aux/nm-glib.h
index 0436d135..9c90d429 100644
--- a/src/libnm-glib-aux/nm-glib.h
+++ b/src/libnm-glib-aux/nm-glib.h
@@ -500,7 +500,7 @@ _nm_g_variant_new_take_string(char *string)
     return value;
 #elif !GLIB_CHECK_VERSION(2, 38, 0)
     GVariant *value;
-    GBytes *bytes;
+    GBytes   *bytes;
 
     g_return_val_if_fail(string != NULL, NULL);
     g_return_val_if_fail(g_utf8_validate(string, -1, NULL), NULL);
diff --git a/src/libnm-glib-aux/nm-hash-utils.h b/src/libnm-glib-aux/nm-hash-utils.h
index e2707fc2..c1306200 100644
--- a/src/libnm-glib-aux/nm-hash-utils.h
+++ b/src/libnm-glib-aux/nm-hash-utils.h
@@ -185,10 +185,10 @@ nm_hash_update_str(NMHashState *state, const char *str)
 /* Like nm_hash_update_str(), but restricted to arrays only. nm_hash_update_str() only works
  * with a @str argument that cannot be NULL. If you have a string pointer, that is never NULL, use
  * nm_hash_update() instead. */
-#define nm_hash_update_strarr(state, str)                                \
-    (_Generic(&(str), const char(*)[sizeof(str)]                         \
-              : nm_hash_update_str((state), (str)), char(*)[sizeof(str)] \
-              : nm_hash_update_str((state), (str))))
+#define nm_hash_update_strarr(state, str)                               \
+    (_Generic(&(str),                                                   \
+        const char(*)[sizeof(str)]: nm_hash_update_str((state), (str)), \
+        char(*)[sizeof(str)]: nm_hash_update_str((state), (str))))
 #else
 #define nm_hash_update_strarr(state, str) nm_hash_update_str((state), (str))
 #endif
diff --git a/src/libnm-glib-aux/nm-inet-utils.c b/src/libnm-glib-aux/nm-inet-utils.c
index 7f710f35..2ee73ad5 100644
--- a/src/libnm-glib-aux/nm-inet-utils.c
+++ b/src/libnm-glib-aux/nm-inet-utils.c
@@ -6,6 +6,7 @@
 
 #include <netinet/in.h>
 #include <arpa/inet.h>
+#include <dlfcn.h>
 
 /*****************************************************************************/
 
@@ -266,34 +267,118 @@ nm_ip6_addr_same_prefix_cmp(const struct in6_addr *addr_a,
 
 /*****************************************************************************/
 
-static gboolean
-_parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
+static int
+_inet_aton(const char *text, in_addr_t *out_addr)
 {
-    gs_free char  *s_free = NULL;
-    struct in_addr a1;
-    guint8         bin[sizeof(a1)];
-    char          *s;
-    int            i;
+    /* Call inet_aton() via dlopen.
+     *
+     * The inet_aton() API is discouraged, and ABI checkers warn when we call
+     * it.
+     *
+     * We want to use this function, but only for testing/asserting. To avoid
+     * the ABI checker's complain, dlopen() the symbol. This is not used for
+     * production.
+     */
+    static gpointer fcn_sym     = NULL;
+    static gsize    initialized = 0;
+    int (*fcn)(const char *text, struct in_addr *out_addr);
+    int       r;
+    in_addr_t a;
+
+    if (g_once_init_enter(&initialized)) {
+        GModule *module;
+
+        module = g_module_open(NULL, G_MODULE_BIND_LAZY);
+        if (module) {
+            if (!g_module_symbol(module, "inet_aton", &fcn_sym))
+                g_module_close(module);
+            else
+                g_module_make_resident(module);
+        }
+        g_once_init_leave(&initialized, 1);
+    }
 
-    if (inet_aton(text, &a1) != 1) {
-        g_set_error_literal(error,
-                            NM_UTILS_ERROR,
-                            NM_UTILS_ERROR_INVALID_ARGUMENT,
-                            "address invalid according to inet_aton()");
-        return FALSE;
+    if (!fcn_sym)
+        return -ENOSYS;
+
+    fcn = fcn_sym;
+    r   = fcn(text, (gpointer) &a);
+
+    if (r != 1)
+        return -EINVAL;
+
+    NM_SET_OUT(out_addr, a);
+    return 0;
+}
+
+int
+nmtst_inet_aton(const char *text, in_addr_t *out_addr)
+{
+    return _inet_aton(text, out_addr);
+}
+
+static void
+_nm_assert_legacy_addr4(const char *text, in_addr_t addr)
+{
+#if NM_MORE_ASSERTS > 20
+    char      buf1[NM_INET_ADDRSTRLEN];
+    char      buf2[NM_INET_ADDRSTRLEN];
+    int       r;
+    in_addr_t a;
+
+    /* Our legacy parser accepted "text" as "addr".
+     *
+     * However, we want to ensure that whatever we parse is also parsed by old
+     * inet_aton(). So we want to be strictly more strict than inet_aton() in
+     * what we accept.
+     */
+
+    r = _inet_aton(text, &a);
+
+    if (r != 0) {
+        if (r == -ENOSYS)
+            return;
+        g_error("inet_aton(\"%s\") failed with \"%s\", but we expected %s",
+                text,
+                nm_strerror_native(-r),
+                nm_inet4_ntop(addr, buf2));
     }
 
-    /* OK, inet_aton() accepted the format. That's good, because we want
-     * to accept IPv4 addresses in octal format, like 255.255.000.000.
-     * That's what "legacy" means here. inet_pton() doesn't accept those.
+    if (a != addr) {
+        g_error("inet_aton(\"%s\") parsed %s, but we expected %s",
+                text,
+                nm_inet4_ntop(a, buf1),
+                nm_inet4_ntop(addr, buf2));
+    }
+#endif
+}
+
+static gboolean
+_parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
+{
+    gs_free char *s_free = NULL;
+    union {
+        guint8    b[sizeof(in_addr_t)];
+        in_addr_t a;
+    } addr;
+    char *s;
+    int   i;
+
+    /* inet_pton() does strict parsing of IPv4 address. Good.
      *
-     * But inet_aton() also ignores trailing garbage and formats with fewer than
-     * 4 digits. That is just too crazy and we don't do that. Perform additional checks
-     * and reject some forms that inet_aton() accepted.
+     * However, inet_aton() used to accept much more relaxed forms (e.g. octal
+     * and hex numbers, not having 4 components but fewer, ignore any trailing
+     * garbage).
+     *
+     * Some places where we accept input, we want to be slightly more forgiving
+     * than inet_pton() and accept some (not all!) forms of what inet_aton()
+     * would accept. For example, we want to accept 255.000.000.000.
+     *
+     * We reimplement that below.
      *
      * Note that we still should (of course) accept everything that inet_pton()
-     * accepts. However this code never gets called if inet_pton() succeeds
-     * (see below, aside the assertion code). */
+     * accepts. This is ensured because the caller only calls this function
+     * after inet_pton() failed. */
 
     if (NM_STRCHAR_ANY(text, ch, (!(ch >= '0' && ch <= '9') && !NM_IN_SET(ch, '.', 'x')))) {
         /* We only accepts '.', digits, and 'x' for "0x". */
@@ -306,7 +391,7 @@ _parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
 
     s = nm_memdup_maybe_a(300, text, strlen(text) + 1, &s_free);
 
-    for (i = 0; i < G_N_ELEMENTS(bin); i++) {
+    for (i = 0; i < G_N_ELEMENTS(addr.b); i++) {
         char  *current_token = s;
         gint32 v;
 
@@ -316,7 +401,7 @@ _parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
             s++;
         }
 
-        if ((i == G_N_ELEMENTS(bin) - 1) != (s == NULL)) {
+        if ((i == G_N_ELEMENTS(addr.b) - 1) != (s == NULL)) {
             /* Exactly for the last digit, we expect to have no more following token.
              * But this isn't the case. Abort. */
             g_set_error(error,
@@ -344,26 +429,12 @@ _parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
             return FALSE;
         }
 
-        bin[i] = v;
+        addr.b[i] = v;
     }
 
-    if (memcmp(bin, &a1, sizeof(bin)) != 0) {
-        /* our parsing did not agree with what inet_aton() gave. Something
-         * is wrong. Abort. */
-        g_set_error(
-            error,
-            NM_UTILS_ERROR,
-            NM_UTILS_ERROR_INVALID_ARGUMENT,
-            "inet_aton() result 0x%08x differs from computed value 0x%02hhx%02hhx%02hhx%02hhx",
-            a1.s_addr,
-            bin[0],
-            bin[1],
-            bin[2],
-            bin[3]);
-        return FALSE;
-    }
+    _nm_assert_legacy_addr4(text, addr.a);
 
-    *out_addr = a1.s_addr;
+    *out_addr = addr.a;
     return TRUE;
 }
 
diff --git a/src/libnm-glib-aux/nm-inet-utils.h b/src/libnm-glib-aux/nm-inet-utils.h
index 8421929e..65ceeb2e 100644
--- a/src/libnm-glib-aux/nm-inet-utils.h
+++ b/src/libnm-glib-aux/nm-inet-utils.h
@@ -3,26 +3,28 @@
 #ifndef __NM_INET_UTILS_H__
 #define __NM_INET_UTILS_H__
 
-typedef struct _NMIPAddr {
-    union {
-        guint8          addr_ptr[sizeof(struct in6_addr)];
-        in_addr_t       addr4;
-        struct in_addr  addr4_struct;
-        struct in6_addr addr6;
-
-        /* NMIPAddr is really a union for IP addresses.
-         * However, as ethernet addresses fit in here nicely, use
-         * it also for an ethernet MAC address. */
-        guint8      ether_addr_octet[6 /*ETH_ALEN*/];
-        NMEtherAddr ether_addr;
-
-        guint8 array[sizeof(struct in6_addr)];
-    };
+#include "libnm-std-aux/unaligned-fundamental.h"
+
+typedef union _NMIPAddr {
+    guint8          addr_ptr[sizeof(struct in6_addr)];
+    in_addr_t       addr4;
+    struct in_addr  addr4_struct;
+    struct in6_addr addr6;
+
+    /* This union field only exists, so that it's guaranteed that NMIPAddr has
+     * a suitable alignment. We use that with nm_ether_addr_zero macro, that
+     * aliases nm_ip_addr_zero. */
+    NMEtherAddr _ether_addr;
 } NMIPAddr;
 
-#define NM_IP_ADDR_INIT \
-    {                   \
-        .array = { 0 }  \
+typedef struct _NMIPAddrTyped {
+    NMIPAddr addr;
+    gint8    addr_family;
+} NMIPAddrTyped;
+
+#define NM_IP_ADDR_INIT   \
+    {                     \
+        .addr_ptr = { 0 } \
     }
 
 #define _NM_IN6ADDR_INIT(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
@@ -90,14 +92,15 @@ nm_ip_addr_set(int addr_family, gpointer dst, gconstpointer src)
 static inline gboolean
 nm_ip_addr_is_null(int addr_family, gconstpointer addr)
 {
-    NMIPAddr a;
+    struct in6_addr a6;
 
-    nm_ip_addr_set(addr_family, &a, addr);
+    nm_assert(addr);
 
     if (NM_IS_IPv4(addr_family))
-        return a.addr4 == 0;
+        return unaligned_read_ne32(addr) == 0;
 
-    return IN6_IS_ADDR_UNSPECIFIED(&a.addr6);
+    memcpy(&a6, addr, sizeof(struct in6_addr));
+    return IN6_IS_ADDR_UNSPECIFIED(&a6);
 }
 
 static inline NMIPAddr
@@ -117,7 +120,7 @@ nm_ip_addr_init(int addr_family, gconstpointer src)
 
         /* ensure all bytes of the union are initialized. If only to make
          * valgrind happy. */
-        memset(&a.array[sizeof(in_addr_t)], 0, sizeof(a) - sizeof(in_addr_t));
+        memset(&a.addr_ptr[sizeof(in_addr_t)], 0, sizeof(a) - sizeof(in_addr_t));
     } else
         memcpy(&a, src, sizeof(struct in6_addr));
 
@@ -143,6 +146,30 @@ nm_ip_addr_from_packed_array(int addr_family, gconstpointer ipaddr_arr, gsize id
 
 /*****************************************************************************/
 
+static inline int
+nm_ip_addr_typed_cmp(const NMIPAddrTyped *a, const NMIPAddrTyped *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, addr_family);
+    NM_CMP_DIRECT_MEMCMP(&a->addr, &b->addr, nm_utils_addr_family_to_size(a->addr_family));
+    return 0;
+}
+
+static inline gboolean
+nm_ip_addr_typed_equal(const NMIPAddrTyped *a, const NMIPAddrTyped *b)
+{
+    return nm_ip_addr_typed_cmp(a, b) == 0;
+}
+
+static inline void
+nm_ip_addr_typed_hash_update(NMHashState *h, const NMIPAddrTyped *addr)
+{
+    nm_hash_update_vals(h, addr->addr_family);
+    nm_hash_update_mem(h, &addr->addr, nm_utils_addr_family_to_size(addr->addr_family));
+}
+
+/*****************************************************************************/
+
 static inline guint32
 nm_ip4_addr_netmask_to_prefix(in_addr_t subnetmask)
 {
@@ -342,6 +369,8 @@ nm_inet6_ntop_dup(const struct in6_addr *addr)
 
 /*****************************************************************************/
 
+int nmtst_inet_aton(const char *text, in_addr_t *out_addr);
+
 gboolean nm_inet_parse_bin_full(int         addr_family,
                                 gboolean    accept_legacy,
                                 const char *text,
diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c
index d1430595..ec016ed8 100644
--- a/src/libnm-glib-aux/nm-io-utils.c
+++ b/src/libnm-glib-aux/nm-io-utils.c
@@ -21,6 +21,86 @@
 
 /*****************************************************************************/
 
+int
+nm_io_fcntl_getfl(int fd)
+{
+    int f;
+
+    nm_assert(fd >= 0);
+
+    f = fcntl(fd, F_GETFL, 0);
+
+    /* The caller really must provide a valid FD. For a valid FD, there is not
+     * reason why this call could fail (or how we could handle the failure).
+     *
+     * Unlike plain fcntl(), nm_io_fcntl_getfl() cannot fail. */
+    nm_assert(f != -1);
+
+    /* We not only assert that the return value is "!= -1", but that it's not
+     * negative. Negative flags would be very odd, and not something we would
+     * expect for a successful call. */
+    nm_assert(f >= 0);
+
+    return f;
+}
+
+int
+nm_io_fcntl_setfl(int fd, int flags)
+{
+    int f;
+    int errsv;
+
+    nm_assert(fd >= 0);
+    nm_assert(flags >= 0);
+
+    f = fcntl(fd, F_SETFL, flags);
+    if (f != 0) {
+        errsv = errno;
+
+        nm_assert(errsv != EBADF);
+
+        return -NM_ERRNO_NATIVE(errsv);
+    }
+
+    return 0;
+}
+
+int
+nm_io_fcntl_setfl_update(int fd, int flags_mask, int flags_value)
+{
+    int flags_current;
+
+    nm_assert(fd >= 0);
+    nm_assert(flags_mask > 0);
+    nm_assert(flags_value >= 0);
+    nm_assert(((~flags_mask) & flags_value) == 0);
+
+    flags_current = nm_io_fcntl_getfl(fd);
+    return nm_io_fcntl_setfl(fd, (flags_current & ~flags_mask) | (flags_mask & flags_value));
+}
+
+void
+nm_io_fcntl_setfl_update_nonblock(int fd)
+{
+    int r;
+
+    nm_assert(fd >= 0);
+
+    r = nm_io_fcntl_setfl_update(fd, O_NONBLOCK, O_NONBLOCK);
+
+    /* nm_io_fcntl_setfl_update() already asserts that it cannot fail with
+     * EBADF.
+     *
+     * In nm_io_fcntl_setfl_update_nonblock() only sts O_NONBLOCK, where we
+     * don't expect any other error. Kernel should never reject setting this
+     * flags, and if it did, we have to find out how to handle that. Currently
+     * we don't handle it and assert against failure. */
+
+    nm_assert(r == 0);
+}
+
+/*****************************************************************************/
+
 _nm_printf(4, 5) static int _get_contents_error(GError    **error,
                                                 int         errsv,
                                                 int        *out_errsv,
@@ -79,7 +159,7 @@ _nm_printf(4, 5) static int _get_contents_error(GError    **error,
  *  the NUL byte. That is, it reads only files up to a length of
  *  @max_length - 1 bytes.
  * @length: optional output argument of the read file size.
- * @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
+ * @out_errsv: (out) (optional): on error, a positive errno. or zero.
  * @error:
  *
  *
@@ -276,7 +356,7 @@ nm_utils_fd_get_contents(int                         fd,
  *   the NUL byte. That is, it reads only files up to a length of
  *   @max_length - 1 bytes.
  * @length: optional output argument of the read file size.
- * @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
+ * @out_errsv: (out) (optional): on error, a positive errno. or zero.
  * @error:
  *
  * A reimplementation of g_file_get_contents() with a few differences:
@@ -444,7 +524,7 @@ nm_utils_file_set_contents(const char            *filename,
 /**
  * nm_utils_file_stat:
  * @filename: the filename to stat.
- * @out_st: (allow-none) (out): if given, this will be passed to stat().
+ * @out_st: (out) (nullable): if given, this will be passed to stat().
  *
  * Just wraps stat() and gives the errno number as function result instead
  * of setting the errno (though, errno is also set). It's only for convenience
@@ -482,13 +562,17 @@ nm_utils_fd_read(int fd, NMStrBuf *out_string)
     g_return_val_if_fail(fd >= 0, -1);
     g_return_val_if_fail(out_string, -1);
 
-    /* If the buffer size is 0, we allocate NM_UTILS_GET_NEXT_REALLOC_SIZE_1000 (1000 bytes)
-     * the first time. Afterwards, the buffer grows exponentially.
+    /* Reserve at least 488+1 bytes of buffer size. That is probably a suitable
+     * compromise between not wasting too much buffer space and not reading too much.
      *
-     * Note that with @buf_available, we always would read as much buffer as we actually
-     * have reserved. */
-    nm_str_buf_maybe_expand(out_string, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
+     * Note that when we start with an empty buffer, the first allocation of
+     * 488+1 bytes will actually allocate 1000 bytes. So if we were to receive
+     * one byte at a time, we don't need a reallocation for the first 1000-(488+1)
+     * bytes. Afterwards grows the buffer exponentially.
+     */
+    nm_str_buf_maybe_expand(out_string, NM_UTILS_GET_NEXT_REALLOC_SIZE_488 + 1, FALSE);
 
+    /* We always use all the available buffer size. */
     buf_available = out_string->allocated - out_string->len;
 
     n_read = read(fd, &((nm_str_buf_get_str_unsafe(out_string))[out_string->len]), buf_available);
diff --git a/src/libnm-glib-aux/nm-io-utils.h b/src/libnm-glib-aux/nm-io-utils.h
index 54018140..0021138f 100644
--- a/src/libnm-glib-aux/nm-io-utils.h
+++ b/src/libnm-glib-aux/nm-io-utils.h
@@ -10,6 +10,16 @@
 
 /*****************************************************************************/
 
+int nm_io_fcntl_getfl(int fd);
+
+int nm_io_fcntl_setfl(int fd, int flags);
+
+int nm_io_fcntl_setfl_update(int fd, int flags_mask, int flags_value);
+
+void nm_io_fcntl_setfl_update_nonblock(int fd);
+
+/*****************************************************************************/
+
 /**
  * NMUtilsFileGetContentsFlags:
  * @NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE: no flag
diff --git a/src/libnm-glib-aux/nm-logging-fwd.h b/src/libnm-glib-aux/nm-logging-fwd.h
index 72e5723c..f7ab9098 100644
--- a/src/libnm-glib-aux/nm-logging-fwd.h
+++ b/src/libnm-glib-aux/nm-logging-fwd.h
@@ -177,8 +177,8 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 #define _LOGW(...) _NMLOG(_LOGL_WARN, __VA_ARGS__)
 #define _LOGE(...) _NMLOG(_LOGL_ERR, __VA_ARGS__)
 
-#define _LOGT_ENABLED(...) _NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
-#define _LOGD_ENABLED(...) _NMLOG_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOGT_ENABLED(...) G_UNLIKELY(_NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
+#define _LOGD_ENABLED(...) G_UNLIKELY(_NMLOG_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__))
 #define _LOGI_ENABLED(...) _NMLOG_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
 #define _LOGW_ENABLED(...) _NMLOG_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
 #define _LOGE_ENABLED(...) _NMLOG_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
@@ -192,7 +192,7 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 /* _LOGT() and _LOGt() both log with level TRACE, but the latter is disabled by default,
  * unless building with --with-more-logging. */
 #if NM_MORE_LOGGING
-#define _LOGt_ENABLED(...)    _NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOGt_ENABLED(...)    G_UNLIKELY(_NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
 #define _LOGt(...)            _NMLOG(_LOGL_TRACE, __VA_ARGS__)
 #define _LOGt_err(errsv, ...) _NMLOG_err(errsv, _LOGL_TRACE, __VA_ARGS__)
 #else
@@ -233,8 +233,8 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 #define _LOG2W(...) _NMLOG2(_LOGL_WARN, __VA_ARGS__)
 #define _LOG2E(...) _NMLOG2(_LOGL_ERR, __VA_ARGS__)
 
-#define _LOG2T_ENABLED(...) _NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
-#define _LOG2D_ENABLED(...) _NMLOG2_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOG2T_ENABLED(...) G_UNLIKELY(_NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
+#define _LOG2D_ENABLED(...) G_UNLIKELY(_NMLOG2_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__))
 #define _LOG2I_ENABLED(...) _NMLOG2_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
 #define _LOG2W_ENABLED(...) _NMLOG2_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
 #define _LOG2E_ENABLED(...) _NMLOG2_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
@@ -246,7 +246,7 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 #define _LOG2E_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_ERR, __VA_ARGS__)
 
 #if NM_MORE_LOGGING
-#define _LOG2t_ENABLED(...)    _NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOG2t_ENABLED(...)    G_UNLIKELY(_NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
 #define _LOG2t(...)            _NMLOG2(_LOGL_TRACE, __VA_ARGS__)
 #define _LOG2t_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_TRACE, __VA_ARGS__)
 #else
@@ -278,8 +278,8 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 #define _LOG3W(...) _NMLOG3(_LOGL_WARN, __VA_ARGS__)
 #define _LOG3E(...) _NMLOG3(_LOGL_ERR, __VA_ARGS__)
 
-#define _LOG3T_ENABLED(...) _NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
-#define _LOG3D_ENABLED(...) _NMLOG3_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOG3T_ENABLED(...) G_UNLIKELY(_NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
+#define _LOG3D_ENABLED(...) G_UNLIKELY(_NMLOG3_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__))
 #define _LOG3I_ENABLED(...) _NMLOG3_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
 #define _LOG3W_ENABLED(...) _NMLOG3_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
 #define _LOG3E_ENABLED(...) _NMLOG3_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
@@ -291,7 +291,7 @@ extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
 #define _LOG3E_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_ERR, __VA_ARGS__)
 
 #if NM_MORE_LOGGING
-#define _LOG3t_ENABLED(...)    _NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOG3t_ENABLED(...)    G_UNLIKELY(_NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__))
 #define _LOG3t(...)            _NMLOG3(_LOGL_TRACE, __VA_ARGS__)
 #define _LOG3t_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_TRACE, __VA_ARGS__)
 #else
diff --git a/src/libnm-glib-aux/nm-macros-internal.h b/src/libnm-glib-aux/nm-macros-internal.h
index 0534ee5d..9972dc44 100644
--- a/src/libnm-glib-aux/nm-macros-internal.h
+++ b/src/libnm-glib-aux/nm-macros-internal.h
@@ -239,7 +239,7 @@ _nm_auto_freev(gpointer ptr)
 
 /**
  * NM_G_ERROR_MSG:
- * @error: (allow-none): the #GError instance
+ * @error: (nullable): the #GError instance
  *
  * All functions must follow the convention that when they
  * return a failure, they must also set the GError to a valid
@@ -259,41 +259,41 @@ NM_G_ERROR_MSG(GError *error)
 
 #if _NM_CC_SUPPORT_GENERIC
 #define _NM_CONSTCAST_FULL_1(type, obj_expr, obj) \
-    (_Generic ((obj_expr), \
-               const void        *: ((const type *) (obj)), \
-                     void        *: ((      type *) (obj)), \
-               const type        *: ((const type *) (obj)), \
-                     type        *: ((      type *) (obj))))
+    (_Generic((obj_expr),                         \
+        const void *: ((const type *) (obj)),     \
+        void *: ((type *) (obj)),                 \
+        const type *: ((const type *) (obj)),     \
+        type *: ((type *) (obj))))
 #define _NM_CONSTCAST_FULL_2(type, obj_expr, obj, alias_type2) \
-    (_Generic ((obj_expr), \
-               const void        *: ((const type *) (obj)), \
-                     void        *: ((      type *) (obj)), \
-               const alias_type2 *: ((const type *) (obj)), \
-                     alias_type2 *: ((      type *) (obj)), \
-               const type        *: ((const type *) (obj)), \
-                     type        *: ((      type *) (obj))))
+    (_Generic((obj_expr),                                      \
+        const void *: ((const type *) (obj)),                  \
+        void *: ((type *) (obj)),                              \
+        const alias_type2 *: ((const type *) (obj)),           \
+        alias_type2 *: ((type *) (obj)),                       \
+        const type *: ((const type *) (obj)),                  \
+        type *: ((type *) (obj))))
 #define _NM_CONSTCAST_FULL_3(type, obj_expr, obj, alias_type2, alias_type3) \
-    (_Generic ((obj_expr), \
-               const void        *: ((const type *) (obj)), \
-                     void        *: ((      type *) (obj)), \
-               const alias_type2 *: ((const type *) (obj)), \
-                     alias_type2 *: ((      type *) (obj)), \
-               const alias_type3 *: ((const type *) (obj)), \
-                     alias_type3 *: ((      type *) (obj)), \
-               const type        *: ((const type *) (obj)), \
-                     type        *: ((      type *) (obj))))
+    (_Generic((obj_expr),                                                   \
+        const void *: ((const type *) (obj)),                               \
+        void *: ((type *) (obj)),                                           \
+        const alias_type2 *: ((const type *) (obj)),                        \
+        alias_type2 *: ((type *) (obj)),                                    \
+        const alias_type3 *: ((const type *) (obj)),                        \
+        alias_type3 *: ((type *) (obj)),                                    \
+        const type *: ((const type *) (obj)),                               \
+        type *: ((type *) (obj))))
 #define _NM_CONSTCAST_FULL_4(type, obj_expr, obj, alias_type2, alias_type3, alias_type4) \
-    (_Generic ((obj_expr), \
-               const void        *: ((const type *) (obj)), \
-                     void        *: ((      type *) (obj)), \
-               const alias_type2 *: ((const type *) (obj)), \
-                     alias_type2 *: ((      type *) (obj)), \
-               const alias_type3 *: ((const type *) (obj)), \
-                     alias_type3 *: ((      type *) (obj)), \
-               const alias_type4 *: ((const type *) (obj)), \
-                     alias_type4 *: ((      type *) (obj)), \
-               const type        *: ((const type *) (obj)), \
-                     type        *: ((      type *) (obj))))
+    (_Generic((obj_expr),                                                                \
+        const void *: ((const type *) (obj)),                                            \
+        void *: ((type *) (obj)),                                                        \
+        const alias_type2 *: ((const type *) (obj)),                                     \
+        alias_type2 *: ((type *) (obj)),                                                 \
+        const alias_type3 *: ((const type *) (obj)),                                     \
+        alias_type3 *: ((type *) (obj)),                                                 \
+        const alias_type4 *: ((const type *) (obj)),                                     \
+        alias_type4 *: ((type *) (obj)),                                                 \
+        const type *: ((const type *) (obj)),                                            \
+        type *: ((type *) (obj))))
 #define _NM_CONSTCAST_FULL_x(type, obj_expr, obj, n, ...) \
     (_NM_CONSTCAST_FULL_##n(type, obj_expr, obj, ##__VA_ARGS__))
 #define _NM_CONSTCAST_FULL_y(type, obj_expr, obj, n, ...) \
@@ -308,18 +308,18 @@ NM_G_ERROR_MSG(GError *error)
 
 #if _NM_CC_SUPPORT_GENERIC
 #define NM_UNCONST_PTR(type, arg) \
-    _Generic((arg), const type * : ((type *) (arg)), type * : ((type *) (arg)))
+    _Generic((arg), const type *: ((type *) (arg)), type *: ((type *) (arg)))
 #else
 #define NM_UNCONST_PTR(type, arg) ((type *) (arg))
 #endif
 
 #if _NM_CC_SUPPORT_GENERIC
-#define NM_UNCONST_PPTR(type, arg) \
-    _Generic ((arg), \
-              const type *     *: ((type **) (arg)), \
-                    type *     *: ((type **) (arg)), \
-              const type *const*: ((type **) (arg)), \
-                    type *const*: ((type **) (arg)))
+#define NM_UNCONST_PPTR(type, arg)              \
+    _Generic((arg),                             \
+        const type **: ((type **) (arg)),       \
+        type **: ((type **) (arg)),             \
+        const type *const *: ((type **) (arg)), \
+        type *const *: ((type **) (arg)))
 #else
 #define NM_UNCONST_PPTR(type, arg) ((type **) (arg))
 #endif
@@ -377,30 +377,29 @@ NM_G_ERROR_MSG(GError *error)
  * These macros do the cast, but they only accept a compatible input
  * type, otherwise they will fail compilation.
  */
-#define NM_CAST_STRV_MC(value) \
-    (_Generic ((value), \
-               const char *     *: (const char *     *) (value), \
-                     char *     *: (const char *     *) (value), \
-                           void *: (const char *     *) (value)))
-#define NM_CAST_STRV_CC(value) \
-    (_Generic ((value), \
-               const char *const*: (const char *const*) (value), \
-               const char *     *: (const char *const*) (value), \
-                     char *const*: (const char *const*) (value), \
-                     char *     *: (const char *const*) (value), \
-                     const void *: (const char *const*) (value), \
-                           void *: (const char *const*) (value)))
+#define NM_CAST_STRV_MC(value)                  \
+    (_Generic((value),                          \
+        const char **: (const char **) (value), \
+        char **: (const char **) (value),       \
+        void *: (const char **) (value)))
+#define NM_CAST_STRV_CC(value)                              \
+    (_Generic((value),                                      \
+        const char *const *: (const char *const *) (value), \
+        const char **: (const char *const *) (value),       \
+        char *const *: (const char *const *) (value),       \
+        char **: (const char *const *) (value),             \
+        const void *: (const char *const *) (value),        \
+        void *: (const char *const *) (value)))
 #else
 #define NM_CAST_STRV_MC(value) ((const char **) (value))
 #define NM_CAST_STRV_CC(value) ((const char *const *) (value))
 #endif
 
 #if _NM_CC_SUPPORT_GENERIC
-#define NM_PROPAGATE_CONST(test_expr, ptr) \
-    (_Generic ((test_expr), \
-               const typeof (*(test_expr)) *: ((const typeof (*(ptr)) *) (ptr)), \
-                                     default: (_Generic ((test_expr), \
-                                                         typeof (*(test_expr)) *: (ptr)))))
+#define NM_PROPAGATE_CONST(test_expr, ptr)                              \
+    (_Generic((test_expr),                                              \
+        const typeof(*(test_expr)) *: ((const typeof(*(ptr)) *) (ptr)), \
+        default: (_Generic((test_expr), typeof(*(test_expr)) *: (ptr)))))
 #else
 #define NM_PROPAGATE_CONST(test_expr, ptr) (ptr)
 #endif
@@ -1023,34 +1022,43 @@ nm_g_variant_equal(GVariant *a, GVariant *b)
 #define NM_FLAGS_SET(flags, val)              \
     ({                                        \
         const typeof(flags) _flags = (flags); \
-        const typeof(flags) _val   = (val);   \
+        typeof(_flags)      _val   = (val);   \
                                               \
-        _flags | _val;                        \
+        (void) (FALSE && (flags) == (val));   \
+                                              \
+        (_flags | _val);                      \
     })
 
 #define NM_FLAGS_UNSET(flags, val)            \
     ({                                        \
         const typeof(flags) _flags = (flags); \
-        const typeof(flags) _val   = (val);   \
+        typeof(_flags)      _val   = (val);   \
+                                              \
+        (void) (FALSE && (flags) == (val));   \
                                               \
-        _flags & (~_val);                     \
+        (_flags & (~_val));                   \
     })
 
-#define NM_FLAGS_ASSIGN(flags, val, assign)            \
-    ({                                                 \
-        const typeof(flags) _flags = (flags);          \
-        const typeof(flags) _val   = (val);            \
-                                                       \
-        (assign) ? _flags | (_val) : _flags & (~_val); \
+#define NM_FLAGS_ASSIGN(flags, val, assign)              \
+    ({                                                   \
+        const typeof(flags) _flags = (flags);            \
+        typeof(_flags)      _val   = (val);              \
+                                                         \
+        (void) (FALSE && (flags) == (val));              \
+                                                         \
+        (assign) ? (_flags | _val) : (_flags & (~_val)); \
     })
 
-#define NM_FLAGS_ASSIGN_MASK(flags, mask, val) \
-    ({                                         \
-        const typeof(flags) _flags = (flags);  \
-        const typeof(flags) _mask  = (mask);   \
-        const typeof(flags) _val   = (val);    \
-                                               \
-        ((_flags & ~_mask) | (_mask & _val));  \
+#define NM_FLAGS_ASSIGN_MASK(flags, mask, val)  \
+    ({                                          \
+        const typeof(flags) _flags = (flags);   \
+        typeof(_flags)      _mask  = (mask);    \
+        typeof(_flags)      _val   = (val);     \
+                                                \
+        (void) (FALSE && (flags) == (val));     \
+        (void) (FALSE && (flags) == (mask));    \
+                                                \
+        ((_flags & (~_mask)) | (_val & _mask)); \
     })
 
 /*****************************************************************************/
@@ -1078,11 +1086,20 @@ nm_g_variant_equal(GVariant *a, GVariant *b)
 /* mirrors g_ascii_isspace() and what we consider spaces in general. */
 #define NM_ASCII_SPACES " \n\t\r\f"
 
-/* Like NM_ASCII_SPACES, but without "\f" (0x0c, Formfeed Page Break).
- * This is what for example systemd calls WHITESPACE and what it uses to tokenize
- * the kernel command line. */
+/* Like NM_ASCII_SPACES, but without "\f" (0x0c, Formfeed Page Break).  This is
+ * what for example systemd calls WHITESPACE and what it uses to tokenize the
+ * kernel command line. */
 #define NM_ASCII_WHITESPACES " \n\t\r"
 
+/* mirrors <ctype.h>'s isspace() with C locale. It's like NM_ASCII_SPACES but
+ * additionally also considers '\v' (vertical tab). */
+#define NM_ASCII_SPACES_CTYPE NM_ASCII_SPACES "\v"
+
+/* mirrors kernel's isspace() from "include/linux/ctype.h", which treats as
+ * space the common ASCII spaces, including '\v' (vertical tab), but also
+ * '\240' (non-breaking space, NBSP in Latin-1). */
+#define NM_ASCII_SPACES_KERNEL NM_ASCII_SPACES_CTYPE "\240"
+
 static inline gboolean
 nm_ascii_is_whitespace(char ch)
 {
@@ -1092,6 +1109,13 @@ nm_ascii_is_whitespace(char ch)
     return NM_IN_SET(ch, ' ', '\n', '\t', '\r');
 }
 
+static inline gboolean
+nm_ascii_is_space_kernel(char ch)
+{
+    /* Checks whether @ch is in NM_ASCII_SPACES_KERNEL. */
+    return NM_IN_SET(ch, ' ', '\n', '\t', '\r', '\f', '\v', '\240');
+}
+
 #define NM_ASCII_NEWLINE "\n\r"
 
 static inline gboolean
@@ -1490,23 +1514,21 @@ _nm_strndup_a_step(char *s, const char *str, gsize len)
  *
  * Instead, this generic macro is supposed to handle all integers correctly. */
 #if _NM_CC_SUPPORT_GENERIC
-#define nm_strdup_int(val)                                                       \
-    _Generic((val), char                                                         \
-             : g_strdup_printf("%d", (int) (val)),                               \
-                                                                                 \
-               signed char                                                       \
-             : g_strdup_printf("%d", (signed) (val)), signed short               \
-             : g_strdup_printf("%d", (signed) (val)), signed                     \
-             : g_strdup_printf("%d", (signed) (val)), signed long                \
-             : g_strdup_printf("%ld", (signed long) (val)), signed long long     \
-             : g_strdup_printf("%lld", (signed long long) (val)),                \
-                                                                                 \
-               unsigned char                                                     \
-             : g_strdup_printf("%u", (unsigned) (val)), unsigned short           \
-             : g_strdup_printf("%u", (unsigned) (val)), unsigned                 \
-             : g_strdup_printf("%u", (unsigned) (val)), unsigned long            \
-             : g_strdup_printf("%lu", (unsigned long) (val)), unsigned long long \
-             : g_strdup_printf("%llu", (unsigned long long) (val)))
+#define nm_strdup_int(val)                                                   \
+    _Generic((val),                                                          \
+        char: g_strdup_printf("%d", (int) (val)),                            \
+                                                                             \
+        signed char: g_strdup_printf("%d", (signed) (val)),                  \
+        signed short: g_strdup_printf("%d", (signed) (val)),                 \
+        signed: g_strdup_printf("%d", (signed) (val)),                       \
+        signed long: g_strdup_printf("%ld", (signed long) (val)),            \
+        signed long long: g_strdup_printf("%lld", (signed long long) (val)), \
+                                                                             \
+        unsigned char: g_strdup_printf("%u", (unsigned) (val)),              \
+        unsigned short: g_strdup_printf("%u", (unsigned) (val)),             \
+        unsigned: g_strdup_printf("%u", (unsigned) (val)),                   \
+        unsigned long: g_strdup_printf("%lu", (unsigned long) (val)),        \
+        unsigned long long: g_strdup_printf("%llu", (unsigned long long) (val)))
 #else
 #define nm_strdup_int(val)                                        \
     ((sizeof(val) == sizeof(guint64) && ((typeof(val)) -1) > 0)   \
diff --git a/src/libnm-glib-aux/nm-prioq.c b/src/libnm-glib-aux/nm-prioq.c
index 3448dcd9..e74b2b99 100644
--- a/src/libnm-glib-aux/nm-prioq.c
+++ b/src/libnm-glib-aux/nm-prioq.c
@@ -16,15 +16,38 @@
 
 #include "nm-prioq.h"
 
-#include <errno.h>
-#include <stdlib.h>
-
 /*****************************************************************************/
 
-struct _NMPrioqItem {
+typedef struct _NMPrioqItem {
     void     *data;
     unsigned *idx;
-};
+} PrioqItem;
+
+/*****************************************************************************/
+
+#define _nm_assert_q(q)                                         \
+    G_STMT_START                                                \
+    {                                                           \
+        const NMPrioq *const _q2 = (q);                         \
+                                                                \
+        nm_assert(_q2);                                         \
+        nm_assert(_q2->_priv.n_items == 0 || _q2->_priv.items); \
+        nm_assert(_q2->_priv.compare_func);                     \
+    }                                                           \
+    G_STMT_END
+
+#define _nm_assert_item(q, item)                                \
+    G_STMT_START                                                \
+    {                                                           \
+        const NMPrioq *const   _q    = (q);                     \
+        const PrioqItem *const _item = (item);                  \
+                                                                \
+        _nm_assert_q(_q);                                       \
+                                                                \
+        nm_assert(_item >= _q->_priv.items);                    \
+        nm_assert(_item < &_q->_priv.items[_q->_priv.n_items]); \
+    }                                                           \
+    G_STMT_END
 
 /*****************************************************************************/
 
@@ -72,6 +95,15 @@ nm_prioq_destroy(NMPrioq *q)
     if (!q || !q->_priv.compare_func)
         return;
 
+    _nm_assert_q(q);
+
+    while (q->_priv.n_items > 0) {
+        PrioqItem *i = &q->_priv.items[--q->_priv.n_items];
+
+        if (i->idx)
+            *i->idx = NM_PRIOQ_IDX_NULL;
+    }
+
     free(q->_priv.items);
     q->_priv.compare_func = NULL;
 }
@@ -81,8 +113,7 @@ nm_prioq_destroy(NMPrioq *q)
 static int
 compare(NMPrioq *q, unsigned a, unsigned b)
 {
-    nm_assert(q);
-    nm_assert(q->_priv.compare_func);
+    _nm_assert_q(q);
     nm_assert(a != b);
     nm_assert(a < q->_priv.n_items);
     nm_assert(b < q->_priv.n_items);
@@ -99,15 +130,13 @@ compare(NMPrioq *q, unsigned a, unsigned b)
 static void
 swap(NMPrioq *q, unsigned j, unsigned k)
 {
-    nm_assert(q);
+    _nm_assert_q(q);
     nm_assert(j < q->_priv.n_items);
     nm_assert(k < q->_priv.n_items);
-
     nm_assert(!q->_priv.items[j].idx || *(q->_priv.items[j].idx) == j);
     nm_assert(!q->_priv.items[k].idx || *(q->_priv.items[k].idx) == k);
 
-    NM_SWAP(&q->_priv.items[j].data, &q->_priv.items[k].data);
-    NM_SWAP(&q->_priv.items[j].idx, &q->_priv.items[k].idx);
+    NM_SWAP(&q->_priv.items[j], &q->_priv.items[k]);
 
     if (q->_priv.items[j].idx)
         *q->_priv.items[j].idx = j;
@@ -119,7 +148,7 @@ swap(NMPrioq *q, unsigned j, unsigned k)
 static unsigned
 shuffle_up(NMPrioq *q, unsigned idx)
 {
-    nm_assert(q);
+    _nm_assert_q(q);
     nm_assert(idx < q->_priv.n_items);
 
     while (idx > 0) {
@@ -140,7 +169,7 @@ shuffle_up(NMPrioq *q, unsigned idx)
 static unsigned
 shuffle_down(NMPrioq *q, unsigned idx)
 {
-    nm_assert(q);
+    _nm_assert_q(q);
 
     for (;;) {
         unsigned j;
@@ -184,19 +213,21 @@ nm_prioq_put(NMPrioq *q, void *data, unsigned *idx)
 {
     unsigned k;
 
-    nm_assert(q);
+    _nm_assert_q(q);
+    nm_assert(q->_priv.n_items < G_MAXUINT);
 
-    if (q->_priv.n_items >= q->_priv.n_allocated) {
+    if (G_UNLIKELY(q->_priv.n_items >= q->_priv.n_allocated)) {
         q->_priv.n_allocated = NM_MAX((q->_priv.n_items + 1u) * 2u, 16u);
-        q->_priv.items       = g_renew(struct _NMPrioqItem, q->_priv.items, q->_priv.n_allocated);
+        q->_priv.items       = g_renew(PrioqItem, q->_priv.items, q->_priv.n_allocated);
     }
 
     k = q->_priv.n_items++;
 
-    q->_priv.items[k] = (struct _NMPrioqItem){
+    q->_priv.items[k] = (PrioqItem){
         .data = data,
         .idx  = idx,
     };
+
     if (idx)
         *idx = k;
 
@@ -204,71 +235,88 @@ nm_prioq_put(NMPrioq *q, void *data, unsigned *idx)
 }
 
 static void
-remove_item(NMPrioq *q, struct _NMPrioqItem *i)
+remove_item(NMPrioq *q, PrioqItem *i)
 {
-    struct _NMPrioqItem *l;
-    unsigned             k;
+    PrioqItem *l;
+    unsigned   k;
 
-    nm_assert(q);
-    nm_assert(i);
-    nm_assert(q->_priv.n_items > 0);
-    nm_assert(i >= q->_priv.items);
-    nm_assert(i < &q->_priv.items[q->_priv.n_items]);
+    _nm_assert_item(q, i);
 
-    l = &q->_priv.items[q->_priv.n_items - 1u];
+    if (i->idx)
+        *i->idx = NM_PRIOQ_IDX_NULL;
+
+    q->_priv.n_items--;
+
+    l = &q->_priv.items[q->_priv.n_items];
 
     if (i == l) {
-        /* Last entry, let's just remove it */
-        q->_priv.n_items--;
+        /* Last entry, nothing to do. */
         return;
     }
 
-    /* Not last entry, let's replace the last entry with
-     * this one, and reshuffle */
+    /* Not last entry, let's replace this entry with the last one, and
+     * reshuffle */
+
     k = i - q->_priv.items;
 
     *i = *l;
+
     if (i->idx)
         *i->idx = k;
-    q->_priv.n_items--;
 
     k = shuffle_down(q, k);
     shuffle_up(q, k);
 }
 
-_nm_pure static struct _NMPrioqItem *
+static PrioqItem *
 find_item(NMPrioq *q, void *data, unsigned *idx)
 {
-    struct _NMPrioqItem *i;
-
-    nm_assert(q);
+    PrioqItem *i;
 
-    if (q->_priv.n_items <= 0)
-        return NULL;
+    _nm_assert_q(q);
 
-    if (idx) {
-        if (*idx == NM_PRIOQ_IDX_NULL || *idx >= q->_priv.n_items)
-            return NULL;
-
-        i = &q->_priv.items[*idx];
-        if (i->data == data)
-            return i;
-    } else {
+    if (G_UNLIKELY(!idx)) {
+        /* We allow using NMPrioq without "idx". In that case, it does a linear
+         * search for the data. */
         for (i = q->_priv.items; i < &q->_priv.items[q->_priv.n_items]; i++) {
             if (i->data == data)
                 return i;
         }
+        return NULL;
     }
 
-    return NULL;
+    /* If the user however provides an "idx" pointer, then we assert that it is
+     * consistent. That is, if data is not in the queue, then we require that
+     * "*idx" is NM_PRIOQ_IDX_NULL, and otherwise we require that we really
+     * find "data" at index "*idx".
+     *
+     * This means, when the user calls nm_prioq_{remove,update,reshuffle}()
+     * with an "idx", then they must make sure that the index is consistent.
+     * Usually this means they are required to initialize the index to
+     * NM_PRIOQ_IDX_NULL while the data is not in the heap.
+     *
+     * This is done to assert more, and requires a stricter usage of the API
+     * (in the hope to find misuses of the index). */
+
+    if (*idx >= q->_priv.n_items) {
+        nm_assert(*idx == NM_PRIOQ_IDX_NULL);
+        return NULL;
+    }
+
+    i = &q->_priv.items[*idx];
+
+    if (i->data != data)
+        return nm_assert_unreachable_val(NULL);
+
+    return i;
 }
 
 gboolean
 nm_prioq_remove(NMPrioq *q, void *data, unsigned *idx)
 {
-    struct _NMPrioqItem *i;
+    PrioqItem *i;
 
-    nm_assert(q);
+    _nm_assert_q(q);
 
     i = find_item(q, data, idx);
     if (!i)
@@ -278,28 +326,60 @@ nm_prioq_remove(NMPrioq *q, void *data, unsigned *idx)
     return TRUE;
 }
 
+static void
+reshuffle_item(NMPrioq *q, PrioqItem *i)
+{
+    unsigned k;
+
+    _nm_assert_item(q, i);
+
+    k = i - q->_priv.items;
+    k = shuffle_down(q, k);
+    shuffle_up(q, k);
+}
+
 gboolean
 nm_prioq_reshuffle(NMPrioq *q, void *data, unsigned *idx)
 {
-    struct _NMPrioqItem *i;
-    unsigned             k;
+    PrioqItem *i;
 
-    nm_assert(q);
+    _nm_assert_q(q);
 
     i = find_item(q, data, idx);
     if (!i)
         return FALSE;
 
-    k = i - q->_priv.items;
-    k = shuffle_down(q, k);
-    shuffle_up(q, k);
+    reshuffle_item(q, i);
     return TRUE;
 }
 
+void
+nm_prioq_update(NMPrioq *q, void *data, unsigned *idx, bool queued /* or else remove */)
+{
+    PrioqItem *i;
+
+    _nm_assert_q(q);
+
+    i = find_item(q, data, idx);
+
+    if (!i) {
+        if (queued)
+            nm_prioq_put(q, data, idx);
+        return;
+    }
+
+    if (!queued) {
+        remove_item(q, i);
+        return;
+    }
+
+    reshuffle_item(q, i);
+}
+
 void *
 nm_prioq_peek_by_index(NMPrioq *q, unsigned idx)
 {
-    nm_assert(q);
+    _nm_assert_q(q);
 
     if (idx >= q->_priv.n_items)
         return NULL;
@@ -312,7 +392,7 @@ nm_prioq_pop(NMPrioq *q)
 {
     void *data;
 
-    nm_assert(q);
+    _nm_assert_q(q);
 
     if (q->_priv.n_items <= 0)
         return NULL;
diff --git a/src/libnm-glib-aux/nm-prioq.h b/src/libnm-glib-aux/nm-prioq.h
index 918c6447..51e5b059 100644
--- a/src/libnm-glib-aux/nm-prioq.h
+++ b/src/libnm-glib-aux/nm-prioq.h
@@ -43,6 +43,8 @@ void     nm_prioq_put(NMPrioq *q, void *data, unsigned *idx);
 gboolean nm_prioq_remove(NMPrioq *q, void *data, unsigned *idx);
 gboolean nm_prioq_reshuffle(NMPrioq *q, void *data, unsigned *idx);
 
+void nm_prioq_update(NMPrioq *q, void *data, unsigned *idx, bool queued /* or else remove */);
+
 void *nm_prioq_peek_by_index(NMPrioq *q, unsigned idx) _nm_pure;
 
 static inline void *
@@ -53,7 +55,7 @@ nm_prioq_peek(NMPrioq *q)
 
 void *nm_prioq_pop(NMPrioq *q);
 
-#define NM_PRIOQ_FOREACH_ITEM(q, p) for (unsigned _i = 0; (p = nm_prioq_peek_by_index(q, _i)); _i++)
+#define nm_prioq_for_each(q, p) for (unsigned _i = 0; (p = nm_prioq_peek_by_index((q), _i)); _i++)
 
 _nm_pure static inline unsigned
 nm_prioq_size(NMPrioq *q)
diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c
index 2050d2f9..bbc5536a 100644
--- a/src/libnm-glib-aux/nm-random-utils.c
+++ b/src/libnm-glib-aux/nm-random-utils.c
@@ -301,7 +301,7 @@ _bad_random_bytes(guint8 *buf, gsize n)
  * nm_random_get_bytes_full:
  * @p: the buffer to fill
  * @n: the number of bytes to write to @p.
- * @out_high_quality: (allow-none) (out): whether the returned
+ * @out_high_quality: (out) (optional): whether the returned
  *   random bytes are of high quality.
  *
  * - will never block
diff --git a/src/libnm-glib-aux/nm-ref-string.h b/src/libnm-glib-aux/nm-ref-string.h
index 1b0cabf2..c7cfe87f 100644
--- a/src/libnm-glib-aux/nm-ref-string.h
+++ b/src/libnm-glib-aux/nm-ref-string.h
@@ -53,7 +53,7 @@ nmtst_ref_string_find(const char *cstr)
 {
     /* WARNING: only use for testing. See nmtst_ref_string_find_len() why. */
     if (!cstr)
-        return FALSE;
+        return NULL;
     return nmtst_ref_string_find_len(cstr, strlen(cstr));
 }
 
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
index 702a63e9..34a3af20 100644
--- a/src/libnm-glib-aux/nm-shared-utils.c
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -20,7 +20,9 @@
 
 #include "c-list/src/c-list.h"
 #include "nm-errno.h"
+#include "nm-time-utils.h"
 #include "nm-str-buf.h"
+#include "nm-time-utils.h"
 
 G_STATIC_ASSERT(sizeof(NMEtherAddr) == 6);
 G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) == 1);
@@ -64,7 +66,7 @@ nm_ether_addr_from_string(NMEtherAddr *addr, const char *str)
  *
  * Checks if only the bottom 64bits of the address are set.
  *
- * Return value: %TRUE or %FALSE
+ * Returns: %TRUE or %FALSE
  */
 gboolean
 _nm_utils_inet6_is_token(const struct in6_addr *in6addr)
@@ -437,7 +439,7 @@ nm_g_bytes_new_from_variant_ay(GVariant *var)
 
 /**
  * nm_g_bytes_equal_mem:
- * @bytes: (allow-none): a #GBytes array to compare. Note that
+ * @bytes: (nullable): a #GBytes array to compare. Note that
  *   %NULL is treated like an #GBytes array of length zero.
  * @mem_data: the data pointer with @mem_len bytes
  * @mem_len: the length of the data pointer
@@ -692,7 +694,7 @@ nm_strdict_to_variant_asv(GHashTable *strdict)
  * nm_strquote:
  * @buf: the output buffer of where to write the quoted @str argument.
  * @buf_len: the size of @buf.
- * @str: (allow-none): the string to quote.
+ * @str: (nullable): the string to quote.
  *
  * Writes @str to @buf with quoting. The resulting buffer
  * is always NUL terminated, unless @buf_len is zero.
@@ -1764,11 +1766,11 @@ nm_utils_escaped_tokens_escape_full(const char                     *str,
  * nm_utils_escaped_tokens_options_split:
  * @str: the src string. This string will be modified in-place.
  *   The output values will point into @str.
- * @out_key: (allow-none): the returned output key. This will always be set to @str
- *   itself. @str will be modified to contain only the unescaped, truncated
- *   key name.
- * @out_val: returns the parsed (and unescaped) value or %NULL, if @str contains
- *   no '=' delimiter.
+ * @out_key: (out) (nullable): the returned output key. This will always be set
+ *   to @str itself. @str will be modified to contain only the unescaped,
+ *   truncated key name.
+ * @out_val: (out) (nullable): returns the parsed (and unescaped) value or
+ *   %NULL, if @str contains no '=' delimiter.
  *
  * Honors backslash escaping to parse @str as "key=value" pairs. Optionally, if no '='
  * is present, @out_val will be returned as %NULL. Backslash can be used to escape
@@ -2224,7 +2226,7 @@ nm_utils_error_is_notfound(GError *error)
  * @object: the target object
  * @property_name: the property name
  * @value: the #GValue to set
- * @error: (allow-none): optional error argument
+ * @error: optional error argument
  *
  * A reimplementation of g_object_set_property(), but instead
  * returning an error instead of logging a warning. All g_object_set*()
@@ -2524,7 +2526,7 @@ _str_buf_append_c_escape_octal(NMStrBuf *strbuf, char ch)
 
 /**
  * nm_utils_buf_utf8safe_unescape:
- * @str: (allow-none): the string to unescape. The string itself is a NUL terminated
+ * @str: (nullable): the string to unescape. The string itself is a NUL terminated
  *   ASCII string, that can have C-style backslash escape sequences (which
  *   are to be unescaped). Non-ASCII characters (e.g. UTF-8) are taken verbatim, so
  *   it doesn't care that this string is UTF-8. However, usually this is a UTF-8 encoded
@@ -2755,13 +2757,16 @@ nm_utils_buf_utf8safe_escape(gconstpointer           buf,
     if (g_utf8_validate(str, buflen, &p) && nul_terminated) {
         /* note that g_utf8_validate() does not allow NUL character inside @str. Good.
          * We can treat @str like a NUL terminated string. */
-        if (!NM_STRCHAR_ANY(str,
-                            ch,
-                            (ch == '\\'
-                             || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
-                                 && nm_ascii_is_ctrl_or_del(ch))
-                             || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
-                                 && nm_ascii_is_non_ascii(ch)))))
+        if (!NM_STRCHAR_ANY(
+                str,
+                ch,
+                (ch == '\\'
+                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
+                     && nm_ascii_is_ctrl_or_del(ch))
+                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
+                     && nm_ascii_is_non_ascii(ch))
+                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE)
+                     && ch == '"'))))
             return str;
     }
 
@@ -2781,7 +2786,9 @@ nm_utils_buf_utf8safe_escape(gconstpointer           buf,
             else if ((NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
                       && nm_ascii_is_ctrl_or_del(ch))
                      || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
-                         && nm_ascii_is_non_ascii(ch)))
+                         && nm_ascii_is_non_ascii(ch))
+                     || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE)
+                         && ch == '"'))
                 _str_buf_append_c_escape_octal(&strbuf, ch);
             else
                 nm_str_buf_append_c(&strbuf, ch);
@@ -3332,9 +3339,9 @@ _utils_hashtable_equal(GHashTable      *hash_a,
 
 /**
  * nm_utils_hashtable_cmp_equal:
- * @a: (allow-none): the hash table or %NULL
- * @b: (allow-none): the other hash table or %NULL
- * @cmp_values: (allow-none): if %NULL, only the keys
+ * @a: (nullable): the hash table or %NULL
+ * @b: (nullable): the other hash table or %NULL
+ * @cmp_values: (nullable): if %NULL, only the keys
  *   will be compared. Otherwise, this function is used to
  *   check whether all keys are equal.
  * @user_data: the argument for @cmp_values.
@@ -3401,8 +3408,8 @@ _hashtable_cmp_func(gconstpointer a, gconstpointer b, gpointer user_data)
 
 /**
  * nm_utils_hashtable_cmp:
- * @a: (allow-none): the hash to compare. May be %NULL.
- * @b: (allow-none): the other hash to compare. May be %NULL.
+ * @a: (nullable): the hash to compare. May be %NULL.
+ * @b: (nullable): the other hash to compare. May be %NULL.
  * @do_fast_precheck: if %TRUE, assume that the hashes are equal
  *   and that it is worth calling nm_utils_hashtable_cmp_equal() first.
  *   That requires, that both hashes have the same equals function
@@ -3410,7 +3417,7 @@ _hashtable_cmp_func(gconstpointer a, gconstpointer b, gpointer user_data)
  * @cmp_keys: the compare function for keys. Usually, the hash/equal function
  *   of both hashes corresponds to this function. If you set @do_fast_precheck
  *   to false, then this is not a requirement.
- * @cmp_values: (allow-none): if %NULL, only the keys are compared.
+ * @cmp_values: (nullable): if %NULL, only the keys are compared.
  *   Otherwise, the values must are also compared with this function.
  *
  * Both hashes must have keys/values of the same domain, so that
@@ -4839,6 +4846,50 @@ nm_g_child_watch_source_new(GPid            pid,
     return source;
 }
 
+gboolean
+nm_g_timeout_reschedule(GSource   **src,
+                        gint64     *p_expiry_msec,
+                        gint64      expiry_msec,
+                        GSourceFunc func,
+                        gpointer    user_data)
+{
+    gint64 now_msec;
+    gint64 timeout_msec;
+
+    /* (Re-)Schedules a timeout at "expiry_msec" (in
+     * nm_utils_get_monotonic_timestamp_msec() scale).
+     *
+     * If a source is already scheduled in "*src" and "*p_expiry_msec" is
+     * identical to "expiry_msec", then we assume the timer is already ticking,
+     * and nothing is rescheduled.
+     *
+     * Otherwise, "*src" gets cancelled (if any), a new timer is scheduled
+     * (assigned to "*src") and the new expiry is written to "*p_expiry_msec".
+     */
+
+    nm_assert(src);
+    nm_assert(p_expiry_msec);
+
+    if (*src) {
+        if (*p_expiry_msec == expiry_msec) {
+            /* already scheduled with same expiry. */
+            return FALSE;
+        }
+        nm_clear_g_source_inst(src);
+    }
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    if (expiry_msec <= now_msec)
+        timeout_msec = 0;
+    else
+        timeout_msec = NM_MIN(expiry_msec - now_msec, (gint64) G_MAXUINT);
+
+    *p_expiry_msec = expiry_msec;
+    *src           = nm_g_timeout_add_source(timeout_msec, func, user_data);
+    return TRUE;
+}
+
 /*****************************************************************************/
 
 #define _CTX_LOG(fmt, ...)                                                                       \
@@ -5301,7 +5352,7 @@ nm_utils_ifname_valid_kernel(const char *name, GError **error)
 
         if (ch == '\0')
             return TRUE;
-        if (NM_IN_SET(ch, '/', ':') || g_ascii_isspace(ch)) {
+        if (NM_IN_SET(ch, '/', ':') || nm_ascii_is_space_kernel(ch)) {
             g_set_error_literal(error,
                                 NM_UTILS_ERROR,
                                 NM_UTILS_ERROR_UNKNOWN,
@@ -6729,7 +6780,7 @@ valid_ldh_char(char c)
  * @s: the hostname to check.
  * @trailing_dot: Accept trailing dot on multi-label names.
  *
- * Return: %TRUE if valid.
+ * Returns: %TRUE if valid.
  */
 gboolean
 nm_hostname_is_valid(const char *s, gboolean trailing_dot)
@@ -6789,3 +6840,263 @@ nm_hostname_is_valid(const char *s, gboolean trailing_dot)
 
     return TRUE;
 }
+
+/*****************************************************************************/
+
+typedef struct {
+    GTask                    *task;
+    GSource                  *source_timeout;
+    GSource                  *source_next_poll;
+    GMainContext             *context;
+    GCancellable             *internal_cancellable;
+    NMUtilsPollProbeStartFcn  probe_start_fcn;
+    NMUtilsPollProbeFinishFcn probe_finish_fcn;
+    gpointer                  probe_user_data;
+    gulong                    cancellable_id;
+    gint64                    last_poll_start_ms;
+    int                       sleep_timeout_ms;
+    int                       ratelimit_timeout_ms;
+    bool                      completed : 1;
+} PollTaskData;
+
+static void
+_poll_task_data_free(gpointer data)
+{
+    PollTaskData *poll_task_data = data;
+
+    nm_assert(G_IS_TASK(poll_task_data->task));
+    nm_assert(!poll_task_data->source_next_poll);
+    nm_assert(!poll_task_data->source_timeout);
+    nm_assert(poll_task_data->cancellable_id == 0);
+
+    g_main_context_unref(poll_task_data->context);
+
+    nm_g_slice_free(poll_task_data);
+}
+
+static void
+_poll_return(PollTaskData *poll_task_data, GError *error_take)
+{
+    nm_clear_g_source_inst(&poll_task_data->source_next_poll);
+    nm_clear_g_source_inst(&poll_task_data->source_timeout);
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(poll_task_data->task),
+                                      &poll_task_data->cancellable_id);
+
+    nm_clear_g_cancellable(&poll_task_data->internal_cancellable);
+
+    if (error_take)
+        g_task_return_error(poll_task_data->task, g_steal_pointer(&error_take));
+    else
+        g_task_return_boolean(poll_task_data->task, TRUE);
+
+    g_object_unref(poll_task_data->task);
+}
+
+static gboolean _poll_start_cb(gpointer user_data);
+
+static void
+_poll_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    PollTaskData                     *poll_task_data = user_data;
+    _nm_unused gs_unref_object GTask *task =
+        poll_task_data->task; /* balance ref from _poll_start_cb() */
+    gs_free_error GError *error = NULL;
+    gint64                now_ms;
+    gint64                wait_ms;
+    gboolean              is_finished;
+
+    is_finished =
+        poll_task_data->probe_finish_fcn(source, result, poll_task_data->probe_user_data, &error);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        /* we already handle this differently. Nothing to do. */
+        return;
+    }
+
+    if (error || is_finished) {
+        _poll_return(poll_task_data, g_steal_pointer(&error));
+        return;
+    }
+
+    now_ms = nm_utils_get_monotonic_timestamp_msec();
+    if (poll_task_data->ratelimit_timeout_ms > 0)
+        wait_ms =
+            (poll_task_data->last_poll_start_ms + poll_task_data->ratelimit_timeout_ms) - now_ms;
+    else
+        wait_ms = 0;
+    if (poll_task_data->sleep_timeout_ms > 0)
+        wait_ms = MAX(wait_ms, poll_task_data->sleep_timeout_ms);
+
+    poll_task_data->source_next_poll =
+        nm_g_source_attach(nm_g_timeout_source_new(MAX(1, wait_ms),
+                                                   G_PRIORITY_DEFAULT,
+                                                   _poll_start_cb,
+                                                   poll_task_data,
+                                                   NULL),
+                           poll_task_data->context);
+}
+
+static gboolean
+_poll_start_cb(gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+
+    nm_clear_g_source_inst(&poll_task_data->source_next_poll);
+
+    poll_task_data->last_poll_start_ms = nm_utils_get_monotonic_timestamp_msec();
+
+    g_object_ref(poll_task_data->task); /* balanced by _poll_done_cb() */
+
+    poll_task_data->probe_start_fcn(poll_task_data->internal_cancellable,
+                                    poll_task_data->probe_user_data,
+                                    _poll_done_cb,
+                                    poll_task_data);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_poll_timeout_cb(gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+
+    _poll_return(poll_task_data, nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "timeout expired"));
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_poll_cancelled_cb(GObject *object, gpointer user_data)
+{
+    PollTaskData *poll_task_data = user_data;
+    GError       *error          = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(poll_task_data->task),
+                              &poll_task_data->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    _poll_return(poll_task_data, error);
+}
+
+/**
+ * nm_utils_poll:
+ * @poll_timeout_ms: if >= 0, then this is the overall timeout for how long we poll.
+ *   When this timeout expires, the request completes with failure (and error set).
+ * @ratelimit_timeout_ms: if > 0, we ratelimit the starts from one prope_start_fcn
+ *   call to the next. We will wait at least this time between two consecutive polls.
+ * @sleep_timeout_ms: if > 0, then we wait after a probe finished this timeout
+ *   before the next. Together with @ratelimit_timeout_ms this determines how
+ *   frequently we probe. We will wait at least this time between the end of the
+ *   previous poll and the next one.
+ * @probe_register_object_fcn: (allow-none): called by nm_utils_poll()
+ *   synchronously, with the new, internal GTask instance. The purpose of this
+ *   callback is a bit obscure, you may want to pass NULL here. It's used by some
+ *   caller to register a weak pointer on the internal GTask instance to track
+ *   the lifetime of the operation.
+ * @probe_start_fcn: used to start a (asynchronous) probe. A probe must be
+ *   completed by calling the provided callback. While a probe is in progress, we
+ *   will not start another. The function is called the first time on an idle
+ *   handler, afterwards it gets called again on each timeout for polling.
+ * @probe_finish_fcn: will be called from the callback of @probe_start_fcn. If the
+ *   function returns %TRUE (polling done) or an error, polling stops. Otherwise,
+ *   another poll will be started.
+ * @probe_user_data: user_data for the probe functions.
+ * @cancellable: cancellable for polling.
+ * @callback: when polling completes.
+ * @user_data: for @callback.
+ *
+ * This uses the current g_main_context_get_thread_default() for scheduling
+ * actions.
+ */
+void
+nm_utils_poll(int                               poll_timeout_ms,
+              int                               ratelimit_timeout_ms,
+              int                               sleep_timeout_ms,
+              NMUtilsPollProbeRegisterObjectFcn probe_register_object_fcn,
+              NMUtilsPollProbeStartFcn          probe_start_fcn,
+              NMUtilsPollProbeFinishFcn         probe_finish_fcn,
+              gpointer                          probe_user_data,
+              GCancellable                     *cancellable,
+              GAsyncReadyCallback               callback,
+              gpointer                          user_data)
+{
+    PollTaskData *poll_task_data;
+
+    poll_task_data  = g_slice_new(PollTaskData);
+    *poll_task_data = (PollTaskData){
+        .task             = nm_g_task_new(NULL, cancellable, nm_utils_poll, callback, user_data),
+        .probe_start_fcn  = probe_start_fcn,
+        .probe_finish_fcn = probe_finish_fcn,
+        .probe_user_data  = probe_user_data,
+        .completed        = FALSE,
+        .context          = g_main_context_ref_thread_default(),
+        .sleep_timeout_ms = sleep_timeout_ms,
+        .ratelimit_timeout_ms = ratelimit_timeout_ms,
+        .internal_cancellable = g_cancellable_new(),
+    };
+
+    g_task_set_task_data(poll_task_data->task, poll_task_data, _poll_task_data_free);
+
+    if (probe_register_object_fcn)
+        probe_register_object_fcn((GObject *) poll_task_data->task, probe_user_data);
+
+    if (poll_timeout_ms >= 0) {
+        poll_task_data->source_timeout =
+            nm_g_source_attach(nm_g_timeout_source_new(poll_timeout_ms,
+                                                       G_PRIORITY_DEFAULT,
+                                                       _poll_timeout_cb,
+                                                       poll_task_data,
+                                                       NULL),
+                               poll_task_data->context);
+    }
+
+    poll_task_data->source_next_poll = nm_g_source_attach(
+        nm_g_idle_source_new(G_PRIORITY_DEFAULT_IDLE, _poll_start_cb, poll_task_data, NULL),
+        poll_task_data->context);
+
+    if (cancellable) {
+        gulong signal_id;
+
+        signal_id = g_cancellable_connect(cancellable,
+                                          G_CALLBACK(_poll_cancelled_cb),
+                                          poll_task_data,
+                                          NULL);
+        if (signal_id == 0) {
+            /* the request is already cancelled. Return. */
+            return;
+        }
+        poll_task_data->cancellable_id = signal_id;
+    }
+}
+
+/**
+ * nm_utils_poll_finish:
+ * @result: the GAsyncResult from the GAsyncReadyCallback callback.
+ * @probe_user_data: the user data provided to nm_utils_poll().
+ * @error: the failure code.
+ *
+ * Returns: %TRUE if the polling completed with success. In that case,
+ *   the error won't be set.
+ *   If the request was cancelled, this is indicated by @error and
+ *   %FALSE will be returned.
+ *   If the probe returned a failure, this returns %FALSE and the error
+ *   provided by @probe_finish_fcn.
+ *   If the request times out, this returns %FALSE with error set.
+ *   Error is always set if (and only if) the function returns %FALSE.
+ */
+gboolean
+nm_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error)
+{
+    GTask        *task;
+    PollTaskData *poll_task_data;
+
+    g_return_val_if_fail(nm_g_task_is_valid(result, NULL, nm_utils_poll), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    task = G_TASK(result);
+
+    if (probe_user_data) {
+        poll_task_data = g_task_get_task_data(task);
+        NM_SET_OUT(probe_user_data, poll_task_data->probe_user_data);
+    }
+
+    return g_task_propagate_boolean(task, error);
+}
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index d2e30504..e37cf5e6 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -79,7 +79,7 @@ G_STATIC_ASSERT(sizeof(int) == sizeof(gint32));
     ({                                             \
         _nm_unused typeof(value) _value = (value); \
                                                    \
-        _Generic((value), int : TRUE);             \
+        _Generic((value), int: TRUE);              \
     })
 #else
 #define _NM_INT_LE_MAXINT32(value)                   \
@@ -208,12 +208,13 @@ typedef struct {
 
 #define NM_ETHER_ADDR_INIT(...) ((NMEtherAddr) _NM_ETHER_ADDR_INIT(__VA_ARGS__))
 
-struct _NMIPAddr;
-extern const struct _NMIPAddr nm_ip_addr_zero;
+union _NMIPAddr;
+
+extern const union _NMIPAddr nm_ip_addr_zero;
 
 /* Let's reuse nm_ip_addr_zero also for nm_ether_addr_zero. It's a union that
  * also contains a NMEtherAddr field. */
-#define nm_ether_addr_zero (*((const NMEtherAddr *) ((gconstpointer) &nm_ip_addr_zero)))
+#define nm_ether_addr_zero (*((const NMEtherAddr *) &nm_ip_addr_zero))
 
 static inline int
 nm_ether_addr_cmp(const NMEtherAddr *a, const NMEtherAddr *b)
@@ -1251,12 +1252,16 @@ typedef enum {
      * It will backslash escape ascii characters according to nm_ascii_is_non_ascii(). */
     NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII = 0x0002,
 
+    /* Escape '"' as ASCII "\\042". This is useful when escaping a string so that
+     * it can be unescaped with `echo -e $PASTE_TEXT`. */
+    NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE = 0x0004,
+
     /* This flag only has an effect during escaping to ensure we
      * don't leak secrets in memory. Note that during unescape we
      * know the maximum result size from the beginning, and no
      * reallocation happens. Thus, unescape always avoids leaking
      * secrets already. */
-    NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET = 0x0004,
+    NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET = 0x0008,
 
     /* This flag only has an effect during unescaping. It means
      * that non-escaped whitespaces (g_ascii_isspace()) will be
@@ -1264,7 +1269,7 @@ typedef enum {
      * this flag is only useful for gracefully accepting user input
      * with spaces. With this flag, escape and unescape may no longer
      * yield the original input. */
-    NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES = 0x0008,
+    NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES = 0x0010,
 } NMUtilsStrUtf8SafeFlags;
 
 const char *nm_utils_buf_utf8safe_escape(gconstpointer           buf,
@@ -1352,10 +1357,6 @@ nm_g_variant_new_au(const guint32 *data, gsize len)
     return g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, data, len, sizeof(guint32));
 }
 
-struct _NMIPAddr;
-
-extern const struct _NMIPAddr nm_ip_addr_zero;
-
 static inline GVariant *
 nm_g_variant_new_ay_inaddr(int addr_family, gconstpointer addr)
 {
@@ -1554,6 +1555,12 @@ nm_g_timeout_add_source(guint timeout_msec, GSourceFunc func, gpointer user_data
         NULL);
 }
 
+gboolean nm_g_timeout_reschedule(GSource   **src,
+                                 gint64     *p_expiry_msec,
+                                 gint64      expiry_msec,
+                                 GSourceFunc func,
+                                 gpointer    user_data);
+
 static inline GSource *
 nm_g_timeout_add_seconds_source(guint timeout_sec, GSourceFunc func, gpointer user_data)
 {
@@ -2724,7 +2731,7 @@ guint8 *nm_utils_hexstr2bin_alloc(const char *hexstr,
  * can parse addresses of any length. That is, you don't need
  * to know the length before-hand.
  *
- * Return value: @buffer, or %NULL if @asc couldn't be parsed.
+ * Returns: @buffer, or %NULL if @asc couldn't be parsed.
  */
 static inline guint8 *
 _nm_utils_hwaddr_aton(const char *asc, gpointer buffer, gsize buffer_length, gsize *out_length)
@@ -3250,4 +3257,31 @@ nm_path_startswith(const char *path, const char *prefix)
 
 gboolean nm_hostname_is_valid(const char *s, gboolean trailing_dot);
 
+/*****************************************************************************/
+
+typedef void (*NMUtilsPollProbeRegisterObjectFcn)(GObject *object, gpointer user_data);
+
+typedef void (*NMUtilsPollProbeStartFcn)(GCancellable       *cancellable,
+                                         gpointer            probe_user_data,
+                                         GAsyncReadyCallback callback,
+                                         gpointer            user_data);
+
+typedef gboolean (*NMUtilsPollProbeFinishFcn)(GObject      *source,
+                                              GAsyncResult *result,
+                                              gpointer      probe_user_data,
+                                              GError      **error);
+
+void nm_utils_poll(int                               poll_timeout_ms,
+                   int                               ratelimit_timeout_ms,
+                   int                               sleep_timeout_ms,
+                   NMUtilsPollProbeRegisterObjectFcn probe_register_object_fcn,
+                   NMUtilsPollProbeStartFcn          probe_start_fcn,
+                   NMUtilsPollProbeFinishFcn         probe_finish_fcn,
+                   gpointer                          probe_user_data,
+                   GCancellable                     *cancellable,
+                   GAsyncReadyCallback               callback,
+                   gpointer                          user_data);
+
+gboolean nm_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error);
+
 #endif /* __NM_SHARED_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-str-buf.h b/src/libnm-glib-aux/nm-str-buf.h
index 32ab8da8..c62381e5 100644
--- a/src/libnm-glib-aux/nm-str-buf.h
+++ b/src/libnm-glib-aux/nm-str-buf.h
@@ -522,8 +522,8 @@ nm_str_buf_get_char(const NMStrBuf *strbuf, gsize index)
 
 /**
  * nm_str_buf_finalize:
- * @strbuf: an initilized #NMStrBuf
- * @out_len: (out): (allow-none): optional output
+ * @strbuf: an initialized #NMStrBuf
+ * @out_len: (out): (optional): optional output
  *   argument with the length of the returned string.
  *
  * Returns: (transfer full): the string of the buffer
diff --git a/src/libnm-glib-aux/nm-test-utils.h b/src/libnm-glib-aux/nm-test-utils.h
index b65818e0..de6fd0f1 100644
--- a/src/libnm-glib-aux/nm-test-utils.h
+++ b/src/libnm-glib-aux/nm-test-utils.h
@@ -203,6 +203,54 @@
     }                                                             \
     G_STMT_END
 
+#define nmtst_assert_cmpmem(m1, l1, m2, l2)                                                     \
+    G_STMT_START                                                                                \
+    {                                                                                           \
+        const guint8 *const _m1 = (gpointer) (m1);                                              \
+        const guint8 *const _m2 = (gpointer) (m2);                                              \
+        const gsize         _l1 = (l1);                                                         \
+        const gsize         _l2 = (l2);                                                         \
+                                                                                                \
+        /* This is like g_assert_cmpmem(), however on failure it actually
+         * prints the compared buffer contents, which is useful for debugging
+         * the test failure. */                       \
+                                                                                                \
+        g_assert(_l1 == 0 || _m1);                                                              \
+        g_assert(_l2 == 0 || _m2);                                                              \
+                                                                                                \
+        if (_l1 != _l2 || (_l1 > 0 && memcmp(_m1, _m2, _l1) != 0)) {                            \
+            gs_free char *_s1 = NULL;                                                           \
+            gs_free char *_s2 = NULL;                                                           \
+                                                                                                \
+            g_error(                                                                            \
+                "ERROR: %s:%d : buffer [\"%s\" (%s, %zu bytes)] differs from [\"%s\" (%s, %zu " \
+                "bytes)]:\n"                                                                    \
+                "   a=[ \"%s\" ]\n"                                                             \
+                "   b=[ \"%s\" ]\n",                                                            \
+                __FILE__,                                                                       \
+                (int) __LINE__,                                                                 \
+                #m1,                                                                            \
+                #l1,                                                                            \
+                _l1,                                                                            \
+                #m2,                                                                            \
+                #l2,                                                                            \
+                _l2,                                                                            \
+                (_s1 = nm_utils_buf_utf8safe_escape_cp(                                         \
+                     _m1,                                                                       \
+                     _l1,                                                                       \
+                     NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL                                    \
+                         | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE))                    \
+                    ?: "",                                                                      \
+                (_s2 = nm_utils_buf_utf8safe_escape_cp(                                         \
+                     _m2,                                                                       \
+                     _l2,                                                                       \
+                     NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL                                    \
+                         | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE))                    \
+                    ?: "");                                                                     \
+        }                                                                                       \
+    }                                                                                           \
+    G_STMT_END
+
 /*****************************************************************************/
 
 /* Our nm-error error numbers use negative values to signal failure.
@@ -1249,7 +1297,7 @@ nmtst_stable_rand(guint64 seed, gpointer buf, gsize len)
 
 /**
  * nmtst_get_rand_word_length:
- * @rand: (allow-none): #GRand instance or %NULL to use the singleton.
+ * @rand: (nullable): #GRand instance or %NULL to use the singleton.
  *
  * Returns: a random integer >= 0, that most frequently is somewhere between
  * 0 and 16, but (with decreasing) probability, it can be larger. This can
@@ -2839,8 +2887,7 @@ _nmtst_variant_new_vardict(int dummy, ...)
     G_STMT_END
 #else
 #define _nmtst_assert_variant_bytestring_cmp_str(_ptr, _ptr2, _len) \
-    G_STMT_START                                                    \
-    {}                                                              \
+    G_STMT_START {}                                                 \
     G_STMT_END
 #endif
 
diff --git a/src/libnm-glib-aux/nm-uuid.c b/src/libnm-glib-aux/nm-uuid.c
index cdfa5f62..df1b10c0 100644
--- a/src/libnm-glib-aux/nm-uuid.c
+++ b/src/libnm-glib-aux/nm-uuid.c
@@ -411,7 +411,7 @@ nm_uuid_generate_from_string_str(const char   *s,
  * @uuid_type: the UUID type to use. Prefer version 5 unless you have
  *   good reasons.
  * @type_args: the namespace UUID.
- * @strv: (allow-none): the strv list to hash. Can be NULL, in which
+ * @strv: (nullable): the strv list to hash. Can be NULL, in which
  *   case the result is different from an empty array.
  * @len: if negative, @strv is a NULL terminated array. Otherwise,
  *   it is the length of the strv array. In the latter case it may
diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c
index 3eaca547..84d87d48 100644
--- a/src/libnm-glib-aux/tests/test-shared-general.c
+++ b/src/libnm-glib-aux/tests/test-shared-general.c
@@ -2313,6 +2313,130 @@ test_inet_utils(void)
 
 /*****************************************************************************/
 
+static gboolean
+_inet_parse(int addr_family, const char *str, gboolean accept_legacy, gpointer out_addr)
+{
+    int        addr_family2   = -1;
+    int *const p_addr_family2 = nmtst_get_rand_bool() ? &addr_family2 : NULL;
+    NMIPAddr   addr;
+    gboolean   success;
+
+    g_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+
+    success =
+        nm_inet_parse_bin_full((p_addr_family2 && nmtst_get_rand_bool()) ? AF_UNSPEC : addr_family,
+                               accept_legacy,
+                               str,
+                               p_addr_family2,
+                               &addr);
+
+    if (success) {
+        g_assert(!p_addr_family2 || NM_IN_SET(*p_addr_family2, AF_INET, AF_INET6));
+        if (p_addr_family2 && *p_addr_family2 != addr_family) {
+            success = FALSE;
+        } else
+            g_assert(!p_addr_family2 || *p_addr_family2 == addr_family);
+    } else
+        g_assert(addr_family2 == -1);
+
+    if (out_addr && success)
+        nm_ip_addr_set(addr_family, out_addr, &addr);
+
+    return success;
+}
+
+#define _inet_parse_fail(check, accept_legacy)                             \
+    G_STMT_START                                                           \
+    {                                                                      \
+        NMIPAddr _addr;                                                    \
+        gboolean _success;                                                 \
+                                                                           \
+        _success = _inet_parse(nmtst_get_rand_bool() ? AF_INET : AF_INET6, \
+                               "" check "",                                \
+                               (accept_legacy),                            \
+                               nmtst_get_rand_bool() ? &_addr : NULL);     \
+        g_assert(!_success);                                               \
+    }                                                                      \
+    G_STMT_END
+
+#define _inet_parse_good(check, expected, accept_legacy)                        \
+    G_STMT_START                                                                \
+    {                                                                           \
+        int               _accept_legacy = (accept_legacy);                     \
+        const char *const _check         = "" check "";                         \
+        const char *const _expected      = expected ?: _check;                  \
+        NMIPAddr          _addr[2];                                             \
+        gboolean          _success[2];                                          \
+                                                                                \
+        if (_accept_legacy == -1)                                               \
+            _accept_legacy = nmtst_get_rand_bool();                             \
+                                                                                \
+        _success[0] = _inet_parse(AF_INET6, _check, _accept_legacy, &_addr[0]); \
+        _success[1] = _inet_parse(AF_INET, _check, _accept_legacy, &_addr[1]);  \
+                                                                                \
+        g_assert(NM_IN_SET(_success[0], FALSE, TRUE));                          \
+        g_assert(NM_IN_SET(_success[1], FALSE, TRUE));                          \
+        g_assert(_success[0] != _success[1]);                                   \
+                                                                                \
+        if (_success[0])                                                        \
+            nmtst_assert_ip6_address(&_addr[0].addr6, _expected);               \
+        else                                                                    \
+            nmtst_assert_ip4_address(_addr[1].addr4, _expected);                \
+                                                                                \
+        if (_success[1]) {                                                      \
+            in_addr_t _a4;                                                      \
+            int       _r;                                                       \
+                                                                                \
+            _r = nmtst_inet_aton(_check, &_a4);                                 \
+            g_assert_cmpint(_r, ==, 0);                                         \
+            nmtst_assert_ip4_address(_a4, _expected);                           \
+        }                                                                       \
+    }                                                                           \
+    G_STMT_END
+
+static void
+test_inet_parse_ip4_legacy(void)
+{
+    _inet_parse_fail("", -1);
+    _inet_parse_fail(" ", -1);
+    _inet_parse_fail("a", -1);
+    _inet_parse_fail("0", -1);
+    _inet_parse_fail("0.1", -1);
+    _inet_parse_fail("0.4.1", -1);
+    _inet_parse_fail("1.2.3.05", FALSE);
+    _inet_parse_fail("192.000.002.010", FALSE);
+    _inet_parse_fail("1.2.3..5", -1);
+    _inet_parse_fail("1.2.3.0x", -1);
+    _inet_parse_fail("0xC0000234", -1);
+    _inet_parse_fail("192.0.2.2X", -1);
+    _inet_parse_fail("192.0.2.3 Y", -1);
+    _inet_parse_fail("192.0.2.4\nZ", -1);
+    _inet_parse_fail("192.0.2.5\tT", -1);
+    _inet_parse_fail("192.0.2.6 Y", -1);
+    _inet_parse_fail("192.0.2.7\n", -1);
+    _inet_parse_fail("192.0.2.7\t", -1);
+    _inet_parse_fail("192.0.2.7 ", -1);
+    _inet_parse_fail("00x0019.0000001.000000.0x1", -1);
+    _inet_parse_fail("192.0.2.7.", -1);
+    _inet_parse_fail("192.0.2.7.0", -1);
+
+    _inet_parse_good("192.0.2.1", NULL, -1);
+    _inet_parse_good("1.2.3.4", NULL, -1);
+    _inet_parse_good("192.167.3.4", NULL, -1);
+
+    _inet_parse_good("192.000.002.010", "192.0.2.8", TRUE);
+    _inet_parse_good("255.000.000.000", "255.0.0.0", TRUE);
+    _inet_parse_good("1.2.3.05", "1.2.3.5", TRUE);
+    _inet_parse_good("01.2.3.05", "1.2.3.5", TRUE);
+    _inet_parse_good("192.00167.0003.4", "192.119.3.4", TRUE);
+    _inet_parse_good("0x19.00167.0003.4", "25.119.3.4", TRUE);
+    _inet_parse_good("0x19.000000167.0000003.4", "25.119.3.4", TRUE);
+    _inet_parse_good("0x0019.000000167.0000003.04", "25.119.3.4", TRUE);
+    _inet_parse_good("0x0019.0000001.000000.0x1", "25.1.0.1", TRUE);
+}
+
+/*****************************************************************************/
+
 static void
 test_garray(void)
 {
@@ -2348,7 +2472,7 @@ test_garray(void)
 static int
 _prioq_cmp(gconstpointer a, gconstpointer b)
 {
-    NM_CMP_DIRECT(GPOINTER_TO_UINT(a), GPOINTER_TO_UINT(b));
+    NM_CMP_DIRECT(*((const guint32 *) a), *((const guint32 *) b));
     return 0;
 }
 
@@ -2362,11 +2486,12 @@ static void
 test_nm_prioq(void)
 {
     nm_auto_prioq NMPrioq q = NM_PRIOQ_ZERO;
-    gpointer              data[200];
-    gpointer              data_pop[200];
+    guint32               data[200];
+    const guint32        *data_pop[200];
     guint                 data_idx[G_N_ELEMENTS(data)];
     guint                 i;
     guint                 n;
+    guint                 m;
     gpointer              p;
 
     if (nmtst_get_rand_one_case_in(10))
@@ -2379,37 +2504,67 @@ test_nm_prioq(void)
 
     g_assert(nm_prioq_size(&q) == 0);
 
-    if (nmtst_get_rand_one_case_in(10))
+    if (nmtst_get_rand_one_case_in(100))
         return;
 
     for (i = 0; i < G_N_ELEMENTS(data); i++) {
-        data[i]     = GUINT_TO_POINTER((nmtst_get_rand_uint32() % G_N_ELEMENTS(data)) + 1u);
+        data[i]     = nmtst_get_rand_uint32() % G_N_ELEMENTS(data);
         data_idx[i] = NM_PRIOQ_IDX_NULL;
     }
 
-    nm_prioq_put(&q, data[0], NULL);
+    nm_prioq_put(&q, &data[0], NULL);
     g_assert(nm_prioq_size(&q) == 1);
 
     p = nm_prioq_pop(&q);
-    g_assert(p == data[0]);
+    g_assert(p == &data[0]);
     g_assert(nm_prioq_size(&q) == 0);
 
     g_assert(!nm_prioq_pop(&q));
 
     n = nmtst_get_rand_uint32() % G_N_ELEMENTS(data);
     for (i = 0; i < n; i++)
-        nm_prioq_put(&q, data[i], &data_idx[i]);
+        nm_prioq_put(&q, &data[i], &data_idx[i]);
 
-    g_assert_cmpint(nm_prioq_size(&q), ==, n);
+    m = n;
+    for (i = 0; i < n; i++) {
+        if (!nmtst_get_rand_bool())
+            continue;
 
-    if (nmtst_get_rand_one_case_in(10))
+        data[i] = nmtst_get_rand_uint32() % G_N_ELEMENTS(data);
+        switch (nmtst_get_rand_uint32() % 4) {
+        case 0:
+            nm_prioq_reshuffle(&q, &data[i], &data_idx[i]);
+            break;
+        case 1:
+            nm_prioq_remove(&q, &data[i], nmtst_get_rand_bool() ? &data_idx[i] : NULL);
+            m--;
+            break;
+        case 2:
+            nm_prioq_update(&q, &data[i], &data_idx[i], TRUE);
+            break;
+        case 3:
+            nm_prioq_update(&q, &data[i], nmtst_get_rand_bool() ? &data_idx[i] : NULL, FALSE);
+            m--;
+            break;
+        }
+    }
+
+    g_assert_cmpint(nm_prioq_size(&q), ==, m);
+
+    if (nmtst_get_rand_one_case_in(50))
         return;
 
-    for (i = 0; i < n; i++) {
+    for (i = 0; i < m; i++) {
         data_pop[i] = nm_prioq_pop(&q);
         g_assert(data_pop[i]);
-        if (i > 0)
+        g_assert_cmpint(*data_pop[i], >=, 0);
+        g_assert_cmpint(*data_pop[i], <, G_N_ELEMENTS(data));
+        g_assert(data_pop[i] >= &data[0]);
+        g_assert(data_pop[i] < &data[n]);
+        if (i > 0) {
             g_assert(_prioq_cmp(data_pop[i - 1], data_pop[i]) <= 0);
+            g_assert_cmpint(*data_pop[i - 1], <=, *data_pop[i]);
+        }
     }
 
     g_assert(!nm_prioq_pop(&q));
@@ -2464,6 +2619,7 @@ main(int argc, char **argv)
     g_test_add_func("/general/test_path_simplify", test_path_simplify);
     g_test_add_func("/general/test_hostname_is_valid", test_hostname_is_valid);
     g_test_add_func("/general/test_inet_utils", test_inet_utils);
+    g_test_add_func("/general/test_inet_parse_ip4_legacy", test_inet_parse_ip4_legacy);
     g_test_add_func("/general/test_garray", test_garray);
     g_test_add_func("/general/test_nm_prioq", test_nm_prioq);
     g_test_add_func("/general/test_nm_random", test_nm_random);