From b933c0c70c172569fe48db0ade57ad96716aa5bd Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 11:41:29 +0200 Subject: [PATCH 01/11] Use consistent deinit/destroy semantics --- zig/src/abi/runtime.zig | 6 +-- zig/src/core/actor.zig | 2 +- zig/src/net/connection.zig | 7 ++-- zig/src/net/daemon.zig | 10 ++--- zig/src/openvpn/connection.zig | 9 ++--- zig/src/openvpn/internal/control.zig | 1 - zig/src/openvpn/internal/processing.zig | 1 - zig/src/openvpn/internal/session.zig | 5 +-- zig/src/openvpn/internal/session_context.zig | 1 - .../openvpn/internal/session_negotiator.zig | 1 - zig/src/partout.zig | 4 +- zig/src/testing/mock.zig | 31 ++++++++------- zig/src/wireguard/connection.zig | 8 ++-- zig/tests/abi/runtime.zig | 14 +++---- zig/tests/core/actor.zig | 4 +- zig/tests/net/daemon.zig | 38 +++++++++---------- zig/tests/openvpn/connection.zig | 4 +- zig/tests/wireguard/connection.zig | 10 ++--- 18 files changed, 75 insertions(+), 81 deletions(-) diff --git a/zig/src/abi/runtime.zig b/zig/src/abi/runtime.zig index 83d628dc..f8782795 100644 --- a/zig/src/abi/runtime.zig +++ b/zig/src/abi/runtime.zig @@ -159,7 +159,7 @@ pub const DaemonRuntime = struct { }, }, ); - errdefer self.daemon.deinit(); + errdefer self.daemon.destroy(); // Bind the platform to the underlying OS callbacks self.platform.attach(); @@ -169,8 +169,8 @@ pub const DaemonRuntime = struct { return self; } - pub fn deinit(self: *DaemonRuntime, allocator: std.mem.Allocator) void { - self.daemon.deinit(); + pub fn destroy(self: *DaemonRuntime, allocator: std.mem.Allocator) void { + self.daemon.destroy(); self.platform.deinit(); self.registry.deinit(allocator); self.options.deinit(allocator); diff --git a/zig/src/core/actor.zig b/zig/src/core/actor.zig index 17150b50..768576d6 100644 --- a/zig/src/core/actor.zig +++ b/zig/src/core/actor.zig @@ -84,7 +84,7 @@ pub fn ActorWithFinish( return self; } - pub fn deinit(self: *Self) void { + pub fn destroy(self: *Self) void { self.shutdown(); self.cond.deinit(); self.mutex.deinit(); diff --git a/zig/src/net/connection.zig b/zig/src/net/connection.zig index e5ee441e..afaa6b17 100644 --- a/zig/src/net/connection.zig +++ b/zig/src/net/connection.zig @@ -169,10 +169,11 @@ pub const Connection = struct { stop: *const fn (*anyopaque, u32, Events) void, network_change: *const fn (*anyopaque, io.ReachabilityInfo, Events) void, better_path: *const fn (*anyopaque, Events) void, - deinit: *const fn (*anyopaque) void, /// Called synchronously when the shared daemon-owned looper /// terminates, before runtime recovery or connection destruction. looper_terminated: ?*const fn (*anyopaque, ?looper.Looper.Failure) void = null, + /// Destroys this object. This is the very last step of the lifecycle. + destroy: *const fn (*anyopaque) void, }; pub fn start(self: Connection, events: Events) StartError!bool { @@ -208,7 +209,7 @@ pub const Connection = struct { block(self.ptr, failure); } - pub fn deinit(self: Connection) void { - self.vtable.deinit(self.ptr); + pub fn destroy(self: Connection) void { + self.vtable.destroy(self.ptr); } }; diff --git a/zig/src/net/daemon.zig b/zig/src/net/daemon.zig index 7ce17dd5..18809398 100644 --- a/zig/src/net/daemon.zig +++ b/zig/src/net/daemon.zig @@ -185,7 +185,7 @@ pub const Daemon = struct { }; errdefer { daemon.is_deinitializing = true; - actor.deinit(); + actor.destroy(); } if (activeConnectionModule(&profile) != null) { @@ -194,7 +194,7 @@ pub const Daemon = struct { return daemon; } - pub fn deinit(self: *Daemon) void { + pub fn destroy(self: *Daemon) void { // This is crucial to guarantee that there will not be in-flight // handlers (reachability, better path, connection events) still calling // into the daemon. @@ -209,7 +209,7 @@ pub const Daemon = struct { // Suppress the actor's unexpected-termination path: stop() already // dismantled the connection runtime. self.is_deinitializing = true; - self.actor.deinit(); + self.actor.destroy(); self.monitor.setEventHandler(null); self.monitor.stopObserving(); @@ -797,7 +797,7 @@ pub const Daemon = struct { module, sb, ); - errdefer connection.deinit(); + errdefer connection.destroy(); // Publish a complete runtime before the looper can invoke its // terminal callback. @@ -844,7 +844,7 @@ pub const Daemon = struct { error.TerminalFailure => {}, else => log.writef(.debug, "Unable to stop connection looper: {s}", .{@errorName(err)}), }; - runtime.connection.deinit(); + runtime.connection.destroy(); runtime.looper.deinit(); self.allocator.destroy(runtime.looper); self.connection_runtime = null; diff --git a/zig/src/openvpn/connection.zig b/zig/src/openvpn/connection.zig index 004f6ab6..cc096194 100644 --- a/zig/src/openvpn/connection.zig +++ b/zig/src/openvpn/connection.zig @@ -151,7 +151,7 @@ const OpenVPNConnection = struct { return created.asConnection(); } - fn deinit(self: *OpenVPNConnection) void { + fn destroy(self: *OpenVPNConnection) void { log.write(.debug, "Deinit _OpenVPNConnectionV3"); if (self.current_session) |session| { session.setDelegate(null); @@ -168,7 +168,6 @@ const OpenVPNConnection = struct { if (self.credentials) |*credentials| credentials.deinit(self.allocator); self.allocator.free(self.cache_dir); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } @@ -957,7 +956,7 @@ const openvpn_connection_vtable = net.Connection.VTable{ .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, .looper_terminated = looperDidTerminate, }; @@ -1000,9 +999,9 @@ fn looperDidTerminate( self.looperDidTerminate(failure); } -fn deinit(ptr: *anyopaque) void { +fn destroy(ptr: *anyopaque) void { const self: *OpenVPNConnection = @ptrCast(@alignCast(ptr)); - self.deinit(); + self.destroy(); } pub const testing = struct { diff --git a/zig/src/openvpn/internal/control.zig b/zig/src/openvpn/internal/control.zig index 2de04bc1..3bd7c1d6 100644 --- a/zig/src/openvpn/internal/control.zig +++ b/zig/src/openvpn/internal/control.zig @@ -58,7 +58,6 @@ pub fn ControlChannel(comptime Serializer: type) type { self.sent_dates_ms.deinit(); self.serializer.deinit(self.allocator); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } diff --git a/zig/src/openvpn/internal/processing.zig b/zig/src/openvpn/internal/processing.zig index 9639d49d..abfe8ecf 100644 --- a/zig/src/openvpn/internal/processing.zig +++ b/zig/src/openvpn/internal/processing.zig @@ -188,7 +188,6 @@ pub const LinkProcessor = struct { self.tcp_read_buffer.deinit(self.allocator); self.processor.deinit(); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } diff --git a/zig/src/openvpn/internal/session.zig b/zig/src/openvpn/internal/session.zig index 574a2c22..fb161a30 100644 --- a/zig/src/openvpn/internal/session.zig +++ b/zig/src/openvpn/internal/session.zig @@ -213,7 +213,7 @@ pub const Session = struct { errdefer self.lifecycle_lock.deinit(); self.shutdown_actor = try ShutdownActor.create(allocator, self); errdefer { - self.shutdown_actor.?.deinit(); + self.shutdown_actor.?.destroy(); self.shutdown_actor = null; } return self; @@ -239,7 +239,7 @@ pub const Session = struct { if (self.shutdown_actor) |actor| { self.shutdown_actor = null; - actor.deinit(); + actor.destroy(); } switch (self.state) { .stopped => {}, @@ -254,7 +254,6 @@ pub const Session = struct { self.allocator.free(self.ca_filename); self.lifecycle_lock.deinit(); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } diff --git a/zig/src/openvpn/internal/session_context.zig b/zig/src/openvpn/internal/session_context.zig index c3240328..ac88cbaf 100644 --- a/zig/src/openvpn/internal/session_context.zig +++ b/zig/src/openvpn/internal/session_context.zig @@ -88,7 +88,6 @@ pub const ActiveContext = struct { self.old_keys.deinit(self.allocator); self.remote_endpoint.deinit(self.allocator); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } diff --git a/zig/src/openvpn/internal/session_negotiator.zig b/zig/src/openvpn/internal/session_negotiator.zig index aecff5f1..3d9a0189 100644 --- a/zig/src/openvpn/internal/session_negotiator.zig +++ b/zig/src/openvpn/internal/session_negotiator.zig @@ -167,7 +167,6 @@ pub const Negotiator = struct { if (self.tls) |tls| tls.destroy(); self.pending_packets.deinit(); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } diff --git a/zig/src/partout.zig b/zig/src/partout.zig index c0b73d2f..739f1e4f 100644 --- a/zig/src/partout.zig +++ b/zig/src/partout.zig @@ -99,7 +99,7 @@ pub export fn partout_daemon_start( options.deinit(allocator); return mapErrorToCode(err); }; - errdefer runtime.deinit(allocator); + errdefer runtime.destroy(allocator); runtime.start() catch |err| return mapErrorToCode(err); daemon_runtime = runtime; @@ -114,7 +114,7 @@ pub export fn partout_daemon_hold() callconv(.c) void { pub export fn partout_daemon_stop() callconv(.c) void { const runtime = daemon_runtime orelse return; runtime.stop(); - runtime.deinit(allocator); + runtime.destroy(allocator); daemon_runtime = null; } diff --git a/zig/src/testing/mock.zig b/zig/src/testing/mock.zig index 00a8d329..037ce334 100644 --- a/zig/src/testing/mock.zig +++ b/zig/src/testing/mock.zig @@ -36,10 +36,9 @@ pub const MockSerializedExecutor = struct { return self; } - pub fn deinit(self: *MockSerializedExecutor) void { - self.actor.deinit(); + pub fn destroy(self: *MockSerializedExecutor) void { + self.actor.destroy(); const allocator = self.allocator; - self.* = undefined; allocator.destroy(self); } @@ -85,7 +84,7 @@ pub const MockConnectionEnvironment = struct { allocator: std.mem.Allocator, ) !void { const executor = try MockSerializedExecutor.create(allocator); - errdefer executor.deinit(); + errdefer executor.destroy(); self.* = .{ .looper = try net.Looper.init(allocator, .{ .on_finish = .{ .callback = onLooperFinish }, @@ -96,7 +95,7 @@ pub const MockConnectionEnvironment = struct { pub fn deinit(self: *MockConnectionEnvironment) void { self.looper.deinit(); - self.executor.deinit(); + self.executor.destroy(); } pub fn serializedExecutor( @@ -127,8 +126,8 @@ pub const MockRuntime = struct { runtime: *MockDaemonRuntime, fn finishTeardown(self: *const Instance, allocator: std.mem.Allocator) void { - self.daemon.deinit(); - self.runtime.deinit(allocator); + self.daemon.destroy(); + self.runtime.destroy(allocator); if (self.bindings) |*bindings| { if (bindings.release) |release| { release(bindings); @@ -145,7 +144,7 @@ pub const MockRuntime = struct { const profile_json = util.cString(c_profile); const runtime = try MockDaemonRuntime.create(allocator, args); - errdefer runtime.deinit(allocator); + errdefer runtime.destroy(allocator); var profile = api.Profile.parse(allocator, profile_json) catch |profile_err| { return switch (profile_err) { @@ -181,7 +180,7 @@ pub const MockRuntime = struct { error.OutOfMemory => error.OutOfMemory, }; }; - errdefer new_daemon.deinit(); + errdefer new_daemon.destroy(); new_daemon.start() catch { return error.InvalidProfile; @@ -258,7 +257,7 @@ pub const MockDaemonRuntime = struct { return self; } - pub fn deinit(self: *const MockDaemonRuntime, allocator: std.mem.Allocator) void { + pub fn destroy(self: *MockDaemonRuntime, allocator: std.mem.Allocator) void { self.registry.deinit(allocator); allocator.destroy(self); } @@ -571,7 +570,7 @@ const mock_connection_vtable = net_conn.Connection.VTable{ .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, }; fn mockTunnelConnectionCreate( @@ -606,7 +605,7 @@ fn networkChange(_: *anyopaque, _: net_io.ReachabilityInfo, _: net_conn.Connecti fn betterPath(_: *anyopaque, _: net_conn.Connection.Events) void {} -fn deinit(ptr: *anyopaque) void { +fn destroy(ptr: *anyopaque) void { const self: *MockConnection = @ptrCast(@alignCast(ptr)); self.tunnel_info.deinit(self.allocator); self.allocator.destroy(self); @@ -821,7 +820,7 @@ const daemon_mock_connection_vtable = net_conn.Connection.VTable{ .stop = daemonMockStop, .network_change = daemonMockNetworkChange, .better_path = daemonMockBetterPath, - .deinit = daemonMockDeinit, + .destroy = daemonMockDestroy, }; fn daemonMockStart(_: *anyopaque, events: net_conn.Connection.Events) net_conn.StartError!bool { @@ -854,7 +853,7 @@ fn daemonMockNetworkChange( _: net_conn.Connection.Events, ) void {} -fn daemonMockDeinit(ptr: *anyopaque) void { +fn daemonMockDestroy(ptr: *anyopaque) void { const self: *DaemonMockConnection = @ptrCast(@alignCast(ptr)); self.allocator.destroy(self); } @@ -903,7 +902,7 @@ const blocking_connection_vtable = net_conn.Connection.VTable{ .stop = blockingStop, .network_change = blockingNetworkChange, .better_path = blockingBetterPath, - .deinit = blockingDeinit, + .destroy = blockingDestroy, }; fn blockingStart(_: *anyopaque, events: net_conn.Connection.Events) net_conn.StartError!bool { @@ -930,7 +929,7 @@ fn blockingBetterPath(ptr: *anyopaque, _: net_conn.Connection.Events) void { fn blockingNetworkChange(_: *anyopaque, _: net_io.ReachabilityInfo, _: net_conn.Connection.Events) void {} -fn blockingDeinit(_: *anyopaque) void {} +fn blockingDestroy(_: *anyopaque) void {} pub fn blockingConnectionImplementation(blocking_connection: *BlockingStopConnection) net_conn.ConnectionImplementation { return .{ diff --git a/zig/src/wireguard/connection.zig b/zig/src/wireguard/connection.zig index 53a7f5db..6d26bf7b 100644 --- a/zig/src/wireguard/connection.zig +++ b/zig/src/wireguard/connection.zig @@ -107,7 +107,7 @@ const WireGuardConnection = struct { return created.asConnection(); } - fn deinit(self: *WireGuardConnection) void { + fn destroy(self: *WireGuardConnection) void { const allocator = self.allocator; log.write(.debug, "Deinit _WireGuardConnectionV2"); self.stopDataCountTimer(); @@ -450,7 +450,7 @@ const wireguard_connection_vtable = net.Connection.VTable{ .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, }; fn start(ptr: *anyopaque, events: net.Connection.Events) net.ConnectionStartError!bool { @@ -481,9 +481,9 @@ fn betterPath(ptr: *anyopaque, events: net.Connection.Events) void { self.betterPath(events); } -fn deinit(ptr: *anyopaque) void { +fn destroy(ptr: *anyopaque) void { const self: *WireGuardConnection = @ptrCast(@alignCast(ptr)); - self.deinit(); + self.destroy(); } pub const testing = struct { diff --git a/zig/tests/abi/runtime.zig b/zig/tests/abi/runtime.zig index adee19d9..d5165acd 100644 --- a/zig/tests/abi/runtime.zig +++ b/zig/tests/abi/runtime.zig @@ -135,7 +135,7 @@ test "daemon runtime owns options during lifecycle" { try runtime.start(); runtime.stop(); - runtime.deinit(allocator); + runtime.destroy(allocator); } test "starts DNS-only profile through tunnel controller" { @@ -148,7 +148,7 @@ test "starts DNS-only profile through tunnel controller" { mock.dnsOnlyProfileJson(), runtime.context(®istry), ); - defer daemon.deinit(); + defer daemon.destroy(); try daemon.start(); @@ -181,7 +181,7 @@ test "starts profile without active modules without tunnel settings" { empty_profile_json, runtime.context(®istry), ); - defer daemon.deinit(); + defer daemon.destroy(); try daemon.start(); defer daemon.stop(); @@ -201,7 +201,7 @@ test "requires a connection implementation for active connection profiles" { mock.connectionProfileJson(), runtime.context(®istry), ); - defer daemon.deinit(); + defer daemon.destroy(); try std.testing.expectError( error.MissingConnectionImplementation, @@ -215,7 +215,7 @@ test "starts and stops connection profile through injected dependencies" { var registry = try mockConnectionRegistry(allocator); defer registry.deinit(allocator); var daemon = try createDaemonWithJson(allocator, mock.connectionProfileJson(), runtime.context(®istry)); - defer daemon.deinit(); + defer daemon.destroy(); try std.testing.expect(daemon.isConnectionProfile()); @@ -259,7 +259,7 @@ test "stop blocks until connection teardown finishes" { mock.connectionProfileJson(), runtime.context(®istry), ); - defer daemon.deinit(); + defer daemon.destroy(); try daemon.start(); @@ -276,7 +276,7 @@ test "network monitor gates immediate connection evaluation" { var registry = try mockConnectionRegistry(allocator); defer registry.deinit(allocator); var daemon = try createDaemonWithJson(allocator, mock.connectionProfileJson(), runtime.context(®istry)); - defer daemon.deinit(); + defer daemon.destroy(); try daemon.start(); defer daemon.stop(); diff --git a/zig/tests/core/actor.zig b/zig/tests/core/actor.zig index 6934188a..fbfb8896 100644 --- a/zig/tests/core/actor.zig +++ b/zig/tests/core/actor.zig @@ -41,7 +41,7 @@ test "actor serializes sync and async messages" { var state = CounterState{}; const actor = try CounterActor.create(allocator, &state); - defer actor.deinit(); + defer actor.destroy(); try actor.schedule(.{ .add = 2 }); try actor.perform(.{ .add = 4 }); @@ -56,7 +56,7 @@ test "actor propagates errors and rejects messages after shutdown" { var state = CounterState{}; const actor = try CounterActor.create(allocator, &state); - defer actor.deinit(); + defer actor.destroy(); try std.testing.expectError(error.Rejected, actor.perform(.fail)); diff --git a/zig/tests/net/daemon.zig b/zig/tests/net/daemon.zig index 8cb3ad01..3fa5c58a 100644 --- a/zig/tests/net/daemon.zig +++ b/zig/tests/net/daemon.zig @@ -159,7 +159,7 @@ test "connection daemon starts settings-only profile" { &monitor, .{}, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); try std.testing.expect(sut.isSettingsOnly()); @@ -188,7 +188,7 @@ test "connection daemon starts connection and publishes lifecycle status" { &monitor, .{ .stop_delay_ms = 100 }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); try std.testing.expect(sut.isConnectionProfile()); @@ -239,7 +239,7 @@ test "connection daemon hold preserves published environment" { &monitor, .{ .stop_delay_ms = 100 }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); const remove_count_before_hold = events.remove_count; @@ -276,7 +276,7 @@ test "connection daemon does not cancel tunnel when connection fails to start" { &monitor, .{ .starts_immediately = true }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); try std.testing.expectEqual(@as(usize, 1), failing_connection.start_count); @@ -315,7 +315,7 @@ test "connection daemon passes connection options into sandbox" { .cache_dir = "/tmp/openvpn-cache", }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); defer sut.stop(); @@ -357,7 +357,7 @@ test "connection daemon resets data count when connection disconnects" { .reconnection_delay_ms = 60_000, }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); defer sut.stop(); @@ -389,7 +389,7 @@ test "connection daemon honors disabled cancellation for connection requests" { .cancels_unrecoverable = false, }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); defer sut.stop(); @@ -424,7 +424,7 @@ test "connection daemon replaces a terminal looper and reconnects" { .reconnection_delay_ms = 60_000, }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); defer sut.stop(); @@ -473,7 +473,7 @@ test "connection daemon terminates when its actor finishes" { &monitor, .{ .cancels_unrecoverable = false }, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); sut.actor.shutdown(); @@ -509,7 +509,7 @@ test "connection daemon connects when previously unreachable network becomes rea &monitor, .{}, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); defer sut.stop(); @@ -546,7 +546,7 @@ test "connection daemon forwards better path events to current connection" { &monitor, .{}, ); - defer sut.deinit(); + defer sut.destroy(); monitor.onBetterPath(); try std.testing.expectEqual(@as(usize, 0), blocking_connection.better_path_count); @@ -581,7 +581,7 @@ test "connection daemon runs delayed connection work on its actor" { &monitor, .{}, ); - defer sut.deinit(); + defer sut.destroy(); try sut.start(); while (!delayed_connection.did_run.load(.acquire)) { @@ -613,7 +613,7 @@ test "connection daemon drains an overlapping timer callback and drops stale wor &monitor, .{}, ); - defer sut.deinit(); + defer sut.destroy(); defer sut.stop(); defer delayed_connection.allow_timer_enqueue.store(true, .release); @@ -719,7 +719,7 @@ const DelayedConnection = struct { fn betterPath(_: *anyopaque, _: net.Connection.Events) void {} - fn deinit(ptr: *anyopaque) void { + fn destroy(ptr: *anyopaque) void { const self: *DelayedConnection = @ptrCast(@alignCast(ptr)); self.timer.deinit(); } @@ -729,7 +729,7 @@ const DelayedConnection = struct { .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, }; const implementation_vtable = net.ConnectionImplementation.VTable{ @@ -779,14 +779,14 @@ const FailingStartConnection = struct { fn betterPath(_: *anyopaque, _: net.Connection.Events) void {} - fn deinit(_: *anyopaque) void {} + fn destroy(_: *anyopaque) void {} const vtable = net.Connection.VTable{ .start = start, .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, }; const implementation_vtable = net.ConnectionImplementation.VTable{ @@ -895,7 +895,7 @@ const SandboxCapture = struct { } } - fn deinit(ptr: *anyopaque) void { + fn destroy(ptr: *anyopaque) void { const self: *SandboxCapture = @ptrCast(@alignCast(ptr)); self.deinit_count += 1; } @@ -905,7 +905,7 @@ const SandboxCapture = struct { .stop = stop, .network_change = networkChange, .better_path = betterPath, - .deinit = deinit, + .destroy = destroy, .looper_terminated = looperTerminated, }; diff --git a/zig/tests/openvpn/connection.zig b/zig/tests/openvpn/connection.zig index 12d9567c..0dcf8318 100644 --- a/zig/tests/openvpn/connection.zig +++ b/zig/tests/openvpn/connection.zig @@ -54,7 +54,7 @@ test "OpenVPN connection borrows the daemon looper" { }; var controller = mock.MockTunnelController{}; const executor = try mock.MockSerializedExecutor.create(allocator); - defer executor.deinit(); + defer executor.destroy(); const created = try connection.createConnection( &context, allocator, @@ -69,7 +69,7 @@ test "OpenVPN connection borrows the daemon looper" { .serialized_executor = executor.interface(), }, ); - created.deinit(); + created.destroy(); try looper.perform(void, null, Callbacks.barrier); try looper.stop(); diff --git a/zig/tests/wireguard/connection.zig b/zig/tests/wireguard/connection.zig index d7a6c7de..aab3a3cb 100644 --- a/zig/tests/wireguard/connection.zig +++ b/zig/tests/wireguard/connection.zig @@ -202,7 +202,7 @@ test "WireGuard connection erases adapter activation errors at the generic bound .looper = &environment.looper, .serialized_executor = environment.serializedExecutor(), }); - defer created.deinit(); + defer created.destroy(); try std.testing.expectError(error.UnableToStart, created.start(recorder.events())); try std.testing.expectEqual(@as(usize, 1), fake_backend.turn_on_count); @@ -240,7 +240,7 @@ test "WireGuard connection starts and stops through backend and controller" { .serialized_executor = environment.serializedExecutor(), .options = .{ .min_data_count_interval = 2345 }, }); - defer created.deinit(); + defer created.destroy(); try std.testing.expectEqual(@as(u32, 2345), connection.testing.dataCountIntervalMs(created)); try std.testing.expect(try created.start(recorder.events())); @@ -296,7 +296,7 @@ test "WireGuard connection resolves hostname endpoints through sandbox resolver" .serialized_executor = environment.serializedExecutor(), .options = .{ .dns_timeout = 1234 }, }); - defer created.deinit(); + defer created.destroy(); try std.testing.expect(try created.start(recorder.events())); adapter.testing.setNetworkChangeBehavior( @@ -446,7 +446,7 @@ test "WireGuard connection handles network monitor events" { .looper = &environment.looper, .serialized_executor = environment.serializedExecutor(), }); - defer created.deinit(); + defer created.destroy(); try std.testing.expect(try created.start(recorder.events())); created.betterPath(recorder.events()); @@ -523,7 +523,7 @@ test "WireGuard connection retries temporary shutdown resume and re-resolves pee .looper = &environment.looper, .serialized_executor = environment.serializedExecutor(), }); - defer created.deinit(); + defer created.destroy(); connection.testing.setTemporaryShutdownRetryDelayMs(created, 1); try std.testing.expect(try created.start(recorder.events())); From b164cf53342e20218865df97ca8452124765069c Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 12:13:29 +0200 Subject: [PATCH 02/11] Tighten address format check --- zig/src/openvpn/connection.zig | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/zig/src/openvpn/connection.zig b/zig/src/openvpn/connection.zig index cc096194..4e452922 100644 --- a/zig/src/openvpn/connection.zig +++ b/zig/src/openvpn/connection.zig @@ -437,8 +437,11 @@ const OpenVPNConnection = struct { remote_options: *const api.OpenVPNConfiguration, ) void { log.write(.notice, "Session did start"); - const remote_address = api.Address.parseRaw(remote_endpoint.address) orelse unreachable; - log.writef(.info, "\tEndpoint: {s}", .{remote_address}); + const address = api.Address.parseRaw(remote_endpoint.address) orelse { + self.failTunnelSetup(session, .invalidValue); + return; + }; + log.writef(.info, "\tEndpoint: {s}", .{address}); log.writef(.info, "\tProtocol: {s}:{d}", .{ remote_endpoint.proto.socket_type.raw(), remote_endpoint.proto.port, @@ -459,10 +462,6 @@ const OpenVPNConnection = struct { }; defer NetworkSettingsBuilder.deinitModules(self.allocator, modules); - const address = api.Address.parseRaw(remote_endpoint.address) orelse { - self.failTunnelSetup(session, .invalidValue); - return; - }; const info = api.TunnelRemoteInfoWrapper{ .profile = self.profile.*, .original_module_id = self.module_id, From eacf657dcc1077c400fc42582ae17809ac840474 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 14:10:40 +0200 Subject: [PATCH 03/11] Add log variants that panic in debug --- zig/src/core/logging.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/zig/src/core/logging.zig b/zig/src/core/logging.zig index e6241b22..13071751 100644 --- a/zig/src/core/logging.zig +++ b/zig/src/core/logging.zig @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0 const std = @import("std"); +const builtin = @import("builtin"); const api = @import("api.zig"); const concurrency = @import("concurrency.zig"); @@ -137,6 +138,20 @@ pub fn writeCString(level: Level, message: [*:0]const u8) void { dispatchCString(logger, @intFromEnum(level), message); } +pub fn writeAndFailDebug(message: [:0]const u8) void { + write(.err, message); + if (builtin.mode == .Debug) { + @panic(message); + } +} + +pub fn writefAndFailDebug(comptime fmt: []const u8, args: anytype) void { + writef(.err, fmt, args); + if (builtin.mode == .Debug) { + std.debug.panic(fmt, args); + } +} + fn dispatchSlice( logger: Logger, level: c_int, From 96d5b7b88b4b6286a151e83d4889a2018470f03f Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 12:33:20 +0200 Subject: [PATCH 04/11] Replace unreachable with compileError --- zig/src/c/exports.zig | 2 +- zig/src/core/logging.zig | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/zig/src/c/exports.zig b/zig/src/c/exports.zig index 0bcbb4f3..3edb56bf 100644 --- a/zig/src/c/exports.zig +++ b/zig/src/c/exports.zig @@ -35,7 +35,7 @@ pub const CryptoBackend = enum { if (@hasDecl(crypto, "PARTOUT_CRYPTO_OPENSSL")) return .openssl; if (@hasDecl(crypto, "PARTOUT_CRYPTO_MBEDTLS")) return .native; if (builtin.is_test) return .mock; - unreachable; + @compileError("no default crypto backend is available"); } }; diff --git a/zig/src/core/logging.zig b/zig/src/core/logging.zig index 13071751..14f5240b 100644 --- a/zig/src/core/logging.zig +++ b/zig/src/core/logging.zig @@ -358,7 +358,8 @@ fn forLog( value: anytype, ) ![]const u8 { const T = @TypeOf(value); - const format = comptime logFormat(T) orelse unreachable; + const format = comptime logFormat(T) orelse + @compileError("unsupported type passed to forLog()"); return switch (format) { .declared => T.logging_formatter(allocator, value), .model => |adapter| formatWithAdapter(allocator, value, adapter), @@ -369,7 +370,7 @@ fn forLog( .array => switch (@typeInfo(T)) { .array => formatArray(allocator, &value), .pointer => formatArray(allocator, value), - else => unreachable, + else => @compileError("array log format requires an array or pointer type"), }, .dictionary => formatDictionary(allocator, &value), .dereference => forLog(allocator, value.*), From 71fb1cf6c304534fd29adac76f4cfde88c33ff05 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 13:09:57 +0200 Subject: [PATCH 05/11] Ignore minor failures - Wrong queue advance() invariant - Unexpected looper stop failures --- zig/src/net/looper.zig | 14 ++++++++------ zig/src/net/looper_queue.zig | 6 +++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/zig/src/net/looper.zig b/zig/src/net/looper.zig index 8083ece9..fdf97b20 100644 --- a/zig/src/net/looper.zig +++ b/zig/src/net/looper.zig @@ -394,7 +394,7 @@ pub const Looper = struct { while (!completion.done) { self.condition.wait(&self.lock); } - const result = completion.result; + const completion_failure = completion.failure; self.lock.unlock(); self.joinWorker(); @@ -404,11 +404,13 @@ pub const Looper = struct { self.waiter_count -= 1; self.condition.broadcast(); self.lock.unlock(); - if (result) |err| return switch (err) { - error.OutOfMemory => error.OutOfMemory, - error.Cancelled => error.Cancelled, - error.TerminalFailure => error.TerminalFailure, - else => unreachable, + if (completion_failure) |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.Cancelled => return error.Cancelled, + error.TerminalFailure => return error.TerminalFailure, + else => log.writefAndFailDebug("Ignoring unexpected Looper stop completion error: {s}", .{ + @errorName(err), + }), }; } diff --git a/zig/src/net/looper_queue.zig b/zig/src/net/looper_queue.zig index fc4e9e41..491a90f0 100644 --- a/zig/src/net/looper_queue.zig +++ b/zig/src/net/looper_queue.zig @@ -6,6 +6,7 @@ const std = @import("std"); const core = @import("../core/exports.zig"); const io = @import("io.zig"); +const log = core.logging; /// Single binary data packet. pub const Packet = []const u8; @@ -319,7 +320,10 @@ pub const WriteQueue = struct { /// Advances the head packet and returns whether it was fully consumed. pub fn advance(self: *WriteQueue, written: usize) bool { - const first = self.head orelse unreachable; + const first = self.head orelse { + log.writeAndFailDebug("Ignoring advance on an empty WriteQueue"); + return true; + }; const remaining = first.data.len - self.offset; std.debug.assert(written <= remaining); if (written < remaining) { From b65a99672ab886445c964781306acec6ec6a82fb Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 14:25:38 +0200 Subject: [PATCH 06/11] Rename Completion.result to .failure --- zig/src/net/looper.zig | 20 ++++++++++---------- zig/src/net/looper_queue.zig | 8 ++++---- zig/tests/net/looper_queue.zig | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/zig/src/net/looper.zig b/zig/src/net/looper.zig index fdf97b20..cc1d64d1 100644 --- a/zig/src/net/looper.zig +++ b/zig/src/net/looper.zig @@ -499,13 +499,13 @@ pub const Looper = struct { while (!completion.done) { self.condition.wait(&self.lock); } - const command_result = completion.result; + const completion_failure = completion.failure; std.debug.assert(self.waiter_count > 0); self.waiter_count -= 1; self.condition.broadcast(); self.lock.unlock(); - if (command_result) |err| return err; + if (completion_failure) |err| return err; if (holder.failure) |err| return err; return holder.value.?; } @@ -539,12 +539,12 @@ pub const Looper = struct { while (!completion.done) { self.condition.wait(&self.lock); } - const result = completion.result; + const completion_failure = completion.failure; std.debug.assert(self.waiter_count > 0); self.waiter_count -= 1; self.condition.broadcast(); self.lock.unlock(); - if (result) |err| return switch (err) { + if (completion_failure) |err| return switch (err) { error.OutOfMemory => error.OutOfMemory, error.Cancelled => error.Cancelled, error.OperationCancelled => error.OperationCancelled, @@ -576,12 +576,12 @@ pub const Looper = struct { while (!completion.done) { self.condition.wait(&self.lock); } - const result = completion.result; + const completion_failure = completion.failure; std.debug.assert(self.waiter_count > 0); self.waiter_count -= 1; self.condition.broadcast(); self.lock.unlock(); - if (result) |err| return switch (err) { + if (completion_failure) |err| return switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.Cancelled, }; @@ -1323,17 +1323,17 @@ pub const Looper = struct { fn queueCompletionLocked( self: *Looper, completion: *Completion, - result: ?CompletionError, + failure: ?CompletionError, ) void { - self.completions.append(completion, result); + self.completions.append(completion, failure); } fn releaseCompletionsLocked(self: *Looper) void { self.completions.releaseAll(); } - fn completeNow(completion: *Completion, result: ?CompletionError) void { - completion.result = result; + fn completeNow(completion: *Completion, failure: ?CompletionError) void { + completion.failure = failure; completion.done = true; } diff --git a/zig/src/net/looper_queue.zig b/zig/src/net/looper_queue.zig index 491a90f0..797cc9d2 100644 --- a/zig/src/net/looper_queue.zig +++ b/zig/src/net/looper_queue.zig @@ -145,8 +145,8 @@ pub const CompletionError = std.mem.Allocator.Error || pub const Completion = struct { // Completion state. done: bool = false, - // Completion error or null on success. - result: ?CompletionError = null, + // Completion failure or null on success. + failure: ?CompletionError = null, // Intrusive completion queue linkage. next: ?*Completion = null, }; @@ -160,9 +160,9 @@ pub const CompletionQueue = struct { pub fn append( self: *CompletionQueue, completion: *Completion, - result: ?CompletionError, + failure: ?CompletionError, ) void { - completion.result = result; + completion.failure = failure; completion.next = null; if (self.tail) |tail| { tail.next = completion; diff --git a/zig/tests/net/looper_queue.zig b/zig/tests/net/looper_queue.zig index e8b198a4..a9cfb1b5 100644 --- a/zig/tests/net/looper_queue.zig +++ b/zig/tests/net/looper_queue.zig @@ -23,7 +23,7 @@ test "completion queue releases completions in FIFO order" { try std.testing.expect(second.next == null); try std.testing.expect(!first.done); try std.testing.expect(!second.done); - try std.testing.expect(second.result.? == error.Cancelled); + try std.testing.expect(second.failure.? == error.Cancelled); queue.releaseAll(); @@ -36,7 +36,7 @@ test "completion queue releases completions in FIFO order" { queue.append(&third, error.TerminalFailure); queue.releaseAll(); try std.testing.expect(third.done); - try std.testing.expect(third.result.? == error.TerminalFailure); + try std.testing.expect(third.failure.? == error.TerminalFailure); } test "command queue detaches ready commands in FIFO order" { From 732f589d84e8340d28e39fb100bbdcbf93e99195 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 14:45:43 +0200 Subject: [PATCH 07/11] Replace unreachable and unwrapping with errors --- zig/src/c/exports.zig | 12 +++++--- zig/src/core/api_manual.zig | 2 +- zig/src/openvpn/connection.zig | 36 ++++++++++------------ zig/src/openvpn/internal/auth.zig | 27 ++++++++-------- zig/src/openvpn/internal/data.zig | 7 +++-- zig/src/openvpn/internal/serialization.zig | 4 +-- zig/src/openvpn/internal/tls.zig | 3 +- zig/src/openvpn/parser.zig | 3 +- zig/src/wireguard/connection.zig | 32 ++++++++++--------- zig/src/wireguard/internal/resolver.zig | 19 +++++++----- zig/src/wireguard/internal/tunnel_info.zig | 31 ++++++++++++++----- zig/tests/c/exports.zig | 22 ++++++++++--- 12 files changed, 119 insertions(+), 79 deletions(-) diff --git a/zig/src/c/exports.zig b/zig/src/c/exports.zig index 3edb56bf..8c71d459 100644 --- a/zig/src/c/exports.zig +++ b/zig/src/c/exports.zig @@ -39,23 +39,25 @@ pub const CryptoBackend = enum { } }; -pub fn cryptoFunctionTable(backend: CryptoBackend) crypto.pp_crypto_fnt { +pub const CryptoFunctionTableError = error{UnsupportedCryptoBackend}; + +pub fn cryptoFunctionTable(backend: CryptoBackend) CryptoFunctionTableError!crypto.pp_crypto_fnt { return switch (backend) { .openssl => if (@hasDecl(crypto, "PARTOUT_CRYPTO_OPENSSL")) crypto.pp_crypto_fnt_openssl() else - unreachable, + error.UnsupportedCryptoBackend, .mbedtls => if (@hasDecl(crypto, "PARTOUT_CRYPTO_MBEDTLS")) crypto.pp_crypto_fnt_mbedtls() else - unreachable, + error.UnsupportedCryptoBackend, .native => if (@hasDecl(crypto, "PARTOUT_CRYPTO_MBEDTLS")) crypto.pp_crypto_fnt_native() else - unreachable, + error.UnsupportedCryptoBackend, .mock => if (builtin.is_test) crypto.pp_crypto_fnt_mock() else - unreachable, + error.UnsupportedCryptoBackend, }; } diff --git a/zig/src/core/api_manual.zig b/zig/src/core/api_manual.zig index 08444c2e..0dcea95d 100644 --- a/zig/src/core/api_manual.zig +++ b/zig/src/core/api_manual.zig @@ -456,7 +456,7 @@ pub const Subnet = struct { else switch (parsed_address.family) { .v4 => 32, .v6 => 128, - .hostname => unreachable, + .hostname => return null, }; if (!parsed_address.family.isValidPrefixLength(prefix)) return null; return .{ diff --git a/zig/src/openvpn/connection.zig b/zig/src/openvpn/connection.zig index 4e452922..c90e9c0e 100644 --- a/zig/src/openvpn/connection.zig +++ b/zig/src/openvpn/connection.zig @@ -81,7 +81,7 @@ const OpenVPNConnection = struct { ) net.ConnectionCreateError!net.Connection { const openvpn = switch (module.module.*) { .OpenVPN => |*value| value, - else => unreachable, + else => return error.MissingConnectionImplementation, }; const source_configuration = if (openvpn.configuration) |*value| value @@ -89,11 +89,11 @@ const OpenVPNConnection = struct { return error.IncompleteModule; const looper = sandbox.looper; - var configuration = configurationApplyingActiveModules( + var configuration = try configurationApplyingActiveModules( allocator, source_configuration, sandbox.profile, - ) catch return error.OutOfMemory; + ); errdefer configuration.deinit(allocator); const prng = PRNG.system(); @@ -115,7 +115,7 @@ const OpenVPNConnection = struct { error.InvalidJson, error.InvalidModel, error.UnsupportedModel, - => unreachable, + => return error.IncompleteModule, } else null; @@ -469,24 +469,22 @@ const OpenVPNConnection = struct { .requires_virtual_device = true, .modules = modules, }; - const tunnel = self.controller.setTunnelSettings(info) catch |err| { + self.tunnel = self.controller.setTunnelSettings(info) catch |err| { self.failTunnelSetup(session, tunnelErrorCode(err)); return; }; - self.tunnel = tunnel orelse { + const active_tunnel = if (self.tunnel) |*value| value else { self.failTunnelSetup(session, .tunNotAvailable); return; }; - const descriptor = if (self.tunnel) |*active_tunnel| blk: { - const fd = active_tunnel.muxDescriptor() orelse { - self.failTunnelSetup(session, .fdUnavailable); - return; - }; - break :blk net.Looper.Descriptor{ - .fd = fd, - .io = active_tunnel.nativeIO(), - }; - } else unreachable; + const fd = active_tunnel.muxDescriptor() orelse { + self.failTunnelSetup(session, .fdUnavailable); + return; + }; + const descriptor = net.Looper.Descriptor{ + .fd = fd, + .io = active_tunnel.nativeIO(), + }; session.setTunnel(descriptor) catch |err| { self.failTunnelSetup(session, tunnelErrorCode(err)); return; @@ -832,10 +830,10 @@ fn configurationApplyingActiveModules( allocator: std.mem.Allocator, source: *const api.OpenVPNConfiguration, profile: *const api.Profile, -) std.mem.Allocator.Error!api.OpenVPNConfiguration { +) net.ConnectionCreateError!api.OpenVPNConfiguration { var configuration = source.clone(allocator) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, - error.InvalidJson, error.InvalidModel, error.UnsupportedModel => unreachable, + error.InvalidJson, error.InvalidModel, error.UnsupportedModel => return error.IncompleteModule, }; errdefer configuration.deinit(allocator); @@ -1012,7 +1010,7 @@ pub const testing = struct { allocator: std.mem.Allocator, source: *const api.OpenVPNConfiguration, profile: *const api.Profile, - ) std.mem.Allocator.Error!api.OpenVPNConfiguration { + ) net.ConnectionCreateError!api.OpenVPNConfiguration { return configurationApplyingActiveModules(allocator, source, profile); } }; diff --git a/zig/src/openvpn/internal/auth.zig b/zig/src/openvpn/internal/auth.zig index ec696810..7b422b01 100644 --- a/zig/src/openvpn/internal/auth.zig +++ b/zig/src/openvpn/internal/auth.zig @@ -77,9 +77,10 @@ pub const PRF = struct { session_id: []const u8, remote_session_id: []const u8, ) !PRF { + const functions = try c_exports_mod.cryptoFunctionTable(backend); return initWithFunctions( allocator, - c_exports_mod.cryptoFunctionTable(backend), + functions, handshake, session_id, remote_session_id, @@ -148,11 +149,11 @@ pub const PRF = struct { var initialized: usize = 0; errdefer for (parts[0..initialized]) |*part| part.deinit(allocator); for (&parts, 0..) |*part, index| { - part.* = keys_data.sliceCopy( + part.* = try keys_data.sliceCopy( allocator, index * KeysConstants.key_length, KeysConstants.key_length, - ) catch unreachable; + ); initialized += 1; } @@ -223,7 +224,7 @@ pub const PRF = struct { chain = next_chain.move(); } - const truncated = output.sliceCopy(allocator, 0, size) catch unreachable; + const truncated = try output.sliceCopy(allocator, 0, size); output.deinit(allocator); return truncated; } @@ -250,7 +251,7 @@ pub const PRF = struct { const hmac_do = functions.hmac_do orelse return error.UnsupportedAlgorithm; const length = hmac_do(&context); if (length == 0 or length > buffer.bytes.len) return error.UnsupportedAlgorithm; - const result = buffer.sliceCopy(allocator, 0, length) catch unreachable; + const result = try buffer.sliceCopy(allocator, 0, length); buffer.deinit(allocator); return result; } @@ -443,27 +444,27 @@ pub const Authenticator = struct { offset += KeysConstants.random_length; const random2_offset = offset; offset += KeysConstants.random_length; - const options_length = self.control_buffer.networkU16(offset) catch unreachable; + const options_length = try self.control_buffer.networkU16(offset); offset += 2; if (self.control_buffer.bytes.len - offset < options_length) return false; - var server_random1 = self.control_buffer.sliceCopy( + var server_random1 = try self.control_buffer.sliceCopy( self.allocator, random1_offset, KeysConstants.random_length, - ) catch unreachable; + ); errdefer server_random1.deinit(self.allocator); - var server_random2 = self.control_buffer.sliceCopy( + var server_random2 = try self.control_buffer.sliceCopy( self.allocator, random2_offset, KeysConstants.random_length, - ) catch unreachable; + ); errdefer server_random2.deinit(self.allocator); - var server_options_data = self.control_buffer.sliceCopy( + var server_options_data = try self.control_buffer.sliceCopy( self.allocator, offset, options_length, - ) catch unreachable; + ); defer server_options_data.deinit(self.allocator); offset += options_length; @@ -508,7 +509,7 @@ pub const Authenticator = struct { try messages.append(allocator, try allocator.dupe(u8, message)); offset += length + 1; } - self.control_buffer.removePrefix(self.allocator, offset) catch unreachable; + try self.control_buffer.removePrefix(self.allocator, offset); return messages.toOwnedSlice(allocator); } diff --git a/zig/src/openvpn/internal/data.zig b/zig/src/openvpn/internal/data.zig index d534ba78..2a4d7768 100644 --- a/zig/src/openvpn/internal/data.zig +++ b/zig/src/openvpn/internal/data.zig @@ -321,11 +321,12 @@ pub const DataPathWrapper = struct { parameters: DataPathParameters, prf: *const PRF, seed: ZeroingData, - ) !DataPathWrapper { - const functions = c_exports_mod.cryptoFunctionTable(parameters.backend).enc; + ) Error!DataPathWrapper { + const functions = (c_exports_mod.cryptoFunctionTable(parameters.backend) catch + return error.UnsupportedAlgorithm).enc; const init_seed = functions.init_seed orelse return error.UnsupportedAlgorithm; _ = init_seed(seed.bytes.ptr, seed.bytes.len); - var keys = try prf.derive(allocator); + var keys = prf.derive(allocator) catch return error.CryptoFailure; defer keys.deinit(allocator); return createWithKeys(allocator, parameters, functions, &keys); } diff --git a/zig/src/openvpn/internal/serialization.zig b/zig/src/openvpn/internal/serialization.zig index 85cebc22..dbabd586 100644 --- a/zig/src/openvpn/internal/serialization.zig +++ b/zig/src/openvpn/internal/serialization.zig @@ -189,7 +189,7 @@ const AuthSerializer = struct { digest: api.OpenVPNDigest, key: api.OpenVPNStaticKey, ) !AuthSerializer { - const functions = c_exports_mod.cryptoFunctionTable(backend).enc; + const functions = (try c_exports_mod.cryptoFunctionTable(backend)).enc; var keys = try deriveKeys(allocator, key); defer keys.deinit(allocator); var bridge = try CryptoKeysBridge.init(allocator, &keys); @@ -308,7 +308,7 @@ const CryptSerializer = struct { backend: CryptoBackend, key: api.OpenVPNStaticKey, ) !CryptSerializer { - const functions = c_exports_mod.cryptoFunctionTable(backend).enc; + const functions = (try c_exports_mod.cryptoFunctionTable(backend)).enc; var keys = try deriveKeys(allocator, key); defer keys.deinit(allocator); var bridge = try CryptoKeysBridge.init(allocator, &keys); diff --git a/zig/src/openvpn/internal/tls.zig b/zig/src/openvpn/internal/tls.zig index c7fe2a26..1970bf85 100644 --- a/zig/src/openvpn/internal/tls.zig +++ b/zig/src/openvpn/internal/tls.zig @@ -54,10 +54,11 @@ pub const TLSWrapper = struct { allocator: std.mem.Allocator, parameters: TLSParameters, ) !*TLSWrapper { + const functions = (try c_exports_mod.cryptoFunctionTable(parameters.backend)).tls; return createWithFunctions( allocator, parameters, - c_exports_mod.cryptoFunctionTable(parameters.backend).tls, + functions, ); } diff --git a/zig/src/openvpn/parser.zig b/zig/src/openvpn/parser.zig index 6131c0ae..3aa4ed05 100644 --- a/zig/src/openvpn/parser.zig +++ b/zig/src/openvpn/parser.zig @@ -188,7 +188,8 @@ fn decryptKeyWithBackend(comptime backend: CryptoBackend) Parser.DecryptKey { c_passphrase.deinit(); } - const function_table = c_mod.cryptoFunctionTable(backend); + const function_table = c_mod.cryptoFunctionTable(backend) catch + return error.DecryptionFailed; const decrypt_function = function_table.key_decrypted_from_pem orelse return error.DecryptionFailed; const c_decrypted = decrypt_function(c_pem.ptr(), c_passphrase.ptr()) orelse diff --git a/zig/src/wireguard/connection.zig b/zig/src/wireguard/connection.zig index 6d26bf7b..a0d5f62f 100644 --- a/zig/src/wireguard/connection.zig +++ b/zig/src/wireguard/connection.zig @@ -72,7 +72,7 @@ const WireGuardConnection = struct { return error.IncompleteModule; break :blk configuration; }, - else => unreachable, + else => return error.MissingConnectionImplementation, }; const created = try allocator.create(WireGuardConnection); @@ -321,10 +321,10 @@ fn configurationApplyingActiveModules( allocator: std.mem.Allocator, source: *const api.WireGuardConfiguration, profile: *const api.Profile, -) std.mem.Allocator.Error!api.WireGuardConfiguration { +) net.ConnectionCreateError!api.WireGuardConfiguration { var configuration = source.clone(allocator) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, - error.InvalidJson, error.InvalidModel, error.UnsupportedModel => unreachable, + error.InvalidJson, error.InvalidModel, error.UnsupportedModel => return error.IncompleteModule, }; errdefer configuration.deinit(allocator); @@ -337,7 +337,7 @@ fn appendActiveModuleAllowedIPs( allocator: std.mem.Allocator, peer: *api.WireGuardRemoteInterface, profile: *const api.Profile, -) std.mem.Allocator.Error!void { +) net.ConnectionCreateError!void { var extra_count: usize = 0; for (profile.modules) |*module| { if (!api.isActiveProfileModule(profile, api.moduleId(module))) continue; @@ -412,25 +412,28 @@ fn cloneRouteDestination( allocator: std.mem.Allocator, route: *const api.Route, family: api.Address.Family, -) std.mem.Allocator.Error!api.Subnet { +) net.ConnectionCreateError!api.Subnet { if (route.destination) |*destination| return cloneSubnet(allocator, destination); return switch (family) { - .v4 => (try api.Subnet.parseRawAlloc(allocator, "0.0.0.0/0")).?, - .v6 => (try api.Subnet.parseRawAlloc(allocator, "::/0")).?, - .hostname => unreachable, + .v4 => (try api.Subnet.parseRawAlloc(allocator, "0.0.0.0/0")) orelse + error.IncompleteModule, + .v6 => (try api.Subnet.parseRawAlloc(allocator, "::/0")) orelse + error.IncompleteModule, + .hostname => error.IncompleteModule, }; } fn cloneAddressAsHostSubnet( allocator: std.mem.Allocator, address: *const api.Address, -) std.mem.Allocator.Error!api.Subnet { +) net.ConnectionCreateError!api.Subnet { return .{ - .address = (try api.Address.parseRawAlloc(allocator, address.raw)).?, + .address = (try api.Address.parseRawAlloc(allocator, address.raw)) orelse + return error.IncompleteModule, .prefix_length = switch (address.family) { .v4 => 32, .v6 => 128, - .hostname => unreachable, + .hostname => return error.IncompleteModule, }, }; } @@ -438,9 +441,10 @@ fn cloneAddressAsHostSubnet( fn cloneSubnet( allocator: std.mem.Allocator, subnet: *const api.Subnet, -) std.mem.Allocator.Error!api.Subnet { +) net.ConnectionCreateError!api.Subnet { return .{ - .address = (try api.Address.parseRawAlloc(allocator, subnet.address.raw)).?, + .address = (try api.Address.parseRawAlloc(allocator, subnet.address.raw)) orelse + return error.IncompleteModule, .prefix_length = subnet.prefix_length, }; } @@ -496,7 +500,7 @@ pub const testing = struct { allocator: std.mem.Allocator, source: *const api.WireGuardConfiguration, profile: *const api.Profile, - ) std.mem.Allocator.Error!api.WireGuardConfiguration { + ) net.ConnectionCreateError!api.WireGuardConfiguration { return configurationApplyingActiveModules(allocator, source, profile); } diff --git a/zig/src/wireguard/internal/resolver.zig b/zig/src/wireguard/internal/resolver.zig index d13180e7..d5f5bbc6 100644 --- a/zig/src/wireguard/internal/resolver.zig +++ b/zig/src/wireguard/internal/resolver.zig @@ -134,7 +134,7 @@ pub const PeerEndpointResolver = struct { // DNS64 networks this can replace an obsolete NAT64 prefix without a // new hostname lookup; other resolvers simply preserve the address. try self.refreshTargets(allocator); - return self.cache.value().?; + return self.cache.value() orelse error.DNSResolutionFailure; } fn populate( @@ -187,9 +187,9 @@ pub const PeerEndpointResolver = struct { fn refreshTargets( self: *const PeerEndpointResolver, allocator: std.mem.Allocator, - ) std.mem.Allocator.Error!void { + ) ResolutionError!void { const reachability = if (self.factory) |factory| factory.currentReachability() else null; - const entries = self.cache.mutableValue().?; + const entries = self.cache.mutableValue() orelse return error.DNSResolutionFailure; for (entries) |*entry| { var mapped = self.resolver.resolveAddress( allocator, @@ -204,8 +204,11 @@ pub const PeerEndpointResolver = struct { }, }; - const parsed = api.Address.parseRaw(mapped); - if (parsed == null or !parsed.?.isIPAddress()) { + const mapped_is_ip_address = if (api.Address.parseRaw(mapped)) |parsed| + parsed.isIPAddress() + else + false; + if (!mapped_is_ip_address) { log.writef(.err, "Unable to re-resolve endpoint: {s}", .{ @errorName(error.InvalidEndpoint), }); @@ -222,10 +225,10 @@ pub const PeerEndpointResolver = struct { previous.deinit(allocator); const source_address = api.Address.parseRaw( entry.base.address, - ) orelse unreachable; + ) orelse return error.InvalidEndpoint; const target_address = api.Address.parseRaw( entry.target.address, - ) orelse unreachable; + ) orelse return error.InvalidEndpoint; if (std.mem.eql(u8, source_address.raw, target_address.raw)) { log.writef( .debug, @@ -271,7 +274,7 @@ pub const PeerEndpointResolver = struct { defer util.freeSlice(net.DNSRecord, allocator, records); const target_address = preferredAddress(records) orelse return error.DNSResolutionFailure; - const target = api.Address.parseRaw(target_address) orelse unreachable; + const target = api.Address.parseRaw(target_address) orelse return error.InvalidEndpoint; log.writef(.debug, "DNS64: mapped {s} to {s}", .{ address, target }); return (api.Endpoint{ .address = target_address, diff --git a/zig/src/wireguard/internal/tunnel_info.zig b/zig/src/wireguard/internal/tunnel_info.zig index 98a3e98a..11e66d0b 100644 --- a/zig/src/wireguard/internal/tunnel_info.zig +++ b/zig/src/wireguard/internal/tunnel_info.zig @@ -15,7 +15,10 @@ pub const TunnelRemoteInfoBuilder = struct { module_id: api.UUID, configuration: *const api.WireGuardConfiguration, - pub const Error = std.mem.Allocator.Error || error{IdGeneration}; + pub const Error = std.mem.Allocator.Error || error{ + IdGeneration, + InvalidConfiguration, + }; const Self = @This(); @@ -39,7 +42,7 @@ pub const TunnelRemoteInfoBuilder = struct { var profile = self.profile.clone(self.allocator) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, - error.InvalidJson, error.InvalidModel, error.UnsupportedModel => unreachable, + error.InvalidJson, error.InvalidModel, error.UnsupportedModel => return error.InvalidConfiguration, }; errdefer profile.deinit(self.allocator); @@ -50,7 +53,10 @@ pub const TunnelRemoteInfoBuilder = struct { // legitimately have zero endpoints or a different endpoint per // peer. Loopback is only a harmless settings placeholder; it is // never given to wg-go and cannot route onto the Internet. - .address = api.Address.parseRaw("127.0.0.1").?, + .address = .{ + .raw = "127.0.0.1", + .family = .v4, + }, .requires_virtual_device = builtin.os.tag != .windows, .modules = modules, }; @@ -62,7 +68,7 @@ pub const TunnelRemoteInfoBuilder = struct { // as Swift does so controller/reporting code can correlate it. return source.clone(self.allocator) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, - error.InvalidJson, error.InvalidModel, error.UnsupportedModel => unreachable, + error.InvalidJson, error.InvalidModel, error.UnsupportedModel => return error.InvalidConfiguration, }; } @@ -122,13 +128,19 @@ pub const TunnelRemoteInfoBuilder = struct { fn buildInterfaceRoute(self: Self, subnet: api.Subnet) Error!api.Route { const destination_text = subnet.networkRawAlloc(self.allocator) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, - error.InvalidModel, error.Stringify => unreachable, + error.InvalidModel, error.Stringify => return error.InvalidConfiguration, }; defer self.allocator.free(destination_text); - var destination = (try api.Subnet.parseRawAlloc(self.allocator, destination_text)) orelse unreachable; + var destination = (try api.Subnet.parseRawAlloc( + self.allocator, + destination_text, + )) orelse return error.InvalidConfiguration; errdefer destination.deinit(self.allocator); - const gateway = (try api.Address.parseRawAlloc(self.allocator, subnet.address.raw)) orelse unreachable; + const gateway = (try api.Address.parseRawAlloc( + self.allocator, + subnet.address.raw, + )) orelse return error.InvalidConfiguration; return .{ .destination = destination, .gateway = gateway, @@ -175,7 +187,10 @@ pub const TunnelRemoteInfoBuilder = struct { fn cloneSubnet(self: Self, subnet: api.Subnet, prefix: u8) Error!api.Subnet { return .{ - .address = (try api.Address.parseRawAlloc(self.allocator, subnet.address.raw)) orelse unreachable, + .address = (try api.Address.parseRawAlloc( + self.allocator, + subnet.address.raw, + )) orelse return error.InvalidConfiguration, .prefix_length = prefix, }; } diff --git a/zig/tests/c/exports.zig b/zig/tests/c/exports.zig index ce8bf437..343d9191 100644 --- a/zig/tests/c/exports.zig +++ b/zig/tests/c/exports.zig @@ -20,17 +20,31 @@ test "default crypto backend follows compiled backends" { } test "crypto function table follows the selected backend" { - const mock = c_exports.cryptoFunctionTable(.mock); + const mock = try c_exports.cryptoFunctionTable(.mock); try std.testing.expectEqualStrings("mock", std.mem.span(mock.name)); if (@hasDecl(c_crypto, "PARTOUT_CRYPTO_OPENSSL")) { - const openssl = c_exports.cryptoFunctionTable(.openssl); + const openssl = try c_exports.cryptoFunctionTable(.openssl); try std.testing.expectEqualStrings("openssl", std.mem.span(openssl.name)); + } else { + try std.testing.expectError( + error.UnsupportedCryptoBackend, + c_exports.cryptoFunctionTable(.openssl), + ); } if (@hasDecl(c_crypto, "PARTOUT_CRYPTO_MBEDTLS")) { - const mbedtls = c_exports.cryptoFunctionTable(.mbedtls); + const mbedtls = try c_exports.cryptoFunctionTable(.mbedtls); try std.testing.expectEqualStrings("mbed", std.mem.span(mbedtls.name)); - const native = c_exports.cryptoFunctionTable(.native); + const native = try c_exports.cryptoFunctionTable(.native); try std.testing.expect(std.mem.startsWith(u8, std.mem.span(native.name), "native-")); + } else { + try std.testing.expectError( + error.UnsupportedCryptoBackend, + c_exports.cryptoFunctionTable(.mbedtls), + ); + try std.testing.expectError( + error.UnsupportedCryptoBackend, + c_exports.cryptoFunctionTable(.native), + ); } } From 659dd6e1171129885202f66709c7d20014ecd00c Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 15:42:41 +0200 Subject: [PATCH 08/11] Do not care about looper terminal failure, just log it --- zig/src/net/daemon.zig | 5 ++--- zig/src/net/looper.zig | 18 +++--------------- zig/src/net/looper_queue.zig | 4 +--- zig/src/openvpn/internal/session.zig | 4 ++-- zig/tests/net/looper_queue.zig | 4 ++-- 5 files changed, 10 insertions(+), 25 deletions(-) diff --git a/zig/src/net/daemon.zig b/zig/src/net/daemon.zig index 18809398..0d9322ab 100644 --- a/zig/src/net/daemon.zig +++ b/zig/src/net/daemon.zig @@ -840,9 +840,8 @@ pub const Daemon = struct { // Keep the borrowed looper object alive until the connection has // released every Session that refers to it, but first join its worker // so the terminal callback cannot race connection deinitialization. - runtime.looper.stop() catch |err| switch (err) { - error.TerminalFailure => {}, - else => log.writef(.debug, "Unable to stop connection looper: {s}", .{@errorName(err)}), + runtime.looper.stop() catch |err| { + log.writef(.debug, "Unable to stop connection looper: {s}", .{@errorName(err)}); }; runtime.connection.destroy(); runtime.looper.deinit(); diff --git a/zig/src/net/looper.zig b/zig/src/net/looper.zig index cc1d64d1..5427620d 100644 --- a/zig/src/net/looper.zig +++ b/zig/src/net/looper.zig @@ -102,8 +102,7 @@ pub const Looper = struct { pub const ScheduleError = SubmissionError || Errors.TaskFailure; pub const StopError = SubmissionError || Errors.InvalidState || - Errors.ReentrantCall || - Errors.TerminalFailure; + Errors.ReentrantCall; pub const WriteError = SubmissionError || io.Error || Errors.TransformFailure || @@ -117,7 +116,6 @@ pub const Looper = struct { lock: core.Mutex = .{}, condition: core.Condition = .{}, state: State = .idle, - terminal_failure: ?Failure = null, // Command submission and synchronous completion. commands: CommandQueue = .{}, @@ -375,10 +373,8 @@ pub const Looper = struct { while (self.state == .stopping) { self.condition.wait(&self.lock); } - const failed = self.terminal_failure != null; self.lock.unlock(); self.joinWorker(); - if (failed) return error.TerminalFailure; return; }, } @@ -407,7 +403,6 @@ pub const Looper = struct { if (completion_failure) |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.Cancelled => return error.Cancelled, - error.TerminalFailure => return error.TerminalFailure, else => log.writefAndFailDebug("Ignoring unexpected Looper stop completion error: {s}", .{ @errorName(err), }), @@ -548,7 +543,7 @@ pub const Looper = struct { error.OutOfMemory => error.OutOfMemory, error.Cancelled => error.Cancelled, error.OperationCancelled => error.OperationCancelled, - else => error.MuxFailure, + error.MuxFailure => error.MuxFailure, }; } @@ -599,12 +594,6 @@ pub const Looper = struct { return self.tun != null; } - pub fn terminalFailure(self: *Looper) ?Failure { - self.lock.lock(); - defer self.lock.unlock(); - return self.terminal_failure; - } - pub fn resumeReading(self: *Looper, side: io.Side) ResumeReadingError!void { self.lock.lock(); defer self.lock.unlock(); @@ -1098,12 +1087,11 @@ pub const Looper = struct { else => {}, } self.state = .stopped; - self.terminal_failure = failure; self.cancelPendingLocked(self.commands.takeReady()); self.read_retries = .{ false, false }; self.write_retries = .{ false, false }; if (self.stop_completion) |completion| { - completeNow(completion, if (failure != null) error.TerminalFailure else null); + completeNow(completion, null); self.stop_completion = null; } self.releaseCompletionsLocked(); diff --git a/zig/src/net/looper_queue.zig b/zig/src/net/looper_queue.zig index 797cc9d2..a5fb6660 100644 --- a/zig/src/net/looper_queue.zig +++ b/zig/src/net/looper_queue.zig @@ -131,7 +131,6 @@ pub const Errors = struct { pub const OperationCancelled = error{OperationCancelled}; pub const ReentrantCall = error{ReentrantCall}; pub const TaskFailure = error{TaskFailure}; - pub const TerminalFailure = error{TerminalFailure}; pub const TransformFailure = error{TransformFailure}; pub const WriteIncomplete = error{WriteIncomplete}; }; @@ -139,8 +138,7 @@ pub const Errors = struct { pub const CompletionError = std.mem.Allocator.Error || Errors.Cancelled || Errors.MuxFailure || - Errors.OperationCancelled || - Errors.TerminalFailure; + Errors.OperationCancelled; pub const Completion = struct { // Completion state. diff --git a/zig/src/openvpn/internal/session.zig b/zig/src/openvpn/internal/session.zig index fb161a30..ec72fafb 100644 --- a/zig/src/openvpn/internal/session.zig +++ b/zig/src/openvpn/internal/session.zig @@ -380,9 +380,9 @@ pub const Session = struct { &prepare, prepareShutdownOnQueue, ) catch |err| { - // A terminal looper has already serialized final state; its owner + // A stopped looper has already serialized final state; its owner // routes `OnFinish` through `looperDidTerminate` while Session lives. - if (err == error.Cancelled or err == error.TerminalFailure) return; + if (err == error.Cancelled) return; log.writef(.err, "Unable to shut down session on looper queue: {s}", .{ @errorName(err), }); diff --git a/zig/tests/net/looper_queue.zig b/zig/tests/net/looper_queue.zig index a9cfb1b5..d961da93 100644 --- a/zig/tests/net/looper_queue.zig +++ b/zig/tests/net/looper_queue.zig @@ -33,10 +33,10 @@ test "completion queue releases completions in FIFO order" { try std.testing.expect(second.next == null); var third = Completion{}; - queue.append(&third, error.TerminalFailure); + queue.append(&third, error.OperationCancelled); queue.releaseAll(); try std.testing.expect(third.done); - try std.testing.expect(third.failure.? == error.TerminalFailure); + try std.testing.expect(third.failure.? == error.OperationCancelled); } test "command queue detaches ready commands in FIFO order" { From 1140460787043b61ea75d50c64ee7c8660339406 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 12:44:17 +0200 Subject: [PATCH 09/11] Panic on hard failures - Syscall failures - Unrecoverable looper failures - RunAfter bugs --- zig/src/core/concurrency.zig | 18 +++++++++--------- zig/src/net/looper.zig | 13 +++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/zig/src/core/concurrency.zig b/zig/src/core/concurrency.zig index 0c5a686c..583e2224 100644 --- a/zig/src/core/concurrency.zig +++ b/zig/src/core/concurrency.zig @@ -375,7 +375,7 @@ pub const RunAfter = struct { self.mutex.unlock(); return; }, - .idle, .scheduled => unreachable, + .idle, .scheduled => @panic("invalid RunAfter state after callback"), } self.cond.broadcast(); self.mutex.unlock(); @@ -450,7 +450,7 @@ pub const RunAfter = struct { self.state = switch (self.state) { .idle, .scheduled => .scheduled, .running, .running_scheduled => .running_scheduled, - .stopping => unreachable, + .stopping => @panic("cannot schedule a stopping RunAfter"), }; self.cond.broadcast(); } @@ -545,7 +545,7 @@ const PosixMutex = struct { pub fn lock(self: *PosixMutex) void { switch (std.c.pthread_mutex_lock(&self.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_mutex_lock failed"), } } @@ -555,7 +555,7 @@ const PosixMutex = struct { pub fn unlock(self: *PosixMutex) void { switch (std.c.pthread_mutex_unlock(&self.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_mutex_unlock failed"), } } @@ -565,7 +565,7 @@ const PosixMutex = struct { pub fn deinit(self: *PosixMutex) void { switch (std.c.pthread_mutex_destroy(&self.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_mutex_destroy failed"), } } }; @@ -582,7 +582,7 @@ const PosixCondition = struct { pub fn wait(self: *PosixCondition, mutex: *Mutex) void { switch (std.c.pthread_cond_wait(&self.raw, &mutex.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_cond_wait failed"), } } @@ -592,7 +592,7 @@ const PosixCondition = struct { pub fn broadcast(self: *PosixCondition) void { switch (std.c.pthread_cond_broadcast(&self.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_cond_broadcast failed"), } } @@ -602,7 +602,7 @@ const PosixCondition = struct { pub fn deinit(self: *PosixCondition) void { switch (std.c.pthread_cond_destroy(&self.raw)) { .SUCCESS => {}, - else => unreachable, + else => @panic("pthread_cond_destroy failed"), } } }; @@ -659,7 +659,7 @@ const WindowsCondition = struct { pub fn wait(self: *WindowsCondition, mutex: *Mutex) void { switch (RtlSleepConditionVariableSRW(&self.raw, &mutex.raw, null, 0)) { .SUCCESS => {}, - else => unreachable, + else => @panic("RtlSleepConditionVariableSRW failed"), } } diff --git a/zig/src/net/looper.zig b/zig/src/net/looper.zig index 5427620d..72c061d8 100644 --- a/zig/src/net/looper.zig +++ b/zig/src/net/looper.zig @@ -364,7 +364,7 @@ pub const Looper = struct { return; }, .started => {}, - .starting => unreachable, + .starting => @panic("Looper remained starting after wait"), .deinitializing => { self.lock.unlock(); return error.Cancelled; @@ -435,7 +435,12 @@ pub const Looper = struct { } if (delay_ms) |delay| { const node = try self.createCommandNode(.{ .perform = task }); - self.scheduler.schedule(&node.timer, delay, onScheduledCommand, self) catch unreachable; + self.scheduler.schedule( + &node.timer, + delay, + onScheduledCommand, + self, + ) catch @panic("Looper scheduler failed after start"); return; } const node = try self.createCommandNode(.{ .perform = task }); @@ -1031,7 +1036,7 @@ pub const Looper = struct { no_buf_retry_delay_ms, onScheduledCommand, self, - ) catch unreachable; + ) catch @panic("Looper read-retry scheduling failed after start"); return null; } @@ -1050,7 +1055,7 @@ pub const Looper = struct { no_buf_retry_delay_ms, onScheduledCommand, self, - ) catch unreachable; + ) catch @panic("Looper write-retry scheduling failed after start"); return null; } From 77b204a2a0789c95227fefecd06a9d3d128be1c8 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 15:52:25 +0200 Subject: [PATCH 10/11] Document Looper.stop() --- zig/src/net/looper.zig | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/zig/src/net/looper.zig b/zig/src/net/looper.zig index 72c061d8..a5b86fe7 100644 --- a/zig/src/net/looper.zig +++ b/zig/src/net/looper.zig @@ -349,6 +349,22 @@ pub const Looper = struct { return true; } + /// Requests an orderly stop and waits for the worker thread to terminate. + /// + /// The stop command runs after work that the looper has already accepted. + /// Entering `.stopping` rejects new work, and delayed commands that have not + /// become ready are cancelled when the worker finishes. Calling `stop` + /// before `start`, or after the worker has already stopped, is a no-op. + /// + /// This function must run outside the looper thread and outside callbacks + /// that borrow looper-owned state. It waits for an in-progress `start` or + /// `stop` transition, but returns `error.Cancelled` if `deinit` takes + /// ownership of shutdown. Failure to allocate the stop command is returned + /// to the caller. + /// + /// A failure that independently terminates the worker is logged by + /// `finish` and delivered to `on_finish`; it is not returned by `stop`. + /// `deinit` is still required to release the looper's resources. pub fn stop(self: *Looper) StopError!void { if (self.isReentrantLifecycleCall()) return error.ReentrantCall; @@ -360,16 +376,24 @@ pub const Looper = struct { } switch (self.state) { .idle => { + // No worker was started, so there is nothing to stop or join. self.lock.unlock(); return; }, - .started => {}, - .starting => @panic("Looper remained starting after wait"), + .started => { + // This caller owns the transition to `.stopping` below. + }, + .starting => { + // The condition loop above must consume this transient state. + @panic("Looper remained starting after wait"); + }, .deinitializing => { + // `deinit` owns shutdown and will cancel pending work and join. self.lock.unlock(); return error.Cancelled; }, - .stopping, .stopped => { + .stopping => { + // Another caller initiated shutdown; wait for it to finish. while (self.state == .stopping) { self.condition.wait(&self.lock); } @@ -377,6 +401,12 @@ pub const Looper = struct { self.joinWorker(); return; }, + .stopped => { + // The worker already finished, but may still need to be joined. + self.lock.unlock(); + self.joinWorker(); + return; + }, } const node = self.createCommandNode(.stop) catch |err| { self.lock.unlock(); From b4a26facce27e81074bbf2e42bcdac6b617f81e2 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 26 Jul 2026 15:59:14 +0200 Subject: [PATCH 11/11] Rename imports --- zig/src/openvpn/internal/session.zig | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/zig/src/openvpn/internal/session.zig b/zig/src/openvpn/internal/session.zig index ec72fafb..91192459 100644 --- a/zig/src/openvpn/internal/session.zig +++ b/zig/src/openvpn/internal/session.zig @@ -4,8 +4,8 @@ const std = @import("std"); -const core_mod = @import("../../core/exports.zig"); -const net_mod = @import("../../net/exports.zig"); +const core = @import("../../core/exports.zig"); +const net = @import("../../net/exports.zig"); const configuration_mod = @import("configuration.zig"); const constants_mod = @import("constants.zig"); const control_mod = @import("control.zig"); @@ -22,9 +22,9 @@ const session_negotiator_mod = @import("session_negotiator.zig"); const tls_mod = @import("tls.zig"); const session_mod = @This(); -const api = core_mod.api; +const api = core.api; const c = helpers_mod.c; -const log = core_mod.logging; +const log = core.logging; const ActiveContext = session_context_mod.ActiveContext; const ActivePhase = session_context_mod.ActivePhase; @@ -118,12 +118,12 @@ pub const Session = struct { ca_filename: []u8, options: SessionOptions, - looper: *net_mod.Looper, + looper: *net.Looper, control_channel: *ControlChannel, shutdown_actor: ?*ShutdownActor, - lifecycle_lock: core_mod.Mutex = .{}, - negotiation_timer: core_mod.RunAfter = .{}, - ping_timer: core_mod.RunAfter = .{}, + lifecycle_lock: core.Mutex = .{}, + negotiation_timer: core.RunAfter = .{}, + ping_timer: core.RunAfter = .{}, delegate: ?SessionDelegate = null, state: SessionState = .{ .stopped = .{ .with_local_options = true } }, @@ -138,7 +138,7 @@ pub const Session = struct { ShutdownFailure, }; - const ShutdownActor = core_mod.Actor( + const ShutdownActor = core.Actor( Session, ShutdownRequest, ShutdownActorError, @@ -147,7 +147,7 @@ pub const Session = struct { pub fn create( allocator: std.mem.Allocator, - looper: *net_mod.Looper, + looper: *net.Looper, configuration: api.OpenVPNConfiguration, credentials: ?api.OpenVPNCredentials, prng: PRNG, @@ -169,7 +169,7 @@ pub const Session = struct { fn createUnwrapped( allocator: std.mem.Allocator, - looper: *net_mod.Looper, + looper: *net.Looper, configuration: api.OpenVPNConfiguration, credentials: ?api.OpenVPNCredentials, prng: PRNG, @@ -268,7 +268,7 @@ pub const Session = struct { pub fn setLink( self: *Session, - descriptor: net_mod.Looper.Descriptor, + descriptor: net.Looper.Descriptor, remote_endpoint: api.ExtendedEndpoint, ) Error!void { return self.setLinkUnwrapped(descriptor, remote_endpoint) catch |err| @@ -277,7 +277,7 @@ pub const Session = struct { fn setLinkUnwrapped( self: *Session, - descriptor: net_mod.Looper.Descriptor, + descriptor: net.Looper.Descriptor, remote_endpoint: api.ExtendedEndpoint, ) !void { var descriptor_transferred = false; @@ -321,12 +321,12 @@ pub const Session = struct { try self.looper.perform(void, &request, setLinkOnQueue); } - pub fn setTunnel(self: *Session, descriptor: net_mod.Looper.Descriptor) Error!void { + pub fn setTunnel(self: *Session, descriptor: net.Looper.Descriptor) Error!void { return self.setTunnelUnwrapped(descriptor) catch |err| errors_mod.sessionError(err); } - fn setTunnelUnwrapped(self: *Session, descriptor: net_mod.Looper.Descriptor) !void { + fn setTunnelUnwrapped(self: *Session, descriptor: net.Looper.Descriptor) !void { var descriptor_transferred = false; defer if (!descriptor_transferred) descriptor.io.cleanup(); @@ -520,7 +520,7 @@ pub const Session = struct { /// Routes the externally owned looper's terminal callback into the /// session. The owner must call this synchronously from `Looper.OnFinish` /// while the Session is alive, and must stop forwarding before `destroy`. - pub fn looperDidTerminate(self: *Session, failure: ?net_mod.Looper.Failure) void { + pub fn looperDidTerminate(self: *Session, failure: ?net.Looper.Failure) void { std.debug.assert(self.looper.isOnQueue()); if (failure) |value| switch (value) { .user => |cause| log.writef(.err, "Session looper finished with error: {s}", .{ @@ -539,12 +539,12 @@ pub const Session = struct { self.finishShutdown(if (failure) |value| failureError(value) else null); } - fn onSideFailure(raw: ?*anyopaque, failure: net_mod.Looper.Failure) void { + fn onSideFailure(raw: ?*anyopaque, failure: net.Looper.Failure) void { const self: *Session = @ptrCast(@alignCast(raw.?)); self.requestShutdown(failureError(failure)); } - fn onLinkFailure(raw: ?*anyopaque, failure: net_mod.Looper.Failure) void { + fn onLinkFailure(raw: ?*anyopaque, failure: net.Looper.Failure) void { const self: *Session = @ptrCast(@alignCast(raw.?)); const cause = failureError(failure); self.requestShutdown(if (cause == error.CryptoFailure) error.Reconnect else cause); @@ -552,8 +552,8 @@ pub const Session = struct { fn onLinkRead( raw: ?*anyopaque, - packets: net_mod.Looper.Packets, - ) !net_mod.Looper.ReadAction { + packets: net.Looper.Packets, + ) !net.Looper.ReadAction { const self: *Session = @ptrCast(@alignCast(raw.?)); const processor = self.link_processor orelse return .keep; var processed = try processor.processInbound(packets); @@ -564,8 +564,8 @@ pub const Session = struct { fn onTunnelRead( raw: ?*anyopaque, - packets: net_mod.Looper.Packets, - ) !net_mod.Looper.ReadAction { + packets: net.Looper.Packets, + ) !net.Looper.ReadAction { const self: *Session = @ptrCast(@alignCast(raw.?)); try self.receiveTunnel(packets); return .keep; @@ -574,7 +574,7 @@ pub const Session = struct { fn receiveLink(self: *Session, packets: []const []const u8) !void { std.debug.assert(self.looper.isOnQueue()); const context = self.state.activeContext() orelse return; - context.last_received_ns = core_mod.concurrency.monotonicNs(); + context.last_received_ns = core.concurrency.monotonicNs(); var negotiator = context.currentNegotiator() orelse { log.write(.fault, "No negotiator"); return error.Assertion; @@ -864,7 +864,7 @@ pub const Session = struct { const last_received = context.last_received_ns orelse return; const timeout_ns = self.keepAliveTimeoutMs(context) *| @as(u64, std.time.ns_per_ms); - if (core_mod.concurrency.monotonicNs() -| last_received > timeout_ns) + if (core.concurrency.monotonicNs() -| last_received > timeout_ns) return error.Timeout; } @@ -923,7 +923,7 @@ pub const Session = struct { } fn delegateCurrentDataCount(self: *Session, context: *ActiveContext) void { - const now = core_mod.concurrency.monotonicNs(); + const now = core.concurrency.monotonicNs(); if (context.last_data_count_ns) |last| { const minimum = self.options.min_data_count_interval_ms *| @as(u64, std.time.ns_per_ms); @@ -936,7 +936,7 @@ pub const Session = struct { }); } - fn failureError(failure: net_mod.Looper.Failure) SessionError { + fn failureError(failure: net.Looper.Failure) SessionError { return switch (failure) { .user => |cause| errors_mod.sessionError(cause), .io => |details| errors_mod.sessionError(details.cause),