Got it, @jlaakkonen — backtrace below, and I think it pins the root cause. gdb attached to the running connmand with connman-debuginfo installed, so the connman frames have full symbols.
Short version: NULL-pointer dereference in get_gateway_cb(), caused by an address-family mismatch. The VPN’s gateway_config is stored in the ipv4_gateway slot even though the endpoint is IPv6, while ipv6_gateway stays NULL — and the RTNL callback then dereferences the IPv6 one.
The crash
Thread 1 "connmand" received signal SIGSEGV, Segmentation fault.
0x000000000045afd4 in get_gateway_cb (gateway=<optimized out>, index=16,
user_data=0x55ae160) at src/connection.c:171
171 config->vpn_phy_index = index;
config = 0x0
si_addr confirms the offset — vpn_phy_index is at +0x20 in struct gateway_config:
si_signo = 11, si_code = 1 (SEGV_MAPERR)
_sigfault = { si_addr = 0x20 }
and at the instruction level, a store through a NULL base register:
=> 0x45afd4 <get_gateway_cb+208>: str w24, [x1, #32]
x1 = 0x0 (config)
x24 = 0x10 (index = 16)
Backtrace
#0 get_gateway_cb (gateway=<optimized out>, index=16, user_data=0x55ae160) at src/connection.c:171
config = 0x0
data = 0x55a5270
params = 0x55ae160
#1 get_route_cb (answer=0x7fc18c62e8, user_data=0x5579970) at src/inet.c:2901
index = 16
addr = 0x7fc18c6188 "fe80::1"
#2 inet_rtnl_recv (chan=0x55a54f0, rtnl_data=0x55ae960) at src/inet.c:2727
#3 inet_rtnl_event (chan=0x55a54f0, cond=G_IO_IN, user_data=0x55ae960) at src/inet.c:2746
#4 0x…294 in ?? () from /usr/lib64/libglib-2.0.so.0
#5 0x…bdc in ?? () from /usr/lib64/libglib-2.0.so.0
#6 g_main_loop_run () from /usr/lib64/libglib-2.0.so.0
#7 main (argc=<optimized out>, argv=<optimized out>) at src/main.c:461
Frame #1 is data->callback(addr, index, data->user_data) with
$13 = { callback = 0x45af04 <get_gateway_cb>, user_data = 0x55ae160 }
The interesting part — the state at the crash
p *data -> (struct gateway_data *) 0x55a5270
{
index4 = 44,
index6 = 0,
service = 0x55a4bc0,
ipv4_gateway = 0x558be60, <-- non-NULL
ipv6_gateway = 0x0, <-- NULL ... and this is the one used
default_checked = false
}
and the ipv4_gateway it did populate contains an IPv6 address:
p *data->ipv4_gateway
{
active = false,
gateway = 0x559daf0 "<IPV6-ENDPOINT>",
vpn = true,
vpn_ip = 0x55ac1c0 "<IPV6-ENDPOINT>",
vpn_phy_index = -1,
vpn_phy_ip = 0x0
}
p *params
{
vpn_gateway = 0x55bc610 "<IPV6-ENDPOINT>",
vpn_index = 44
}
The lookup and the family-selection contradict each other
disassemble /s get_gateway_cb on the live process (offsets confirm the fault: ipv4_gateway is at +0x10, ipv6_gateway at +0x18, vpn_phy_index at +0x20 = si_addr):
155 data = find_vpn_gateway(params->vpn_index, params->vpn_gateway);
…
123 if (data->ipv4_gateway && data->index4 == index &&
124 g_str_equal(data->ipv4_gateway->gateway, gateway))
…
162 family = connman_inet_check_ipaddress(params->vpn_gateway);
164 if (family == AF_INET)
165 config = data->ipv4_gateway;
166 else if (family == AF_INET6)
167 config = data->ipv6_gateway;
0x45afc8 <+196>: ldr x1, [x19, #24] <- reads ipv6_gateway (+0x18) = NULL
168 else
169 goto out;
171 config->vpn_phy_index = index;
0x45afd4 <+208>: str w24, [x1, #32] <- NULL + 0x20 => SIGSEGV
So on this connection:
find_vpn_gateway() found the entry by matching data->index4 == params->vpn_index (44) and g_str_equal(data->ipv4_gateway->gateway, params->vpn_gateway) — i.e. it identified it through the IPv4 slot;
- line 162 then classifies that same string with
connman_inet_check_ipaddress(), which returns AF_INET6 (it is an IPv6 literal);
- so line 167 selects
data->ipv6_gateway, which was never populated → NULL → line 171 writes through it.
The lookup and the family-selection therefore disagree about which slot describes this connection. Either the gateway shouldn’t have been stored in ipv4_gateway in the first place, or line 162 shouldn’t re-derive the family independently of the slot the lookup just matched — you’ll know which. Separately, line 171 has no NULL check, so any such disagreement is a crash rather than a logged error.
(For clarity about what’s evidence vs inference: the struct dumps, the disassembly and the offsets are observed; family itself was <optimized out> in both crash runs, so the AF_INET6 path is inferred from connman_inet_check_ipaddress() on an IPv6 literal plus the fact that the faulting instruction loads from +0x18, which is ipv6_gateway.)
Reproduction
Reproduced on demand, twice with gdb attached — identical backtrace both times (same frames, same config = 0x0, same addr = "fe80::1", index = 16). The struct dumps above are from the second run, where I’d added them to the capture:
- WireGuard VPN provider whose endpoint resolves to an IPv6 address and is reachable on the current transport. Mine: an AAAA-only hostname over an IPv6-only mobile carrier, so the tunnel genuinely establishes and passes traffic.
- Settings → VPN → connect.
- UI cycles “waiting for network” → “idle” → “connected” and settles on “connected” —
connmand SEGVs during that churn.
Correction to my earlier post in this thread: I previously wrote that the crash was independent of whether the tunnel could route. That was wrong, and the distinction turns out to matter. With the same provider on an IPv4-only WiFi — where the AAAA-only endpoint is unroutable — it does not crash; connman just loops
connmand: Connect reply: No route to host (net.connman.Error.Failed)
which I watched repeat every ~2 s for ~17 s with no back-off. The crash needs the tunnel to actually come up so the gateway/route lookup happens. Sorry for the noise — I’d conflated two different failure modes.
Two side effects worth knowing
- Crash loop. After the SEGV, systemd restarts
connmand, the new instance hits the same residual state and SEGVs immediately, ending in Start request repeated too quickly → failed. (Worth knowing if you ask anyone for logs: the journal here is Storage=volatile, so the crashed instance’s own lines rotate away within minutes.)
- Leaked interfaces. Each crash leaves the WireGuard netdev behind — after a few cycles I had
wg0–wg3 all sitting DOWN — because connmand dies before cleaning up. The kernel keeps the addresses and routes, which is exactly why the VPN can look like it’s “working” while the connman control plane is dead (frozen WiFi/mobile toggles, net.connman was not provided by any .service files).
Recovery without a reboot, for anyone else hitting this — connman-vpn survives, so it can remove the provider:
dbus-send --system --print-reply --dest=net.connman.vpn / \
net.connman.vpn.Manager.Remove objpath:/net/connman/vpn/connection/<id>
systemctl reset-failed connman && systemctl restart connman
ip link del wg0 # repeat for any leftovers
Environment: Jolla Phone jp2601, HW 1.0.0.13, SoC MT6858, kernel 6.12.38-4k, SFOS 5.2.0.15, connman-1.40+git23-1.40.1.jolla.aarch64.
I’ve got the repro scripted and the trap re-armable in a minute, so if you want anything else captured — a watchpoint on ipv6_gateway, the state at allocation time, or a patched build tested — just say and I’ll run it.