天泣記

2014-10-19 (Sun)

#1

GitHub: milkode: Use Open3.pipeline to avoid shell.

2014-10-10 (Fri)

#3

GitHub: milkode: grenfiletest.rb:12:in `match': invalid byte sequence in UTF-8 (ArgumentError)

#2

GitHub: rubygems: Don't define MirrorCommand if already defined.

#1 gem のサイズの集計

gem mirror が終わったので、手元にはたくさん gem がある。具体的には 477367個、137GB

とりあえずどんなサイズの gem があるだろうかということでグラフにしてみた。

gem-size.png

10KB くらいが多い。

2014-10-09 (Thu)

#1 gem のバージョンのグラフ

ふと gem mirror をしたところ、spec.4.8 というファイルが目についた。

一瞬 gemspec が入っているのかな、と思ったが、中身を覗いてみると名前、バージョン、プラットフォームだけだった。

% ruby -rpp -e 'pp Marshal.load(File.binread("specs.4.8"))'
[["_", Gem::Version.new("1.0"), "ruby"],
 ["_", Gem::Version.new("1.1"), "ruby"],
 ["_", Gem::Version.new("1.2"), "ruby"],
 ["-", Gem::Version.new("1"), "ruby"],
 ["0mq", Gem::Version.new("0.1.0"), "ruby"],
 ["0mq", Gem::Version.new("0.1.1"), "ruby"],
 ["0mq", Gem::Version.new("0.1.2"), "ruby"],
 ["0mq", Gem::Version.new("0.2.0"), "ruby"],
 ["0mq", Gem::Version.new("0.2.1"), "ruby"],
 ["0mq", Gem::Version.new("0.3.0"), "ruby"],
...

これで何かできるだろうかと思って、とりあえず最初の数の分布を調べてみた。(同じ名前のパッケージはいちばん大きなバージョン以外は無視)

% ruby -rpp -e '
Marshal.load(File.binread("specs.4.8")).
group_by {|ary| ary[0] }.
each {|k, vs|
  v = vs.map {|ary| ary[1] }.max
  puts v.to_s[/\d+/].to_i
}
'|sort -n |uniq -c
  68581 0
  15280 1
   2943 2
    945 3
    332 4
     82 5
     46 6
     27 7
     13 8
     26 9
     17 10
     12 11
      3 12
      7 13
      7 14
      2 15
      2 19
      2 20
      2 21
      1 22
      1 26
      1 28
      2 30
      1 35
      5 42
      1 99
      1 100
      1 111
      1 969
      1 971
      1 1987
      2 2004
      1 2006
      2 2008
     22 2009
      6 2010
     19 2011
     15 2012
     26 2013
     35 2014
      1 3000
      1 7159
      2 9000
      1 9001
      1 50000
      1 20120125
      1 20120701
      1 20121026
      1 2009022403
      1 2010072900

バージョン0 が多いというのはたしかにそうだな。

グラフを描いてみよう。

ふつうに描くと Y軸、X軸にはりついてしまうのだが、さりとて 0 があるから対数グラフには向かないしどうしたものか、と思いつつとりあえず ggplot2 で対数グラフにしたら 0 は印がはみ出る感じに描かれるのだな。

top-version.png

両対数グラフで、バージョン10あたりまでは直線的に落ちていく感じ。

2014-09-29 (Mon)

#1

shellshock で有名になった、シェル関数を子孫のプロセスに export するという bash の機能は、どうも ksh88 に由来するような気がする。

ksh88 のマニュアル には typeset で、-f (関数) と -x (export) を組み合わせられると記述されている。

ksh88 はあまり生き残っていないのだが、AIX 7.1 にまだあったので試してみた。試行錯誤の結果、以下のようにすると動くことが確認できた。

-bash-4.2$ ksh
$ echo bar > bin/foo       # foo という名前のファイルで shell procedure を作る。中身は bar を呼び出す
$ chmod 755 bin/foo        # shell procedure は実行可能でないといけないので実行可能にする
$ which foo                # which で foo が見つかる
/home/akr/bin/foo
$ foo                      # ここで foo を呼び出すと bar が見つからないというエラー
foo: bar:  not found                                                                                  .
$ bar () { echo baz; }     # bar を関数として定義する
$ foo                      # ここで foo を呼び出しても bar は見つからない
foo: bar:  not found                                                                                  .
$ typeset -fx bar          # bar を export するように設定する
$ foo                      # foo を呼び出すと bar が呼び出される
baz
$

試してわかったのだが、これは bash のように環境変数を介する機能ではないようだ。例えば、ksh -c bar としたり、foo の先頭に #!/bin/ksh をつけると bar の定義は伝わらない。

truss すると以下のようになる。

-bash-4.2$ truss -f ksh -c 'bar () { echo baz; }; typeset -fx bar; foo'
...
12124234: 51511337: execve("/home/akr/bin/foo", 0x200119B8, 0x20011EB8) Err#8  ENOEXEC
12124234: 51511337: kopen("/home/akr/bin/foo", O_RDONLY)        = 3
...
baz
12124234: 51511337: kwrite(1, " b a z\n", 4)            = 4
...

つまり、ksh は foo を execve しようとして失敗し、しょうがないので foo を自分で読んで処理して (bar を呼んで) baz を出力する、という流れである。

execve してないので呼び出し元の情報が利用可能で、typeset により export とマークされている bar を見つけて使うのであろう。

というわけで、もし ksh88 が使われていても、(環境変数は関係ないので) shellshock のような話にはならないだろう。

ちなみに、ksh93 では動かない。(ksh93 のマニュアルでも、typeset で -f と -x を組み合わせた時の動作の記述は消えている。)

-bash-4.2$ ksh93 -c 'bar () { echo baz; }; typeset -fx bar; foo'
foo: line 1: bar: not found

なお、pdksh のマニュアル には typeset -fx は効果がないことが書いてある。

2014-09-19 (Fri)

#1

本日のまつもとさんの発表 (基調講演) をネタになるせさんと議論した結果、非常に簡単化した静的解析なら簡単に実装できることがわかったので実装してみた。

% cat static-check.rb
class C
  def m
    xxxx
  end
end

def_methods = {}
call_methods = {}
ObjectSpace.each_object {|o|
  methods = []
  methods.concat o.instance_methods.map {|msym| o.instance_method(msym) } if Module === o
  methods.concat o.methods.map {|msym| o.method(msym) }

  methods.each {|m|
    def_methods[m.name] = true
    asm = RubyVM::InstructionSequence.disasm(m)
    next if !asm
    asm.scan(/mid:([a-zA-Z0-9]+)/) {
      call_methods[$1] = true
    }
  }
}

a = call_methods.keys - def_methods.keys
if !a.empty?
  p a.sort
end
% ruby --disable-gems static-check.rb
["synchronize", "xxxx"]

これにより、定義されていないけれど呼び出しが存在する、xxxx というメソッドを発見できている。いままでは実行が xxxx というメソッド呼び出しに到達しないかぎりこのようなものを発見することはできなかったのだが、ここではそこを実行せずに発見している。

話は簡単で、すべてのメソッド定義のメソッド名をリストアップし、すべてのメソッド呼び出しのメソッド名をリストアップし、後者だけに存在するメソッド名を表示しているだけである。(実装が不完全であるのはわかっているので気にしていない)

飲み会の中で実装したのだが、ちょうどそこにいたまつもとさんやささださんなどに見せていろいろな話が出た。

単純な typo の発見ならこれでもかなり効くだろうとか、定期的に実行して名前の増減を監視すればどうかとか。いつやるかが問題だけど、Rails みたいなのはリクエストを受付始める前でやればいいとか、require の終わりでやればいいとか。あと、InstructionSequence#to_a が使えるだろうとか。

2014-09-10 (Wed)

#2

しかし、いろいろ考えると、やはり方向としては vfork より posix_spawn のほうがいいなぁ、と思う。自分で vfork を使うと、子プロセスでのメモリの書き変えをスタックだけに完璧に制限できるかどうか確信が持てない。確信を持てないから危ない感じの場合 (私の判断としては setuid 可能な場合) は使わないようになっちゃうし。

#1

vfork したとき、親プロセスの他のスレッド (vfork を呼び出した以外のスレッド) は止まるのだろうか。(あと、そもそもメモリは共有されるだろうか)

% cat t.c
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>

static volatile char *global = "vfork doesn't share memory.";

void printn(int prefix, int n)
{
  char buf[4];
  buf[0] = prefix;
  buf[1] = '0' + n;
  buf[2] = '\n';
  buf[3] = '\0';

  write(1, buf, sizeof(buf));
}

void *thread_func(void *arg)
{
  int i;
  for (i = 0; i < 6; i++) {
    printn('t', i);
    sleep(1);
  }
  return NULL;
}

int main(int argc, char *argv[])
{
  int ret;
  pthread_t th;
  pid_t pid;
  int status;
  int j;

  ret = pthread_create(&th, NULL, thread_func, NULL);
  if (ret != 0) { errno = ret; perror("pthread_create"); exit(EXIT_FAILURE); }

  pid = vfork();
  if (pid == -1) { perror("vfork"); exit(EXIT_FAILURE); }

  if (pid == 0) {
    /* child */
    int i;
    global = "vfork share memory.";
    for (i = 0; i < 3; i++) {
      sleep(1);
      printn('c', i);
    }

    _exit(0);
  }

  for (j = 0; j < 3; j++) {
    printn('p', j);
    sleep(1);
  }

  pid = wait(&status);
  if (pid == -1) { perror("wait"); exit(EXIT_FAILURE); }

  ret = pthread_join(th, NULL);
  if (ret != 0) { errno = ret; perror("pthread_join"); exit(EXIT_FAILURE); }

  printf("%s\n", global);

  return EXIT_SUCCESS;
}
% gcc -Wall t.c -lpthread
% ./a.out
t0
c0
t1
t2
c1
t3
c2
p0
t4
p1
t5
p2
vfork share memory.
% uname -srvm
Linux 3.14-2-amd64 #1 SMP Debian 3.14.15-2 (2014-08-09) x86_64

子プロセスで書き換えたグローバル変数が親プロセスに反映されているので親プロセスと子プロセスでメモリは共有されている。共有しているので親プロセスの vfork を呼び出したスレッド p[0-2] と子プロセス c[0-2] は当然同時には実行されなくて、子プロセスが終わった後に親プロセスが動いている。親プロセスで動いている他のスレッド t[0-5] は vfork 中にも動いている。

この調子でいくつか調べてみた。

メモリを共有し、親プロセスの他のスレッドが動作しつづける:

メモリを共有し、親プロセス全体が停止する:

メモリを共有せず、親プロセス全体が停止する:

メモリを共有せず、親プロセスは動作し続ける (fork 相当の動作):

2014-09-08 (Mon)

#1

vfork した状態で setuid すると、本当に危険だろうか。ちょっと試してみよう。

% t.c
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

int main(int argc, char *argv[])
{
  pid_t pid, pid2;
  uid_t ruid, euid, suid;
  int ret;
  int status;

  fprintf(stderr, "parent pid=%ld\n", (long)getpid());

  ret = getresuid(&ruid, &euid, &suid);
  if (ret == -1) {
    perror("getresuid");
    exit(EXIT_FAILURE);
  }
  fprintf(stderr, "parent ruid=%ld euid=%ld suid=%ld\n", (long)ruid, (long)euid, (long)suid);

  pid = vfork();
  if (pid == -1) {
    perror("vfork");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {
    /* child */

    fprintf(stderr, "child pid=%ld\n", (long)getpid());

    ret = setuid(1000);
    if (ret == -1) {
      perror("setuid");
      exit(EXIT_FAILURE);
    }

    ret = getresuid(&ruid, &euid, &suid);
    if (ret == -1) {
      perror("getresuid");
      exit(EXIT_FAILURE);
    }
    fprintf(stderr, "child ruid=%ld euid=%ld suid=%ld\n", (long)ruid, (long)euid, (long)suid);

    sleep(10);
    _exit(EXIT_SUCCESS);
  }

  /* parent */
  fprintf(stderr, "parent\n");

  pid2 = waitpid(pid, &status, WUNTRACED|WCONTINUED);
  if (pid2 == -1) {
    perror("waitpid");
    exit(EXIT_FAILURE);
  }
  if (WIFEXITED(status))
    fprintf(stderr, "pid %ld exit %ld\n", (long)pid2, WEXITSTATUS(status));
  else if (WIFSIGNALED(status))
    fprintf(stderr, "pid %ld signal %ld\n", (long)pid2, WTERMSIG(status));
  else
    fprintf(stderr, "pid %ld unexpected %ld\n", (long)pid2, (long)status);

  return EXIT_SUCCESS;
}
% gcc t.c
% sudo ./a.out
parent pid=8623
parent ruid=0 euid=0 suid=0
child pid=8624
child ruid=1000 euid=1000 suid=1000

で、他の端末から gdb で attach してみる。

(gdb) attach 8624
Attaching to program: /tmp/a/a.out, process 8624
ptrace: Operation not permitted.

お、カーネルが禁止してくれているようだ。

ptrace のマニュアルの EPERM の項に書いてある該当しそうな条件は set-user-ID/set-group-ID programs というところかなぁ。ruid=euid=suid にしてあるので、これら3つ以外になにか記録してあるのだろうか。

kill でシグナルを送ることはできる。

また、vfork でなく、fork でも同じ挙動になる。まぁ、メモリを覗かれるだけでも危ないか。



田中哲