当大小过大时,vector.resize函数会破坏内存。

8

正在发生的是我正在读取加密数据包,遇到一个损坏的数据包,长度返回了一个非常大的随机数。

size_t nLengthRemaining = packet.nLength - (packet.m_pSource->GetPosition() - packet.nDataOffset);

seckey.SecretValues.m_data.resize(nLengthRemaining);

在这段代码中,m_data是一个std::vector<unsigned char>。由于数据包已损坏,nLengthRemaining过大,因此resize函数会抛出异常。问题不在于resize函数会抛出异常(我们处理了异常),而是resize函数已经破坏了内存,这会导致后面出现更多的异常。
我想要做的是,在调用resize之前知道长度是否过大,只有当它是可行的时候才调用resize。我尝试在调用resize之前放置以下代码:
std::vector<unsigned char>::size_type nMaxSize = seckey.SecretValues.m_data.max_size();
if(seckey.SecretValues.m_data.size() + nLengthRemaining >=  nMaxSize) {
    throw IHPGP::PgpException("corrupted packet: length too big.");
}
seckey.SecretValues.m_data.resize(nLengthRemaining);

这段代码使用了std::vector的max_size成员函数来测试nLengthRemaining是否更大。但这可能不可靠,因为nLengthRemaining仍然小于nMaxSize,但显然足够大以导致resize出现问题(nMaxSize为4xxxxxxxxx,nLengthRemaining为3xxxxxxxxx)。

此外,我还没有确定resize抛出的异常。它既不是std::length_error也不是std::bad_alloc。它抛出的异常对我来说并不太重要,但我很好奇。

顺便说一下,你需要知道,这段代码在正常情况下可以正确工作。只有在数据包损坏的情况下才会出现问题。请帮帮我!谢谢。

更新:

@Michael。现在,如果数据包大于5 MB,我将忽略它。我将与其他团队成员讨论可能验证数据包的方法(可能已经存在,而我只是不知道)。我开始认为这确实是我们版本的STL中的一个错误,它抛出的异常甚至不是std::exception,这让我感到惊讶。我将尝试从我的主管那里找出我们正在运行哪个版本的STL(我该如何检查?)。

另一个更新:我刚刚证明这是我在Visual Studio 6开发机上使用的STL版本中的一个错误。我编写了这个示例应用程序:

// VectorMaxSize.cpp:定义控制台应用程序的入口点。 //

#include "stdafx.h"
#include <vector>
#include <iostream>
#include <math.h>
#include <typeinfo>

typedef std::vector<unsigned char> vector_unsigned_char;

void fill(vector_unsigned_char& v) {
    for (int i=0; i<100; i++) v.push_back(i);
}


void oput(vector_unsigned_char& v) {
    std::cout << "size: " << v.size() << std::endl;
    std::cout << "capacity: " << v.capacity() << std::endl;
    std::cout << "max_size: " << v.max_size() << std::endl << std::endl;
}

void main(int argc, char* argv[]) {
    {
        vector_unsigned_char v;

        fill(v);

        try{
            v.resize(static_cast<size_t>(3555555555));
        }catch(std::bad_alloc&) {
            std::cout << "caught bad alloc exception" << std::endl;
        }catch(const std::exception& x) {
            std::cerr << typeid(x).name() << std::endl;
        }catch(...) {
            std::cerr << "unknown exception" << std::endl;
        }

        oput(v);    
        v.reserve(500);
        oput(v);
        v.resize(500);
        oput(v);
    }

    std::cout << "done" << std::endl;
}

在我的VS6开发机上,它的行为和加密项目一样,会引起各种混乱。当我在Visual Studio 2008机器上构建和运行它时,调整大小会抛出std::bad_alloc异常,而向量不会被破坏,正如我们所期望的那样!是时候玩一些EA Sport NCAA足球了,哈哈!


我很好奇你使用的编译器/STL版本是什么。我可以访问的实现如果分配失败不会破坏向量对象。 - jmucchiello
@cchampion:“在我的VS6开发机上...”如果你早点说,我们就不会在这上面浪费那么多时间了。那是十多年前的技术!当然会有bug。看看我的答案吧。 - sbi
1
请查看我的最新编辑 - 问题出在VC6的<xmemory>头文件中(但不是Dunkumware VC错误页面上讨论的那个)。 - Michael Burr
3个回答

