GCC 16.2 GNAT.Sockets ghi đè EAGAIN sau khi accept() thất bại trên FreeBSD
Tóm tắt
Trên FreeBSD 15.1 amd64 với GCC/GNAT 16.2.0, có thể tái hiện hành vi trong đó
GNAT.Sockets.Accept_Socket báo BAD_FILE_DESCRIPTOR sau khi lời gọi accept()
ở chế độ không chặn trả về -1 với errno được đặt thành EAGAIN.
Hành vi này được tái hiện bằng một chương trình Ada độc lập chỉ phụ thuộc vào
GNAT.Sockets và các gói thuộc thư viện chuẩn Ada. Dấu vết lời gọi hệ thống cho thấy
nhân FreeBSD trả về EAGAIN. Sau đó runtime của GNAT gọi hàm trợ giúp vô hiệu hóa
SIGPIPE với giá trị trả về -1 biểu thị thất bại. Trên FreeBSD, hàm trợ giúp này gọi
setsockopt(-1, SOL_SOCKET, SO_NOSIGPIPE, ...). Lời gọi đó thất bại với
EBADF, làm ghi đè giá trị EAGAIN từ accept() trước khi GNAT.Sockets
chuyển lỗi thành một ngoại lệ Ada.
Trình tự lời gọi và lỗi quan sát được là:
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
Mẫu gọi Disable_SIGPIPE vô điều kiện tương tự cũng tồn tại sau Syscall_Socket
trong C_Socket. Không có kiểm thử runtime nào trong lần điều tra này thực thi một
lời gọi socket() bị lỗi, vì vậy trường hợp đó được ghi nhận là một phát hiện
từ việc kiểm tra mã nguồn chứ không phải kết quả runtime đã được tái hiện.
Bản vá mã nguồn chỉ gọi Disable_SIGPIPE khi thao tác socket cấp thấp thành công
(R /= Failure).
Môi trường kiểm thử
Hành vi này được tái hiện trong môi trường sau:
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
Cây nguồn của port GCC 16.2 có bật Ada dùng cho công việc này dựa trên tarball phát hành GCC 16.2.0:
SHA256 (gcc-16.2.0.tar.xz) =
e6738e29597f733270731aa90600f37ffdc045079dfc27ec7e8192cc81085c3e
Bộ bản vá cục bộ của port FreeBSD dùng để xây dựng trình biên dịch này cũng đã được rà soát ở mức mã nguồn.
Không bản vá nào trong số đó thay đổi g-socthi.adb, C_Accept, C_Socket,
Disable_SIGPIPE, SO_NOSIGPIPE hoặc socket.c. Mã nguồn upstream GCC 16.2.0
chứa cùng luồng điều khiển như luồng đã quan sát trong lỗi được tái hiện.
Tham chiếu tới mã nguồn upstream:
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
Chương trình tái hiện độc lập
Chương trình sau tạo một socket TCP lắng nghe, chuyển nó sang chế độ không chặn,
rồi gọi Accept_Socket khi không có máy khách nào đang chờ kết nối.
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;
Biên dịch và chạy bằng trình biên dịch GNAT 16.2 đã cài đặt:
gnatmake16 -q accept_eagain.adb
./accept_eagain
Kết quả quan sát được:
GNAT.SOCKETS.SOCKET_ERROR: [9] Bad file descriptor
Dấu vết bên dưới cho thấy accept() đã trả về EAGAIN trên socket lắng nghe hợp
lệ trước khi một lời gọi setsockopt() tiếp theo đổi errno thành EBADF.
Dấu vết lời gọi hệ thống
Chạy chương trình tái hiện dưới truss cho thấy sự thay đổi của errno như sau:
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'
Lời gọi setsockopt() đầu tiên thao tác trên descriptor 3 của socket lắng nghe hợp lệ.
Hai lời gọi tiếp theo cho thấy sự thay đổi của errno. Khi không có kết nối nào
sẵn sàng trên socket lắng nghe không chặn, accept() trả về -1 với
EAGAIN. Ngay sau đó runtime của GNAT gọi setsockopt() với descriptor -1.
Lời gọi hệ thống thứ hai này đổi errno thành EBADF.
Dấu vết ghi nhận EAGAIN từ accept() và chỉ ghi nhận EBADF ở lời gọi
setsockopt() tiếp theo. Giá trị EAGAIN ban đầu bị ghi đè bên trong runtime của GNAT sau khi
accept() trả về.
Luồng xử lý trong mã nguồn GCC 16.2.0
Trong gcc/ada/libgnat/g-socthi.adb, C_Accept lưu kết quả của Syscall_Accept
vào R. Phần triển khai bảo vệ công việc quản lý socket không chặn bằng điều
kiện R /= Failure, nhưng phần thiết lập SIGPIPE tiếp theo lại được thực hiện
vô điều kiện:
if not SOSC.Thread_Blocking_IO
and then R /= Failure
then
...
end if;
Disable_SIGPIPE (R);
return R;
Do đó, khi Syscall_Accept trả về Failure (-1), runtime vẫn thực thi:
Disable_SIGPIPE (-1);
Disable_SIGPIPE được nhập từ gcc/ada/socket.c dưới tên
__gnat_disable_sigpipe. Trên các hệ thống định nghĩa SO_NOSIGPIPE, phần
triển khai của nó gọi:
setsockopt (fd, SOL_SOCKET, SO_NOSIGPIPE, ...)
FreeBSD cung cấp SO_NOSIGPIPE, vì vậy giá trị trả về -1 biểu thị thất bại được truyền tới
setsockopt(-1, ...), khiến errno được đặt thành EBADF.
Phần triển khai cấp cao hơn trong g-socket.adb sau đó thực hiện gần tương đương
như sau:
Res := C_Accept (...);
if Res = Failure then
Raise_Socket_Error (Socket_Errno);
end if;
Tại thời điểm đó, Socket_Errno đọc được EBADF thay vì EAGAIN từ accept().
Trong đường đi đã tái hiện, hậu xử lý được thực hiện trên giá trị Failure mà
accept() trả về, và lời gọi setsockopt() tiếp theo ghi đè errno trước khi
bên gọi đọc giá trị này.
Phát hiện liên quan trong C_Socket
Cùng tệp GCC 16.2.0 đó chứa mẫu sau trong C_Socket:
R := Syscall_Socket (Domain, Typ, Protocol);
...
Disable_SIGPIPE (R);
return R;
Nếu Syscall_Socket trả về Failure, cùng logic đó có thể dẫn tới
Disable_SIGPIPE (-1) trên hệ thống đích định nghĩa SO_NOSIGPIPE.
Lỗi C_Accept đã được tái hiện. Không có kiểm thử runtime nào trong lần điều tra này
thực thi một lời gọi Syscall_Socket bị lỗi, vì vậy trường hợp C_Socket
được ghi nhận là một phát hiện từ việc kiểm tra mã nguồn chứ không phải kết quả
runtime được tái hiện độc lập.
Bản vá
Bản vá chỉ thực hiện hậu xử lý descriptor khi kết quả lời gọi hệ thống không phải là
Failure. Toàn bộ bản vá cho g-socthi.adb của GCC 16.2.0 được đưa bên dưới:
--- 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;
Bản vá áp dụng cùng một điều kiện bảo vệ cho cả C_Accept và C_Socket. Cả hai
phần thay đổi đều áp dụng sạch vào mã nguồn bản phát hành GCC 16.2.0.
Trạng thái xác minh
Bản vá C_Accept đã được xác minh bằng các kiểm tra sau:
[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
Xác minh runtime sau bản vá
Runtime sau bản vá đã được xác minh mà không cần bootstrap đầy đủ GCC.
g-socthi.ali đã cài đặt ghi lại các tùy chọn biên dịch dùng để xây dựng đơn vị runtime.
g-socthi.adb đã cài đặt được sao chép, hai phần thay đổi được áp dụng, rồi riêng
phần thân runtime đó được biên dịch bằng trình biên dịch GNAT 16.2.0 đã cài đặt với
các tùy chọn được ghi lại.
Sau đó thư viện runtime tĩnh đã cài đặt được sao chép vào một thư mục tạm và chỉ
g-socthi.o được thay thế:
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
Trên cài đặt này, gnatlink16 truyền libgnat.a đã cài đặt tới gcc16 bằng
đường dẫn tuyệt đối, vì vậy chỉ thêm một thư mục -L là chưa đủ để thay thế nó.
Đối với tệp thực thi dùng để xác minh, đầu ra của binder được tạo lại và biên dịch,
sau đó lệnh liên kết gcc16 cuối cùng được chạy lại với libgnat.a tạm thời đã vá
thay cho thư viện đã cài đặt.
Với runtime chưa sửa, chương trình tái hiện báo:
GNAT.SOCKETS.SOCKET_ERROR: [9] Bad file descriptor
và dấu vết liên quan là:
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'
Khi liên kết g-socthi.o đã vá vào thư viện runtime mà các thành phần khác vẫn
giữ nguyên, cùng chương trình tái hiện báo:
GNAT.SOCKETS.SOCKET_ERROR: [35] Resource temporarily unavailable
và dấu vết liên quan trở thành:
setsockopt(3,SOL_SOCKET,SO_NOSIGPIPE,...) = 0
accept(3,...) ERR#35 'Resource temporarily unavailable'
Trong dấu vết này, accept() trả về EAGAIN và không có lời gọi
setsockopt(-1, SO_NOSIGPIPE, ...) nào xảy ra sau đó.
Phần thay đổi C_Socket chỉ được xác nhận qua việc kiểm tra mã nguồn; không có kiểm thử runtime nào
trong lần điều tra này thực thi một lời gọi socket() bị lỗi.