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.
Related C_Socket finding
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.