7
我认为vector::max_size()几乎总是一个“硬编码”的东西——它独立于系统/库准备动态分配多少内存。你的问题似乎是vector实现中的一个bug,在分配失败时会损坏东西。
“Bug”可能是一个过于强烈的词。 vector::resize()是根据vector::insert()定义的,标准对vector::insert()有以下规定:

如果抛出除T的复制构造函数或赋值运算符之外的异常,则没有影响

因此,似乎有时允许resize()操作损坏向量,但如果该操作具有异常安全性将会更好(我认为期望库这样做并不过分,但也许比我想象的要困难)。
你似乎有几个合理的选择:
  • 更改或更新到没有损坏bug的库(您使用的是哪个编译器/库版本?)
  • 不要检查vector::max_size(),而是将nMaxSize设置为您自己合理的最大值,并执行上述操作,但使用该阈值。

编辑:

我看到你正在使用VC6——vector::resize()中肯定有一个bug可能与你的问题有关,尽管看了补丁,我实在看不出来(实际上是vector::insert()中的一个bug,但是如上所述,resize()调用了insert())。我猜值得去Dinkumwares' page for bug fixes to VC6查看并应用修复。

这个问题也可能与该页面上的<xmemory>补丁有关——不清楚那里讨论的是什么bug,但是vector::insert()确实调用了_Destroy(),而vector<>定义了名称_Ty,所以你可能会遇到这个问题。一个好处是——你不必担心管理头文件的更改,因为Microsoft再也不会碰它们了。只需要确保将补丁纳入版本控制并进行文档记录。

请注意,Scott Meyers在 "Effective STL" 中建议使用 SGISTLPort 库以获得比 VC6 自带的更好的 STL 支持。我没有这样做过,所以我不确定那些库的工作效果如何(但我也很少在 VC6 中使用 STL)。当然,如果你有升级到较新版本的 VC 的选项,尽管去做吧。

再编辑一下:

感谢测试程序……

VC6的默认分配器(在中)使用有符号整数指定要分配的元素数量,如果传入的大小为负数(显然这是你在做什么的——当然在测试程序中是这样的),_Allocate()函数会强制将请求的分配大小设置为零并继续执行。请注意,零大小的分配请求几乎总是成功的(不是说vector检查失败了),因此vector::resize()函数会愉快地尝试将其内容移动到新块中,但这个块远远不够大。所以堆被破坏了,很可能会撞上一个无效的内存页,而且无论如何——你的程序都已经崩溃了。

因此,底线是永远不要让VC6一次分配超过INT_MAX个对象。在大多数情况下(包括VC6或其他情况下)可能都不是一个好主意。

另外,你应该记住,VC6使用从new返回0的预标准习惯,而不是抛出bad_alloc


如果T的复制构造函数或赋值运算符之外抛出异常,则没有影响,就好像未调用resize()一样。我相当确定,对向量的任何操作都不应该破坏内存。 - sbi
插入(insert())操作可能会导致复制/赋值操作(当向量内容被复制到新的分配时)- 这些操作是允许“有影响”的。它不应该做出像破坏堆这样的坏事,但不清楚是否发生了这种情况。在这些条件下引发异常可能会导致向量发生更改(也许并非所有元素都转移到新向量中)。他的代码可能会发现向量不再合理。无论如何都不是很好的行为,我认为STL实现可能会更好地处理这种情况。 - Michael Burr
你说得没错,复制/分配 vector<unsigned char> 中的元素不应该导致任何异常 - 这对我来说似乎指向了一个有缺陷的 STL 实现,它不能很好地处理内存不足的情况。我对所使用的平台/编译器/库的详细信息很感兴趣。 - Michael Burr
如果真的是这样,在这些条件下,允许异常导致向量发生变化,我会感到惊讶。此外,我无法从您引用的内容中看出如何阅读它。 - sbi
按照标准我对这行的理解是,如果在vector::insert()调用中抛出异常,则除非异常来自复制构造函数或赋值运算符,否则向量不会发生任何变化(“没有影响”),此时可能发生了一些(未指定的)影响。然而,标准文件并不以清晰著称,因此我的理解可能有偏差。 - Michael Burr
@Michael:重新阅读后,我不得不同意你的怀疑。 <叹气> - sbi

