てきとうなメモ

本の感想とか技術メモとか

bashのスクリプト読み込みの動き

www.iimc.kyoto-u.ac.jp https://www.iimc.kyoto-u.ac.jp/services/comp/pdf/file_loss_insident_20211228.pdf

bash は、シェルスクリプトの実行中に適時シェルスクリプトを読み込みます。この挙動によ る副作用を認識できておらず、実行中のスクリプトが存在している状態でスクリプトの上書きに よりリリースしてしまったことで、途中から修正したシェルスクリプトの再読み込みが発生し、 結果的に未定義の変数を含む find コマンドが実行されてしまいました。この結果、本来のログ ディレクトリに保存されたファイルの削除をする処理ではなく、/LARGE0 のファイルを削除し てしまいました

これは怖い。ただ、スクリプト含めソフトウェアの実行中にソフトウェアをアップデートするのは、うまくいくかもしれんが怖いのでやらないという認識かなあ。

で、bashの場合1行ずつ読み込んでいるの?バッファしていないの?という部分が疑問だった。

以下の記事を見ると、スクリプト実行途中で再読み込みが発生してるっぽい

qiita.com

で、ちょっと調べてみた。

以下で調べている方がいて、だいたい似たような話になる。

(vimの設定に気づいておらず、記事を見るまで現象を再現できなかった。感謝) zenn.dev

結論としてバッファは使っている。input.cの以下の変数にファイルディスクリプタをインデックスにした配列として保存している

static BUFFERED_STREAM **buffers = (BUFFERED_STREAM **)NULL;

バッファをメモリ上に確保しているのは以下。

BUFFERED_STREAM *
fd_to_buffered_stream (fd)
     int fd; 
{
  char *buffer;
  size_t size;
  struct stat sb; 

  if (fstat (fd, &sb) < 0)
    {   
      close (fd);
      return ((BUFFERED_STREAM *)NULL);
    }   

  size = (fd_is_seekable (fd)) ? min (sb.st_size, MAX_INPUT_BUFFER_SIZE) : 1;
  if (size == 0)
    size = 1;
  buffer = (char *)xmalloc (size);

  return (make_buffered_stream (fd, buffer, size));
}

バッファを確保する時にファイルのサイズ(sb.st_size)とMAX_INPUT_BUFFER_SIZEの小さい方を設定してxmallocしている。MAX_INPUT_BUFFER_SIZEは8kぐらい。

この値がBUFFERED_STREAMのb_sizeとして保存される。実際にスクリプトを読み込む時にb_fill_bufferでzreadを呼び、zread(lib/sh/zread.c)はread(2)を呼び、読み込むサイズとしてb_sizeが指定される。

static int
b_fill_buffer (bp)
     BUFFERED_STREAM *bp;
{
...
  nr = zread (bp->b_fd, bp->b_buffer, bp->b_size);

b_fill_bufferはこんな感じのスタックで呼ばれる

#0  b_fill_buffer (bp=0x771790) at input.c:494
#1  0x000000000047ccb2 in buffered_getchar () at input.c:576
#2  0x0000000000428046 in yy_getc () at /Users/chet/src/bash/src/parse.y:1422
#3  0x0000000000428f9d in shell_getc (remove_quoted_newline=1) at /Users/chet/src/bash/src/parse.y:2358
#4  0x000000000042a819 in read_token (command=0) at /Users/chet/src/bash/src/parse.y:3290
#5  0x0000000000429cc8 in yylex () at /Users/chet/src/bash/src/parse.y:2797
#6  0x0000000000424b85 in yyparse () at y.tab.c:1835
#7  0x0000000000424725 in parse_command () at eval.c:347
#8  0x0000000000424807 in read_command () at eval.c:391
#9  0x00000000004241ee in reader_loop () at eval.c:138
#10 0x0000000000421cf4 in main (argc=2, argv=0x7fffffffe108, env=0x7fffffffe120) at shell.c:811

ただ、このままだと8Kもしくはファイルサイズだけバッファしているので、小さなスクリプトだと更新した情報を読み込まないのでは?しかし、デバッグしていると読み込んでいる。何か変な動きをしていて、どこかでファイルディスクリプタのオフセットが戻っているように見えるが、それがどこなのかわからなかった。

こちらの方のツイートで謎が解けた。外部コマンドを実行時にsync_buffered_stream内でlseek(2)して、ファイルディスクリプタのオフセットを外部コマンドの行の後に設定しているということがわかった。

/* Seek backwards on file BFD to synchronize what we've read so far
   with the underlying file pointer. */
int
sync_buffered_stream (bfd)
     int bfd;
{
  BUFFERED_STREAM *bp;
  off_t chars_left;

  if (buffers == 0 || (bp = buffers[bfd]) == 0)
    return (-1);

  chars_left = bp->b_used - bp->b_inputp;
  if (chars_left)
    lseek (bp->b_fd, -chars_left, SEEK_CUR);
  bp->b_used = bp->b_inputp = 0;
  return (0);
}

b_usedがBUFFERED_STREAM上で有効なサイズ、b_inputpがBUFFERED_STREAM上でこれまで読み込んだインデックスなので、外部コマンドの後ろにファイルディスクリプタのオフセットが移動する

このときのスタックはこんな感じ。

#0  sync_buffered_stream (bfd=255) at input.c:557
#1  0x000000000045730e in make_child (command=0x7726d0 "sleep 5", flags=0) at jobs.c:2171
#2  0x0000000000443eb4 in execute_disk_command (words=0x771c90, redirects=0x0, command_line=0x772650 "sleep 5", pipe_in=-1, pipe_out=-1, async=0, fds_to_close=0x7719d0,
    cmdflags=0) at execute_cmd.c:5507
#3  0x0000000000442799 in execute_simple_command (simple_command=0x771910, pipe_in=-1, pipe_out=-1, async=0, fds_to_close=0x7719d0) at execute_cmd.c:4668
#4  0x000000000043b76f in execute_command_internal (command=0x7718d0, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x7719d0) at execute_cmd.c:845
#5  0x000000000043acbd in execute_command (command=0x7718d0) at execute_cmd.c:395
#6  0x00000000004242f7 in reader_loop () at eval.c:170
#7  0x0000000000421cf4 in main (argc=2, argv=0x7fffffffe108, env=0x7fffffffe120) at shell.c:811

さらに、sync_buffered_streamの中ではb_usedとb_inputpが0に設定され、外部コマンドの後ろは読み込んでいないことになるので、次に1文字読み込もうとした時、バッファからは取得せずに、b_fill_bufferが実行されファイルをreadすることになる。