ひさしぶりに 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]