5
我强烈建议在调用库函数之前检查数据是否损坏!使用某种哈希码或校验和算法对数据包进行校验。你不能依赖库来帮助你,因为它无法执行以下操作:如果你给它一个已损坏但仍然有效(从库的角度看)的大小,那么它可能会分配768MB的RAM。这可能在系统中有足够的空闲内存时可以工作,但如果有其他消耗过多内存的程序在运行,则可能失败。所以像上面说的一样:先检查!

我同意。我认为你的问题根源在于你依赖加密算法告诉你它“假装”的大小。你真正需要的是块大小强制执行和填充(就像MD5一样),或者有另一种提供大小信息的带外方式。 - Chris K
我同意,我们确实需要一些验证数据包的方法。我会在周一向这个项目中的其他程序员提到这件事。现在,如果数据包大于5 MB,我将跳过它。 - cchampion

4

当你说“调整大小已损坏内存”时,我不知道你的意思。你是如何确定的?

顺便说一下,我不同意Michael's answer。如果std::vector<> ::resize()在向量扩展时抛出异常,我看到两种可能性:

  1. 要么用于填充新空间(或复制元素)的构造函数之一抛出了异常,或者
  2. 用于增加向量的分配器抛出了异常
  3. 或者在向量事先确定所请求的大小过大并引发异常。

对于std::vector<unsigned char>,我们可以安全地排除#1,因此只剩下#2。如果您没有使用任何特殊分配器,则应使用std::allocator,据我所知,它将调用new来分配内存。而new会抛出std::bad_alloc。但是,您说您无法捕获这个异常,所以我不知道会发生什么。

无论什么情况,它都应该源自于std::exception,因此您可以这样做来查找:
try {
  my_vec.resize( static_cast<std::size_t>(-1) );
} catch(const std::exception& x) {
  std::cerr << typeid(x).name() << '\n';
}

那个的结果是什么?

不管怎样,无论结果是什么,我相当确定它不应该破坏内存。要么这是你的std lib实现中的bug(如果你问我,这不太可能,除非你使用一个非常旧的版本),要么你在其他地方做错了什么。


编辑 现在你说你用的是VS6...

你应该早点说。VC6发布已经十多年了,当时微软因为长期没有出现在标准委员会的会议上而失去了他们的投票权。他们发行的标准库实现来自Dinkumware(好的),但由于法律问题,它却是VC5的那个版本(非常糟糕),有许多较小和较大的错误,甚至没有支持成员模板,尽管VC6编译器支持它。老产品还能期望什么呢?

如果你不能切换到一个更好的VC版本(我建议至少是VC7.1或VS.NET 2003,因为这是向标准合规性迈出的重要一步),至少看看Dinkumware是否仍然销售他们出色的库的VC6t版本。(实际上,我会感到惊讶,但他们曾经有过一个版本,你永远不知道......)

关于异常:在早期的VC版本(包括VC6,不包括VC8,也就是VS.NET 2005,我不确定VC7.1是否包含)中,默认情况下可以通过catch(...)捕获访问冲突。因此,如果这样的catch块捕获到了一些东西,您将无法知道这是否是C++异常。我的建议是只在使用throw;的情况下使用catch(...),以便让该异常传递。如果这样做,您将在AV时得到真正的崩溃,并能够在调试器中跟踪堆栈。如果不这样做,AV将被吞噬,然后您将被困在一个疯狂的应用程序中,而您甚至不知道发生了什么。但除了放弃出现AV的应用程序之外,做任何事情都没有意义。AV是未定义行为的结果之一,之后所有的赌注都是打翻的。

我之所以说它破坏了内存,是因为简单的日志语句开始抛出异常,还有一堆与内存相关的断言不断弹出。在调用 resize 函数后,整个程序都失去了理智。如果你传递一个合理的长度给 resize 函数,这种情况就不会发生。如果我跳过那个数据包,也不会发生这种情况。我将尝试使用代码确定异常类型,我之前不知道 typeid!谢谢。 - cchampion
如果你不知道的话:你必须#include <typeinfo>才能使用它。 - sbi
信不信由你,const std::exception&处理程序没有捕获它。我不知道这个异常是什么。现在我真的开始相信我们使用的STL版本有一个bug。我将在周一与程序员负责人讨论此事。 - cchampion
要成功捕获访问冲突(也称SEH异常),您需要设置编译器设置/Eha。 @cchampion:这就解释了为什么您无法捕获异常,因为它可能甚至不是C++异常。 - newgre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接