てきとうなメモ

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

PHPの足し算のバグ

via はじめてのにき(2012-02-25)

$ php -r 'echo (0x00+2);echo "\n";'
4
$ php -r 'echo (0x00+ 2);echo "\n";'
2
$ php -r 'echo (0x00 + 2);echo "\n";'
2
$ php -r 'echo (0x00 +2);echo "\n";'
4
$ php -v
PHP 5.3.8 (cli) (built: Dec  5 2011 21:24:09) 
Copyright (c) 1997-2011 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2011 Zend Technologies

なかなか面白い。以下のpatchでもうfixしているみたいだけども

古いコードはこっち。最新の5.3.10までバグっている。

<ST_IN_SCRIPTING>{HNUM} {
  char *hex = yytext + 2; /* Skip "0x" */
  int len = yyleng - 2; 

  /* Skip any leading 0s */
  while (*hex == '0') {
    hex++;
    len--;
  }

  if (len < SIZEOF_LONG * 2 || (len == SIZEOF_LONG * 2 && *hex <= '7')) {
    zendlval->value.lval = strtol(hex, NULL, 16); 
    zendlval->type = IS_LONG;
    return T_LNUMBER;
  } else {
    zendlval->value.dval = zend_hex_strtod(hex, NULL);
    zendlval->type = IS_DOUBLE;
    return T_DNUMBER;
  }
}

yytextはマッチしている文字列を含むのだが、'\0'で終了しているわけではなく、長さyylengで切り出す必要があるみたい。

例えば、

echo(0x00+2);

を解析する。0x以降を解析する時にyytextは

0x00+2);

となり、まず'0x'をスキップし、その後0をスキップする。残りの文字列の

+2);

がstrtolに渡されて2と評価される。ここまでが1つのtokenとして評価され、その後'+','2'が評価されるので

0x2+2=2+2=4

となる。すなわち、0x00+nという文字列はnを16進数で評価したものと10進数で評価したものの足し算になる。

16進と10進とで別の値になる方がわかりやすい

$ php -r 'echo(0x00+10);echo("\n");'
26
$ php -r 'echo(0x00+11);echo("\n");'
28
$ php -r 'echo(0x00+100);echo("\n");'
356 

となるのは

0x10+10=16+10=26
0x11+11=17+11=28
0x100+100=256+100=356

だから。

最初の引用で空白の位置で結果が変わるのはstrtolが

strtol("+2);", NULL, 16); // -> 2
strtol("+ 2);", NULL, 16); // -> 0
strtol(" +2);", NULL, 16); // -> 2
strtol(" + 2);", NULL, 16); // -> 0

と計算するためである。

同様に'-n'もstrtolで16進の-nに評価されるので

$ php -r 'echo(0x00-2);'
-4
$ php -r 'echo(0x00-10);'
-26

となる

'*n'や'/n'はstrtolで0になってしまうので普通に0と計算される

$ php -r 'echo(0x00*2);'
0
$ php -r 'echo(0x00*10);'
0
$ php -r 'echo(0x00/2);'
0
$ php -r 'echo(0x00/10);'
0