Repository navigation
TCP server - socket file descriptor is corrupted when the GC is usedΒ #18263
Description
Describe the bug
The pointer to an open file descriptor is corrupted when the GC is used. When compiling v -gc none -d trace_tcp . there is no such error.
A pointer to a file descriptor is always corrupted at a random location. Sometimes before reading, sometimes after, sometimes it happens that it gets corrupted immediately after accept().
I tested on ubuntu arm64 and on amd64 both systems have the issue.
I was able to repeat the bug consistently by running a load test with 50 threads and 15,000 requests.
Due to the fact that the connection cannot be closed, it hangs and is visible through htop (select process and press 'L') as in the screenshot.
In my code, I save the handle to the handle_store variable, and if uncomment the code on lines 47-50), then the connection can be closed successfully if the handle is corrupted after reading or writing.
With GC disabled, I don't get file descriptor corruption and timeouts, but I do get a memory leak... Although I free the buffer after use.
Expected Behavior
Same behavior as when we compile without GC :v -gc none -d trace_tcp .
Current Behavior
File descriptor is corrupted:
Reproduction Steps
./ab-proxy -c 50 -n 15000 -s 5 http://127.0.0.1:1080 (https://github.com/maurice2k/ab-proxy)
or
wrk -c 50 -t 50 -d 3s http://127.0.0.1:1080 (https://github.com/wg/wrk)
And try searching for error: close(): in the output
Server code:
module main
import net
import time
fn main() {
serve('127.0.0.1:1080', 5 * time.second)!
}
fn serve(addr string, timeout time.Duration) ! {
mut l := net.listen_tcp(.ip, addr) or { return error('Listening on ${addr}: ${err}') }
// l.set_accept_timeout(timeout)
eprintln('Listening on ${addr}')
mut count := 0
for {
mut conn := l.accept() or {
if err.code() == net.err_timed_out_code {
// just skip network timeouts, they are normal
continue
}
eprintln('error: accept(), reason: ${err}; skipping')
continue
}
count += 1
eprintln('${count} ${conn.sock.handle}(${conn.sock.handle}) accept()')
// eprintln('${count} Connected from ${conn.peer_addr()!}')
spawn handle_connection(mut conn, timeout, count)
}
}
fn handle_connection(mut conn net.TcpConn, timeout time.Duration, count int) {
handle_store := conn.sock.handle
defer {
// eprintln('${count} Disconnecting from ${conn.peer_addr() or { net.Addr{} }}')
eprintln('${count} ${conn.sock.handle}(${handle_store}) close()')
conn.close() or {
eprintln('${count} ${conn.sock.handle}(${handle_store}) error: close(): ${err}')
// os.print_c_errno()
// try close stored handle
// net.shutdown(handle_store)
// net.close(handle_store) or {
// eprintln('${count} ${handle_store} error: close() stored handle: ${err}')
// }
}
// eprintln('${count} Disconnected')
}
conn.set_read_timeout(timeout)
conn.set_write_timeout(timeout)
mut buffer := []u8{len: 512}
defer {
unsafe {
buffer.free()
}
}
eprintln('${count} ${conn.sock.handle}(${handle_store}) read()')
l := conn.read(mut buffer) or {
eprintln('${count} ${conn.sock.handle}(${handle_store}) error: read(): ${err}')
return
}
// eprintln('Read from ${conn.peer_addr() or {}}, length: ${l}')
// eprintln(buffer[..l].bytestr())
eprintln('${count} ${conn.sock.handle}(${handle_store}) write()')
resp := 'Test OK'
conn.write_string('HTTP/1.0 200 OK\r\nContent-Length: ${resp.len}\r\n\r\n${resp}\r\n') or {
eprintln('${count} ${conn.sock.handle}(${handle_store}) error: write(): ${err}')
return
}
}
Possible Solution
No response
Additional Information/Context
https://discord.com/channels/592103645835821068/592114487759470596/1111237138688782338
V version
V 0.3.4 c45c36c
Environment details (OS name and version, etc.)
V full version: V 0.3.4 c45c36c
OS: linux, Linux version 5.15.49-linuxkit (root@buildkitsandbox) (gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, GNU ld (GNU Binutils) 2.35.2) #1 SMP PREEMPT Tue Sep 13 07:51:32 UTC 2022
Processor: 6 cpus, 64bit, little endian, aarch64
getwd: /mount
vexe: /opt/vlang/v
vexe mtime: 2023-05-24 19:31:10
vroot: OK, value: /opt/vlang
VMODULES: OK, value: /root/.vmodules
VTMP: OK, value: /tmp/v_0
Git version: git version 2.20.1
Git vroot status: weekly.2023.21-7-gc45c36cc (9 commit(s) behind V master)
.git/config present: true
CC version: cc (Debian 8.3.0-6) 8.3.0
thirdparty/tcc status: thirdparty-linux-aarch64 dd547903
Activity
ghostiam commentedon May 25, 2023
Did a little research:
spawnthen there will be no corruption.read()andwrite(), leave onlyclose(), then file descriptor corruption will still occur.free()in the function generator (with compiler rebuild)main__handle_connection_thread_wrapper, but nothing changed.I think the GC can't keep track of what is being launched via
pthread...Old updates
UPD:
If replace
memdupin the generated C code withmemdup_uncollectablein thereturnmethodaccept_only(), then the error disappears (but, yes, a memory leak is expected)._result_net__TcpConn_ptr net__TcpListener_accept_only(net__TcpListener* l) { ... } _result_net__TcpConn_ptr _t4; - _result_ok(&(net__TcpConn*[]) { ((net__TcpConn*)memdup(&(net__TcpConn){.sock = (net__TcpSocket){.Socket = (net__Socket){.handle = 0,},},.handle = new_handle,.write_deadline = (time__Time){.year = 0,.month = 0,.day = 0,.hour = 0,.minute = 0,.second = 0,.microsecond = 0,._v_unix = 0,.is_local = 0,},.read_deadline = (time__Time){.year = 0,.month = 0,.day = 0,.hour = 0,.minute = 0,.second = 0,.microsecond = 0,._v_unix = 0,.is_local = 0,},.read_timeout = _const_net__tcp_default_read_timeout,.write_timeout = _const_net__tcp_default_write_timeout,.is_blocking = 0,}, sizeof(net__TcpConn))) }, (_result*)(&_t4), sizeof(net__TcpConn*)); + _result_ok(&(net__TcpConn*[]) { ((net__TcpConn*)memdup_uncollectable(&(net__TcpConn){.sock = (net__TcpSocket){.Socket = (net__Socket){.handle = 0,},},.handle = new_handle,.write_deadline = (time__Time){.year = 0,.month = 0,.day = 0,.hour = 0,.minute = 0,.second = 0,.microsecond = 0,._v_unix = 0,.is_local = 0,},.read_deadline = (time__Time){.year = 0,.month = 0,.day = 0,.hour = 0,.minute = 0,.second = 0,.microsecond = 0,._v_unix = 0,.is_local = 0,},.read_timeout = _const_net__tcp_default_read_timeout,.write_timeout = _const_net__tcp_default_write_timeout,.is_blocking = 0,}, sizeof(net__TcpConn))) }, (_result*)(&_t4), sizeof(net__TcpConn*)); return _t4; }UPD2: In UPD3 the way is easier.
I was able to solve the problem by corrupting the FD with only `V` (but yes, there was a memory leak). To do this, I had to remove `mut` from all `TcpConn` methods and replace
pub fn (mut l TcpListener) accept() !&TcpConn {pub fn (mut l TcpListener) accept_only() !&TcpConn {fn (mut s TcpSocket) close() ! {with
pub fn (mut l TcpListener) accept() !TcpConn {pub fn (mut l TcpListener) accept_only() !TcpConn {fn (s TcpSocket) close() ! {UPD3:
It turned out that it is possible to solve the problem easier and with less memory leak.
Apparently, it is impossible to transfer reference to the coroutine, the GC is clearly not ready for it.
Diffs
UPD 4:
And an even easier way... Wrap conn in a structure...
We get a memory leak, but got rid of the corrupting to the FD.Full code
UPD5:
upd:And as it turns out, the most significant difference is in how I pass
TcpConn...No bug
Has bug
The difference in C code is how the data is passed to the function.
mut conn := c.connNo bug:
conn = HEAP(net__TcpConn, (c->conn));+
handle_store = (*(conn)).sock.Socket.handle;Has bug
conn = c->conn;or
conn = &c->conn;+
handle_store = conn->sock.Socket.handle;UPD6:
The easiest fix? But once I caught file descriptor corruption even with this fix...
count += 1 eprintln('${count} ${conn.sock.handle}(${conn.sock.handle}) accept()') // eprintln('${count} Connected from ${conn.peer_addr()!}') - spawn handle_connection(mut conn, timeout, count) + spawn handle_connection(conn, timeout, count) -fn handle_connection(conn net.TcpConn, timeout time.Duration, count int) { +fn handle_connection(c net.TcpConn, timeout time.Duration, count int) { + mut conn := c handle_store := conn.sock.handlekbkpbot commentedon Jun 6, 2023
It seems there are some bug in the thread usage.
I create a more simpler test case :
It will produce a lot of error messages.
pithing commentedon Jul 12, 2023
change:
It seems to be a bug derived from a function reference passed before.
tcp server - socket file descriptor is corrupted when the gc... (fixes β¦
tcp server - socket file descriptor is corrupted when the gc... (fixes β¦
checker: fix TCP server socket file descriptor corruption when using β¦