【问题标题】:Why aren't functions like std::is_permutation() intrinsically unsafe?为什么像 std::is_permutation() 这样的函数本质上不是不安全的?
【发布时间】:2013-07-10 23:03:03
【问题描述】:

在过去十年左右的时间里,C 和 C++ 程序员因经常无法执行正确的边界检查而受到打击,尤其是在字符串上。这些故障常常导致主要软件产品出现严重的安全问题。由于缓冲区溢出的不安全性已被充分理解,建立适当边界检查的驱动力已将许多程序员推离了传统的缓冲区和字符串操作函数,如 strcpy()sprintf(),至少部分原因是这些函数倾向于通过假设目标缓冲区的大小来引发缓冲区溢出问题。 std::stringstd::vector 等 STL 类型的优点之一是它们对缓冲区访问的强大控制。

但有一件事让我很困惑。 <algorithms> C++ 标头中的许多最广泛使用的函数似乎都在恳求溢出滥用:特别是那些采用 begin 迭代器(尤其是 InputIterator)而没有匹配的 end 迭代器的函数。例如:

template <class InputIterator, class OutputIterator>
  OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result);

template <class InputIterator, class OutputIterator, class UnaryOperation>
  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperation op);

template <class ForwardIterator1, class ForwardIterator2>
   bool is_permutation (ForwardIterator1 first1, ForwardIterator1 last1,
                        ForwardIterator2 first2);

最后一个例子——is_permutation() 特别有指导意义。 copy()transform() 很容易理解,因此 C++ 程序员应该知道在调用这些函数之前手动检查输出容器的边界使用某些东西就像 back_inserter 一样,它确保输出容器根据需要增长。因此,可以证明尽管copy()transform() 可以被误用,但任何东西都可以,而且程序员很容易就这些函数的最佳实践进行培训。

is_permutation() 是一个更棘手的案例。看看上面的函数声明,你会假设第二个范围的大小(以first2 开头的那个)?第二个范围是否需要与第一个范围相同,或者不更小,或者不更大?我敢打赌,这些问题的简单答案不会浮现在您的脑海中。对于大多数程序员来说,“排列”的概念不如复制的概念那么舒服和熟悉。因此,is_permutation() 错误并以一种或另一种方式溢出缓冲区似乎相对容易。

“查一下!”我听你说。是的,当然。但是,如果程序员记住他们应该记住的所有内容并查找其他所有内容,那么我们就不会有错误和安全漏洞,对吗?

那么,为什么 is_permutation() 和类似的函数(即函数采用所有输入迭代器但不是每个范围的完整开始-结束迭代器对)不需要所有输入范围的完整开始-结束对? (请注意 lexicographical_compare(),例如,确实 满足此要求。)像 is_permutation() 这样的函数实际上并没有我想象的那么不安全吗?

【问题讨论】:

  • 这是一个问题还是一篇博文?
  • 顺便说一句,我喜欢that ale
  • @RobertHarvey 我本来想看小说的,但错过了并发表了宣言。
  • C 和 C++ 传统上出于性能原因会避开边界检查,让软件开发人员来决定他们是否需要它。正如 Stroustrup 本人 once said“C 让你很容易射中自己的脚;C++ 让它更难,但当你这样做时,它会把你的整条腿都炸飞。”
  • 切线:“本质上不安全”有点用词不当。您可以选择 C ​​或 C++ 中的任何内容 --- 甚至是 gets() --- 并且会出现安全且适合使用的情况。

标签: c++ algorithm security stl


【解决方案1】:

大多数语言本质上是不安全的,程序员可以正确使用它。程序员必须知道在调用函数之前使用的参数是否正确。

此外,在某些情况下,例如copy,它允许在开放范围上使用前向迭代器。例如:

std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout," "));

没有对应的迭代器来标记流的结束,而且流确实没有结束,你可以不断的往里面添加。

【讨论】:

  • 第二部分也可以用第二个结束迭代器完成 - 只是总是比较为false
  • @Xeo:这将如何帮助执行 OP 想要的安全性?毕竟,当前接口和使用总是比较为假的第四个迭代器没有区别。是的,我知道,它可以在其他情况下进行更安全的检查......但是语言不是用来照顾你的,它可以让你编写最有效的代码(以更安全的方式,但优先级是性能)。您可以在性能之上构建安全,但您不能在安全之上构建性能。
  • 或者我个人最喜欢的std::back_inserter
【解决方案2】:

在 C++14 中,有 equalis_permutationmismatch 四个迭代器版本来准确解决这一点。

【讨论】:

  • 参考(例如当前 C++15 提案中的章节号或建议的措辞文档的链接)会很有用。
  • N3690 25.2.10 不匹配 [alg.mismatch],25.2.11 相等 [alg.equal],25.2.12 是排列 [alg.is_permutation],
【解决方案3】:

我不确定在 is_permutation 的第二个范围内引入 last 迭代器会使函数变得不那么笨拙。我认为这会让事情变得更加混乱。

排列的问题是语义存在于名称本身中。要检查一个序列是否是另一个序列的排列,您希望没有 last 迭代器的序列至少与第一个序列一样长。

如果不是这样,那么您就不需要调用is_permutation,因为它根本不可能是一个排列。如果它更长,您预计它不会迭代超过第一个序列的长度 - 为什么会这样?嗯,它没有 - 这就是你所期望的,所以没有失去信心。

C++ 确实希望程序员采取基本的预防措施,并让我们在许多情况下负责边界检查。如果不将这种控制权交给程序员,语言的力量就会减弱。如果我调用is_permutation,那么我知道我的第二个迭代器不会溢出,因为我知道什么是排列。我当然不想浪费循环进行毫无意义的边界检查。

我认为这句老话很适用:权力越大,责任越大。这很公平,不是吗?

【讨论】:

  • 我希望第二个序列与第一个序列一样长。如果它更长,它不可能是一个排列。但这是一个小问题:肯定有可能有比第二个序列更多的可用元素;这些是否是概念序列的语义部分取决于您对“第二个序列”所指的确切含义的解释。 IOW,如果您不知道序列的长度相同,则应在调用 is_permutation 之前先检查一下。似乎是一个简单的规则。
  • c++14 将为第二个范围添加 last 的重载。
  • @rici 实际上没有必要谈论第二个序列的长度,只要元素至少和第一个一样多。您可以在较大的序列中测试较小长度的排列,而不必担心只检查N 元素,其中N 是第一个序列的长度。所以更正确的说法是第二个序列必须包含至少与第一个序列一样多的元素。
  • @paddy:就像我说的,我认为它的语义。我会说 first2 所在的 container 需要足够大,first2 + (last1 - first1) 才有效。然而,被检查的 range 正好与第一个范围的大小一样长,这可能是也可能不是整个容器。这与 std::equal 相同,很明显,如果要比较两个整个容器的相等性,则首先必须在调用 std::equal 之前比较它们的长度。所以语义问题是“序列”是指“范围”还是“容器的其余部分”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-14
  • 2015-04-23
  • 2010-10-30
  • 2023-03-27
  • 1970-01-01
  • 2015-09-20
相关资源
最近更新 更多