GCC 16.2 GNAT.Sockets overwrites EAGAIN after accept() fails on FreeBSD

Summary

On FreeBSD 15.1 amd64 with GCC/GNAT 16.2.0, GNAT.Sockets.Accept_Socket reports BAD_FILE_DESCRIPTOR after a non-blocking accept() call returns -1 with errno set to EAGAIN.

The behavior was reproduced with a standalone Ada program that depends only on GNAT.Sockets and packages from the Ada standard library. A syscall trace shows that the FreeBSD kernel returns EAGAIN. The GNAT runtime then calls its SIGPIPE-disabling helper with the failure return value -1. On FreeBSD, that helper calls setsockopt(-1, SOL_SOCKET, SO_NOSIGPIPE, ...). That call fails with EBADF, overwriting the EAGAIN set by accept() before GNAT.Sockets translates the error into an Ada exception.

The observed call and error sequence is:

FreeBSD kernel
  accept(listener) -> -1, errno = EAGAIN

GNAT runtime
  GNAT.Sockets.Thin.C_Accept
  -> Disable_SIGPIPE(-1)
  -> setsockopt(-1, SO_NOSIGPIPE, ...) -> -1, errno = EBADF

GNAT.Sockets
  -> Socket_Error: [9] Bad file descriptor

The same unconditional Disable_SIGPIPE pattern also appears after Syscall_Socket in C_Socket. No runtime test in this investigation exercised a failing socket() call, so that case is documented as a source-audit finding rather than a reproduced runtime result.

The source patch calls Disable_SIGPIPE only when the underlying socket operation succeeds (R /= Failure).

Test environment

The behavior was reproduced in the following environment:

OS:       FreeBSD 15.1-RELEASE-p3
Kernel:   FreeBSD 15.1, __FreeBSD_version 1501000
Arch:     amd64
GNAT:     GNATMAKE 16.2.0
GCC src:  GCC 16.2.0 release

The Ada-enabled GCC 16.2 port tree used for this investigation is based on the GCC 16.2.0 release tarball:

SHA256 (gcc-16.2.0.tar.xz) =
e6738e29597f733270731aa90600f37ffdc045079dfc27ec7e8192cc81085c3e

The local FreeBSD port patch set used to build this compiler was also audited. None of those patches touches g-socthi.adb, C_Accept, C_Socket, Disable_SIGPIPE, SO_NOSIGPIPE, or socket.c. The upstream GCC 16.2.0 sources contain the same control flow observed in the reproduced failure.

Upstream source references:

https://github.com/gcc-mirror/gcc/blob/releases/gcc-16.2.0/gcc/ada/libgnat/g-socthi.adb
https://github.com/gcc-mirror/gcc/blob/releases/gcc-16.2.0/gcc/ada/socket.c
https://github.com/gcc-mirror/gcc/blob/releases/gcc-16.2.0/gcc/ada/libgnat/g-socket.adb

Standalone reproducer

The following program creates a listening TCP socket, switches it to non-blocking mode, and calls Accept_Socket when no client is waiting.

with Ada.Exceptions;
with Ada.Text_IO;
with GNAT.Sockets;

procedure Accept_Eagain is
   use Ada.Exceptions;
   use Ada.Text_IO;
   use GNAT.Sockets;

   Server : Socket_Type;
   Client : Socket_Type;
   Peer   : Sock_Addr_Type;
   Req    : Request_Type := (Name => Non_Blocking_IO, Enabled => True);
begin
   Initialize;
   Create_Socket (Server, Family_Inet, Socket_Stream);
   Bind_Socket
     (Server,
      (Family => Family_Inet,
       Addr   => Inet_Addr ("127.0.0.1"),
       Port   => Any_Port));
   Listen_Socket (Server);
   Control_Socket (Server, Req);

   Accept_Socket (Server, Client, Peer);
   Put_Line ("unexpected accept success");
   Close_Socket (Client);
   Close_Socket (Server);
exception
   when E : Socket_Error =>
      Put_Line (Exception_Name (E) & ": " & Exception_Message (E));
      Close_Socket (Server);
end Accept_Eagain;

Build and run it with the installed GNAT 16.2 compiler:

gnatmake16 -q accept_eagain.adb
./accept_eagain

Observed result:

GNAT.SOCKETS.SOCKET_ERROR: [9] Bad file descriptor

The trace below shows that accept() returned EAGAIN on the valid listening socket before a subsequent setsockopt() changed errno to EBADF.

Syscall trace

Running the reproducer under truss shows the following errno transition:

setsockopt(3,SOL_SOCKET,SO_NOSIGPIPE,...) = 0
accept(3,...)                              ERR#35 'Resource temporarily unavailable'
setsockopt(-1,SOL_SOCKET,SO_NOSIGPIPE,...) ERR#9 'Bad file descriptor'

The first setsockopt() call operates on the valid listening descriptor 3.

The next two calls show how errno changes. accept() returns -1 with EAGAIN when no connection is available on the non-blocking listening socket. Immediately afterward the GNAT runtime calls setsockopt() with descriptor -1. That second syscall changes errno to EBADF.

The trace records EAGAIN from accept() and EBADF only from the subsequent setsockopt() call. The original EAGAIN is therefore overwritten inside the GNAT runtime after accept() returns.

Source path in GCC 16.2.0

In gcc/ada/libgnat/g-socthi.adb, C_Accept stores the result of Syscall_Accept in R. The implementation guards the non-blocking socket bookkeeping with R /= Failure, but performs the subsequent SIGPIPE setup unconditionally:

