ひさしぶりに Ruby の GC bug をひとつ潰した。
[ruby-dev:48098] [Bug #9717] で、callee save register を mark しそびれるというもの。
結局、gc.c の mark_current_machine_context の問題だったと思う。mark_current_machine_context は保守的 GC のために、スタックとレジスタを mark する関数なのだが、C で記述されているので正しく動作させるのはなかなか難しいところである。
gc.s から mark_current_machine_context を取り出すと以下のようになっていた。(Debian GNU/Linux testing (jessie) の gcc 4.8.2-16 で、-O0 を使っている)
mark_current_machine_context: .LFB161: .loc 6 3499 0 .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 pushq %r15 pushq %r14 pushq %r13 pushq %r12 pushq %rbx 外から届いたrbxをスタックフレームにセーブ subq $248, %rsp .cfi_offset 15, -24 .cfi_offset 14, -32 .cfi_offset 13, -40 .cfi_offset 12, -48 .cfi_offset 3, -56 movq %rdi, -280(%rbp) movq %rsi, -288(%rbp) .loc 6 3508 0 leaq -256(%rbp), %rax leaq -48(%rbp), %rbx rbx を書き換えてしまう (ここで外から届いた rbx はスタックフレームにセーブされたものしか存在しなくなる) movq %rbx, (%rax) leaq .L738(%rip), %rdx movq %rdx, 8(%rax) movq %rsp, 16(%rax) jmp .L739 .L738: leaq 48(%rbp), %rbp .L739: .loc 6 3510 0 ここから __builtin_setjmp か? rbx を扱っていない? movq -288(%rbp), %rax movq 488(%rax), %rax movq %rax, -272(%rbp) movq -288(%rbp), %rax movq 480(%rax), %rax movq %rax, -264(%rbp) .loc 6 3512 0 leaq -256(%rbp), %rcx movq -280(%rbp), %rax movl $25, %edx movq %rcx, %rsi movq %rax, %rdi call mark_locations_array mark_locations_array を呼んで jmpbuf を mark .loc 6 3514 0 movq -264(%rbp), %rdx movq -272(%rbp), %rcx movq -280(%rbp), %rax movq %rcx, %rsi movq %rax, %rdi call gc_mark_locations gc_mark_locations を呼ぶが、この関数自体のスタックフレームは範囲の外 SET_STACK_END を外で呼んでいるからこの関数のスタックフレーム境界になっていない そのため、セーブした rbx を mark しない .loc 6 3522 0 addq $248, %rsp popq %rbx popq %r12 popq %r13 popq %r14 popq %r15 popq %rbp .cfi_def_cfa 7, 8 ret .L740: .cfi_endproc .LFE161: .size mark_current_machine_context, .-mark_current_machine_context
ちなみに、callee save な rbx にオブジェクトが入るのは parse.y の new_op_assign_gen で、
asgn->nd_value = NEW_CALL(gettable(vid), op, NEW_LIST(rhs));
という行が以下のようにっていた。
.loc 6 9598 0 movq -48(%rbp), %rax movq %rax, -32(%rbp) .loc 6 9599 0 movq -64(%rbp), %rdx movq -40(%rbp), %rax movl $0, %r8d movl $1, %ecx movl $40, %esi movq %rax, %rdi call node_newnode NEW_LIST(rhs) の呼び出し movq %rax, %rbx 返り値を rbx にコピー movq -24(%rbp), %rdx movq -40(%rbp), %rax NEW_LIST(rhs) の値が入っていた rax を書き潰す (ここで NEW_LIST(rhs) の値は rbx にしか存在しなくなる) movq %rdx, %rsi movq %rax, %rdi call gettable_gen gettable を呼ぶ。ここで rbx は callee save なのでレジスタに入ったまま つまり、この関数のスタックフレームには NEW_LIST(rhs) の値は記録されない movq %rax, %rdx movq -56(%rbp), %rcx movq -40(%rbp), %rax movq %rbx, %r8 rbx を NEW_CALL(...) の引数に使う movl $35, %esi movq %rax, %rdi call node_newnode movq -32(%rbp), %rdx movq %rax, 24(%rdx)
rbx に入っている値は NEW_LIST(rhs) の返り値で NODE * つまり VALUE なので、new_op_assign_gen 自体は問題ではない。
new_op_assign_gen から mark_current_machine_context までの全部はたどらなかったのだが、きっと rbx がそのまま mark_current_machine_context まで届いて、mark しそびれるのだろう。
というわけで、r45542 で SET_STACK_END を mark_current_machine_context の中で呼んでそれ自身のスタックフレームの終端を検出し、そこまで mark するようにした。
というか、昔 SET_STACK_END を呼ぶようにしておいたのだが、r40703 で (ko1 により) 消されたので、復活させた。
しかし、改めて考えてみると、callee save register が mark_current_machine_context でも記録されずに gc_mark_locations までたどり着くと、mark されないかもしれない? (jmpbuf に rbx が書き込まれない場合)
__builtin_setjmp が jmpbuf に rbx をセーブしないのはなんでかなぁ。
OpenSSH 6.5 に ProxyUseFdpass というオプションができていたのを知ったので、試してみた。
% cat test_ProxyUseFdpass #!/usr/bin/ruby # Usage: # ssh -o 'ProxyUseFdpass yes' -o 'ProxyCommand ./test_ProxyUseFdpass %h %p' server.domain require 'socket' host = ARGV[0] port = ARGV[1] s = TCPSocket.open(host, port) stdout = Socket.for_fd(STDOUT.fileno) ancdata = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, s.fileno) stdout.sendmsg("\0", 0, nil, ancdata) % ssh -o 'ProxyUseFdpass yes' -o 'ProxyCommand ./test_ProxyUseFdpass %h %p' localhost Linux jet 3.13-1-amd64 #1 SMP Debian 3.13.7-1 (2014-03-25) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. You have new mail. Last login: Mon Apr 21 18:39:42 2014 from localhost %
動いているようである。起動した ./test_ProxyUseFdpass は接続したソケットを stdout 経由で親 (ssh client) に pass して、すぐに終わってしまえる。その後の ssh protocol なデータ転送は ssh client 自身が行う。先頭でごにょごにょした後 ssh protocol になるという形式に使うものだろうな。
なお、UNIXSocket#send_io でもできる。(偶然、必要な形式に合致している感じ)
% cat test_ProxyUseFdpass2 #!/usr/bin/ruby require 'socket' host = ARGV[0] port = ARGV[1] s = TCPSocket.open(host, port) stdout = UNIXSocket.for_fd(STDOUT.fileno) stdout.send_io(s)
Linux で、RLIMIT_NOFILE を 0 にして poll を呼び出すと EINVAL になるのはなぜ?
% uname -mrsv Linux 3.13-1-amd64 #1 SMP Debian 3.13.10-1 (2014-04-15) x86_64 % cat t.c #include <stdlib.h> #include <stdio.h> #include <sys/time.h> #include <sys/resource.h> #include <poll.h> int main(int argc, char *argv[]) { int ret; struct rlimit rlim; struct pollfd fds[1]; rlim.rlim_cur = 0; rlim.rlim_max = 0; ret = setrlimit(RLIMIT_NOFILE, &rlim); if (ret == -1) { perror("setrlimit"); exit(EXIT_FAILURE); } fds[0].fd = 0; fds[0].events = POLLIN; fds[0].revents = 0; ret = poll(fds, 1, 0); if (ret == -1) { perror("poll"); exit(EXIT_FAILURE); } return 0; } % gcc -Wall t.c % strace ./a.out execve("./a.out", ["./a.out"], [/* 54 vars */]) = 0 brk(0) = 0x1434000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6b6a3f3000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=105979, ...}) = 0 mmap(NULL, 105979, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f6b6a3d9000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\34\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1729984, ...}) = 0 mmap(NULL, 3836480, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f6b69e2d000 mprotect(0x7f6b69fcd000, 2093056, PROT_NONE) = 0 mmap(0x7f6b6a1cc000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19f000) = 0x7f6b6a1cc000 mmap(0x7f6b6a1d2000, 14912, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f6b6a1d2000 close(3) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6b6a3d8000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6b6a3d7000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6b6a3d6000 arch_prctl(ARCH_SET_FS, 0x7f6b6a3d7700) = 0 mprotect(0x7f6b6a1cc000, 16384, PROT_READ) = 0 mprotect(0x7f6b6a3f5000, 4096, PROT_READ) = 0 munmap(0x7f6b6a3d9000, 105979) = 0 setrlimit(RLIMIT_NOFILE, {rlim_cur=0, rlim_max=0}) = 0 poll([{fd=0, events=POLLIN}], 1, 0) = -1 EINVAL (Invalid argument) dup(2) = -1 EMFILE (Too many open files) write(2, "poll: Invalid argument\n", 23poll: Invalid argument ) = 23 exit_group(1) = ?
Debian GNU/Linux testing (jessie) で試した。
む、Debian GNU/Linux 6.0.9 (squeeze) だとならないな。
% uname -mrsv Linux 2.6.18-6-xen-686 #1 SMP Thu Nov 5 19:54:42 UTC 2009 i686
[追記] こさきさんに [PATCH] enforce RLIMIT_NOFILE in poll() を教えてもらった。
教えてもらったことや、自分で読んで考えたことをまとめると、
ということらしい。
[latest]