Exiv2卡死问题

发布于 作者 shisongran留下评论

Exiv2是用来提取图片Exif信息的开源组建。它提供了面向对象风格的接口,使用起来非常便利。近期在使用这个组建的时候遇到了卡死进程的问题,说说最后是如何解决的。

现象

先说下Exiv2的简单调用流程。首先使用Exiv2::ImageFactory::open打开文件,接着使用Exiv2::Image::readMetadata()分析meta信息,最后利用Exiv2::ExifData::findKey找到需要的Exif的Key即可。

在生产环境中实际Run的过程中以上逻辑的确可以获取到Exif信息,能够满足生产的要求。但是时不时发现会有实例卡死在Exiv2的堆栈中,CPU单核打满,堆栈表示在处理Riff相关的信息。

问题定位

利用堆栈信息,我们发现进程实际上是卡死在了Exiv2::Image的子类Exiv2::RiffVideo,在调用该类的Exiv2::RiffVideo::readMetadata时,该类采用了一个循环来做这个事情,代码如下:

C++
    void RiffVideo::readMetadata()
    {
            ....
            ....
            ....
        
        io_->read(buf.pData_, bufMinSize);
        io_->read(buf.pData_, bufMinSize);
        xmpData_["Xmp.video.FileType"] = buf.pData_;

        while (continueTraversing_) decodeBlock();
    } // RiffVideo::readMetadata

其中30行调用了decodeBlock()实现如下:

C++
    void RiffVideo::decodeBlock()
    {
        const long bufMinSize = 4;
        DataBuf buf(bufMinSize+1);
        DataBuf buf2(bufMinSize+1);
        unsigned long size = 0;
        buf.pData_[4] = '\0' ;
        buf2.pData_[4] = '\0' ;

        io_->read(buf2.pData_, 4);

        if(io_->eof() || equalsRiffTag(buf2, "MOVI") || equalsRiffTag(buf2, "DATA")) {
            continueTraversing_ = false;
            return;
        }
        else if(equalsRiffTag(buf2, "HDRL") || equalsRiffTag(buf2, "STRL")) {
            decodeBlock();
        }
        else {
            io_->read(buf.pData_, 4);
            size = Exiv2::getULong(buf.pData_, littleEndian);

        tagDecoder(buf2, size);
        }
    } // RiffVideo::decodeBlock

可以看到在都不符合的时候,会进入tagDecoder函数,而该函数在会在一个分支调用以下内容:

C++
       void RiffVideo::tagDecoder(Exiv2::DataBuf& buf, unsigned long size)
    {
        uint64_t cur_pos = io_->tell();
        static bool listFlag = false, listEnd = false;

        if(equalsRiffTag(buf, "LIST")) {
            listFlag = true;
            listEnd = false;

            while((uint64_t)(io_->tell()) < cur_pos + size) decodeBlock();

            listEnd = true;
            io_->seek(cur_pos + size, BasicIo::beg);
        }
        
            ...
            ...
            ...
        
        else if (listFlag) {
            // std::cout<<"|unprocessed|"<<buf.pData_;
            skipListData();
        }
        else {
            // std::cout<<"|unprocessed|"<<buf.pData_;
            io_->seek(cur_pos + size, BasicIo::beg);
        }
    } // RiffVideo::tagDecoder

如果在进行一份循环后,若listFlag被标记为true,下次循环会调用skipListData(),该函数实现如下:

C++
    void RiffVideo::skipListData()
    {
        const long bufMinSize = 4;
        DataBuf buf(bufMinSize+1);
        buf.pData_[4] = '\0';
        io_->seek(-12, BasicIo::cur);
        io_->read(buf.pData_, 4);
        unsigned long size = Exiv2::getULong(buf.pData_, littleEndian);

        uint64_t cur_pos = io_->tell();
        io_->seek(cur_pos + size, BasicIo::beg);
    } // RiffVideo::skipListData

可以看到它向前跳转了12个字节,如果Exiv2::getULong(buf.pData_, littleEndian);得到的值为4,那么指针在后续的io_->seek中,以及最外层循环中的两次io_->read恰好使得下次进入skipListData时,文件指针和上次一致,导致程序死循环。

解决

实际上,很少有图片请求会走到这块逻辑。RIFF在Exiv2中定义的mimeTypevideo/riff,并不是实际意义上的图片。 由于该服务目标是图片,所以在这儿我直接屏蔽掉了所有mimeType中不包含image/*的请求,生产环境中也没有发现由于该逻辑造成的流量损失,问题解决。 对Riff格式并不了解很多,暂时没有打算深究这块打算,先这样吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注