天泣記

2014-04-09 (Wed)

#1

ひさしぶりに 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 をセーブしないのはなんでかなぁ。

2014-04-21 (Mon)

#1

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)

2014-04-24 (Thu)

#1

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]


田中哲