Perl Jam VI: 骆驼的回归

最近有几场关于Perl安全性的演讲,重点关注CGI模块Bugzilla。David Farrell在Netanel Rubin的Perl Jam马戏团中对此进行了回应。Perl存在许多更严重的问题,我们应该考虑这些问题。

Perl的舍入问题

Perl中批准的数字舍入方式是通过(s)printf,但存在问题。简而言之,它做了错误的事情。

大多数人被教导的规则是1、2、3、4向下舍入到0,而5、6、7、8、9向上舍入到下一个0。这意味着向上舍入的数字比向下舍入的数字多,任何可能舍入的计算都会引入系统性偏差。你不需要看《超人III》就能意识到这种灾难性的全球影响。

舍入数字的方法不止一种。大多数人希望得到最近的数字,但如果你在中间,还有其他选择。不止两种方法。不止三种。好吧,有很多种方法。

  • 四舍五入到最接近的整数

  • 四舍五入到最接近的整数下面

  • 四舍五入到0

  • 四舍五入到0的相反方向

  • 四舍五入到偶数

  • 四舍五入到奇数

  • 交替向上和向下舍入

  • 随机舍入

如果你使用GNU C编译器(或基于它的任何东西),默认情况下是四舍五入到偶数。Perl依赖于这种行为。

$ perl -e 'printf "%.0f\n", shift' 1.5
2

$ perl -e 'printf "%.0f\n", shift' 2.5
2

每次尝试都会得到相同的答案(因此,没有随机或交替舍入)。GNU C编译器也可以使用floor、ceiling或truncate,但它们有类似的问题。

在你进行舍入的过程中,你会得到比奇数更多的偶数。如果你正在编写银行软件,不对称货币舍入可能会使货币不稳定。风险简报中有关于舍入中的安全问题的几个条目。这些问题比一些无足轻重的“攻击”CGI.pm要严重得多,因为程序员不能读懂。

负数的模数

在像vi或emacs、制表符或空格、星球大战或星际迷航(每个问题的第一个答案是正确的)这样的激烈的技术辩论中,那些真正重要的问题,比如负数模加法的正确值,却被忽视了。

二进制“%”是模运算符,它计算第一个操作数与第二个操作数之间的除法余数。给定整数操作数$m$和$n$:如果$n$是正数,则$m\%n$是$m$减去最大的小于或等于$m$的$n$的倍数。如果$n$是负数,则$m\%n$是$m$减去最小的大于或等于$m$的$n$的倍数(即,结果将小于或等于零)。

%运算符的perldoc文档

模运算符对两个数字进行操作。对于$m % $n,你有

$m $n %
0 0 未定义
+ + $m - $n * $i ∈ $n * $i <= $m and ($m - $n * $i) < $n
+ - $m - $n * $i ∈ $n * $i >= $m and ($m - $n * $i) < $n
- +
- -
my( $m, $n ) = @ARGV;

$m //= 137;
$n //= 13;

my $template = <<'HERE';
m = %d  n = %d

   $m %  $n = %d
  -$m %  $n = %d
   $m % -$n = %d
  -$m % -$n = %d
HERE

printf $template,
   $m, $n,
   $m %  $n,
  -$m %  $n,
   $m % -$n,
  -$m % -$n;

运行结果取决于一元减号的位置

$ perl modulo.pl 137 12
m = 137  n = 12

   $m %  $n = 5
  -$m %  $n = 7
   $m % -$n = -7
  -$m % -$n = -5

这个一元减号运算符比取模运算符高两个优先级。Perl让一个运算符比另一个运算符更好用,这完全是另一个问题,但就是这样,我们现在无法修复它。再次尝试。使用括号(这是Perl从LISP那里偷来的一个特性,LISP有额外的特性可以提供)来分隔运算符

my( $m, $n ) = @ARGV;

$m //= 137;
$n //= 13;

my $template = <<'HERE';
m = %d  n = %d

    $m %  $n  = %d
  -($m %  $n) = %d
    $m % -$n  = %d
  -($m % -$n) = %d
HERE

printf $template,
    $m, $n,
    $m %  $n,
  -($m %  $n),
    $m % -$n,
  -($m % -$n);

这次你得到了不同的数字

m = 137  n = 12

    $m %  $n  = 5
  -($m %  $n) = -5
    $m % -$n  = -7
  -($m % -$n) = 7

但是问题更严重,因为这些数字不是文档中所说的应该是的数字。“如果$n是正数,那么$m % $n是$m减去小于或等于$m的最大$n的倍数”。让我们以-137和12为例。对此有几种不同的看法。如果我们将“倍数”称为$i,并且必须为正数,那么不存在这样的$i,使得$n * $i将小于或等于任何负数。如果$i可以是负数,那么“最大”这个词就有点麻烦了。维基百科说大数是正数

伪造的随机数

Perl有一个rand函数。它声称返回“一个大于等于0的随机分数数”,但它并不随机。它不是真正的随机,而是一种可能在你只想用它在中学编程入门课程中完成作业时能工作的伪造随机数。尽管文档中包含脚注说“你不应该在安全性敏感的情况下依赖它”,但它并没有像应该的那样说“永远不要使用这个”。尝试这个程序

$ perl -le 'srand(137); print rand for 1 .. 10'

它输出一些数字,可能看起来像这样

0.470744323291914
0.278795581867115
0.263413724062172
0.646815254210146
0.958771364426031
0.3733677954733
0.561358958619476
0.537256242282716
0.967152799238111
0.846555037715689

再次运行它

0.470744323291914
0.278795581867115
0.263413724062172
0.646815254210146
0.958771364426031
0.3733677954733
0.561358958619476
0.537256242282716
0.967152799238111
0.846555037715689

不仅你得到了相同的数字,而且它们的顺序也相同。Perl试图通过自动调用srand并给它一个“随机”的数字来开始一个完全可重复的序列来隐藏这个问题。

这不是伪造随机数(Perl的文档从未称之为“伪造”)的唯一问题。它们只能表示某些离散的值。例如,查看为什么Perl在Win32上的rand()永远不会生成介于0.890655528357032和0.890685315537721之间的值?。在Windows上,Perl使用15位来表示伪造随机数的范围,而不是Perl能用的53位。

如果你在一个不断运行的应用程序中使用它,这可能会以各种方式出现问题。最终你将回到序列的起点,可能撞上现有的客户数据。

Perl让任何人都能编程

Perl最大的问题可能在于,任何人只要有文本编辑器就能编写程序并将其上传到互联网。这是Perl允许某人完成工作的一项特性,但问题出现在有人试图将其推广到其他人的工作时。例如Not Matt’s Scripts这样的项目试图通过一次修复一个脚本来减轻这个问题。但宇宙的寿命也无法覆盖到所有脚本。

总结

如果你已经读到这儿而没有在Twitter、Reddit或Hacker News上抱怨,恭喜你。你知道这一天是一年中的哪一天。

这些问题都是真实的,如果你应用程序对数字的微小差异敏感(例如计算宇宙的基本常数或养老金基金分配),你可能会使用复杂的数字库,并进行各种级别的审计来验证结果。


这篇文章最初发表在PerlTricks.com上。

标签

brian d foy

brian d foy 是一名 Perl 训练师和作家,同时也是 Perl.com 的资深编辑。他是《Mastering Perl》、《Mojolicious Web Clients》、《Learning Perl Exercises》等书的作者,同时也是《Programming Perl》、《Learning Perl》、《Intermediate Perl》和《Effective Perl Programming》等书的合著者。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在 GitHub 上打开一个 issue 或 pull request 来帮助我们。