if not SOSC.Thread_Blocking_IO
  and then R /= Failure
then
   ...
end if;

Disable_SIGPIPE (R);
return R;

Therefore, when Syscall_Accept returns Failure (-1), the runtime still executes:

Disable_SIGPIPE (-1);

Disable_SIGPIPE is imported from gcc/ada/socket.c as __gnat_disable_sigpipe. On systems that define SO_NOSIGPIPE, its implementation calls:

setsockopt (fd, SOL_SOCKET, SO_NOSIGPIPE, ...)

FreeBSD provides SO_NOSIGPIPE, so the -1 failure result is passed to setsockopt(-1, ...), which sets errno to EBADF.

The higher-level implementation in g-socket.adb then does approximately:

Res := C_Accept (...);

if Res = Failure then
   Raise_Socket_Error (Socket_Errno);
end if;

At that point, Socket_Errno reads EBADF rather than the EAGAIN from accept().

In the reproduced path, post-processing is performed on the Failure value returned by accept(), and the subsequent setsockopt() call replaces errno before the caller reads it.

The same GCC 16.2.0 file contains the following pattern in C_Socket:

R := Syscall_Socket (Domain, Typ, Protocol);
...
Disable_SIGPIPE (R);
return R;

If Syscall_Socket returns Failure, the same logic can reach Disable_SIGPIPE (-1) on targets that define SO_NOSIGPIPE.

The C_Accept failure was reproduced. No runtime test in this investigation exercised a failing Syscall_Socket call, so the C_Socket case is recorded as a related source-audit finding rather than an independently reproduced runtime result.

Patch

The patch performs descriptor post-processing only when the syscall result is not Failure. The complete patch against GCC 16.2.0 g-socthi.adb is included below:

--- gcc/ada/libgnat/g-socthi.adb.orig
+++ gcc/ada/libgnat/g-socthi.adb
@@ -187,7 +187,10 @@
          Discard := C_Ioctl (R, SOSC.FIONBIO, Val'Access);
       end if;

-      Disable_SIGPIPE (R);
+      if R /= Failure then
+         Disable_SIGPIPE (R);
+      end if;
+
       return R;
    end C_Accept;

@@ -533,7 +536,10 @@
          Discard := C_Ioctl (R, SOSC.FIONBIO, Val'Access);
          Set_Non_Blocking_Socket (R, False);
       end if;
-      Disable_SIGPIPE (R);
+      if R /= Failure then
+         Disable_SIGPIPE (R);
+      end if;
+
       return R;
    end C_Socket;

The patch applies the same guard to both C_Accept and C_Socket. Both hunks apply cleanly to the GCC 16.2.0 release source.

Validation status

The C_Accept patch was validated with the following checks:

[PASS] standalone Ada reproducer using only GNAT.Sockets and the Ada standard library
[PASS] installed GNAT 16.2.0 reproduces BAD_FILE_DESCRIPTOR
[PASS] baseline truss records accept() returning EAGAIN first
[PASS] baseline truss records setsockopt(-1, SO_NOSIGPIPE) returning EBADF
[PASS] upstream GCC 16.2.0 source contains the matching control flow
[PASS] local FreeBSD GCC port patches do not introduce or modify this path
[PASS] both C_Accept/C_Socket patch hunks apply without rejects
[PASS] patched g-socthi.adb compiles as a single GNAT runtime unit
[PASS] temporary libgnat.a with only g-socthi.o replaced links successfully
[PASS] patched reproducer reports EAGAIN instead of EBADF
[PASS] patched truss contains no setsockopt(-1, SO_NOSIGPIPE) call

Patched runtime validation

The patched runtime was validated without a full GCC bootstrap. The installed g-socthi.ali records the compiler options used to build the runtime unit. The installed g-socthi.adb was copied, the two-hunk patch was applied, and that single runtime body was compiled with the installed GNAT 16.2.0 compiler using those recorded options.

The installed static runtime archive was then copied to a temporary directory, and only g-socthi.o was replaced:

cp /usr/local/lib/gcc16/gcc/x86_64-portbld-freebsd15.1/16.2.0/adalib/libgnat.a \
   /tmp/libgnat.a
ar r /tmp/libgnat.a g-socthi.o
ranlib /tmp/libgnat.a

On this installation, gnatlink16 passes the installed libgnat.a to gcc16 by an absolute pathname, so adding a -L directory is not sufficient to override it. For the validation binary, the binder output was regenerated and compiled. The final gcc16 link command was then repeated with the temporary patched libgnat.a substituted for the installed archive.

With the unmodified runtime, the reproducer reports:

GNAT.SOCKETS.SOCKET_ERROR: [9] Bad file descriptor

and the relevant trace is:

setsockopt(3,SOL_SOCKET,SO_NOSIGPIPE,...) = 0
accept(3,...)                              ERR#35 'Resource temporarily unavailable'
setsockopt(-1,SOL_SOCKET,SO_NOSIGPIPE,...) ERR#9 'Bad file descriptor'

With the patched g-socthi.o linked into the otherwise unchanged runtime archive, the same reproducer reports:

GNAT.SOCKETS.SOCKET_ERROR: [35] Resource temporarily unavailable

and the relevant trace becomes:

setsockopt(3,SOL_SOCKET,SO_NOSIGPIPE,...) = 0
accept(3,...)                              ERR#35 'Resource temporarily unavailable'

In this trace, accept() returns EAGAIN, and no subsequent setsockopt(-1, SO_NOSIGPIPE, ...) call occurs.

The C_Socket hunk is supported only by source audit; no runtime test in this investigation exercised a failing socket() call.