1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-28 08:48:48 +02:00
vcmi/client/CVideoHandler.cpp
Sergei Trofimovich 23215e039c client/CVideoHandler.cpp: fix crash on video playback
Avoid buffer overflow caused by sws_scale():
    http://trac.ffmpeg.org/ticket/9254
Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
has a few requirements for target data buffers on rescaling:
1. buffer has to be aligned to be usable for SIMD instructions
2. buffer has to be padded to allow small overflow by SIMD instructions
Unfortunately SDL_Surface does not provide these guarantees.
This means that atempt to rescale directly into SDL surface causes
memory corruption. Usually it happens on campaign selection screen
where short video moves start spinning on mouse hover.

To fix [1.] we use av_malloc() for memory allocation.
To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
We have to use intermdiate buffer and then use memcpy() to land it
to SDL_Surface.

Without the change crash has the following backtrace:

```
(gdb) bt
    (c=0x47508940, src=0x1ffeffef50, srcStride=0x1ffeffef30, srcSliceY=0, srcSliceH=116, dst=0x1ffeffef70, dstStride=0x1ffeffef40) at src/libswscale/x86/yuv2rgb_template.c:119
    (c=<optimized out>, srcSlice=<optimized out>, srcStride=0x432afa20, srcSliceY=<optimized out>, srcSliceH=116, dst=<optimized out>, dstStride=0x1ffefff0a0) at src/libswscale/swscale.c:969
    (this=0x1abaa330, x=90, y=72, dst=0x1a85a4c0, forceRedraw=<optimized out>, update=<optimized out>)
    at ../vcmi-9999/client/CVideoHandler.cpp:332
    at ../vcmi-9999/client/gui/CIntObject.cpp:83
    at ../vcmi-9999/client/gui/CGuiHandler.cpp:462
```

valgrind points to corruption right in sws_scale():

```
Invalid write of size 8
   at 0x6C50BD3: ??? (in /usr/lib64/libswscale.so.5.7.100)
   by 0x6C4FAE6: yuv420_rgb32_ssse3 (yuv2rgb_template.c:119)
   by 0x6C28DF2: sws_scale (swscale.c:969)
   by 0x4566F6: CVideoPlayer::nextFrame() (CVideoHandler.cpp:293)
   by 0x4573A6: CVideoPlayer::update(int, int, SDL_Surface*, bool, bool) (CVideoHandler.cpp:332)
   by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83)
   by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319)
   by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462)
   by 0x1F7450: mainLoop (CMT.cpp:1387)
   by 0x1F7450: main (CMT.cpp:513)
 Address 0x475088a8 is 0 bytes after a block of size 92,840 alloc'd
   at 0x483F7E5: malloc (vg_replace_malloc.c:380)
   by 0x52B4E23: SDL_malloc_REAL (SDL_malloc.c:5387)
   by 0x5266237: SDL_SIMDAlloc_REAL (SDL_cpuinfo.c:963)
   by 0x52EF042: SDL_CreateRGBSurfaceWithFormat_REAL (SDL_surface.c:123)
   by 0x2649AC: CSDL_Ext::newSurface(int, int, SDL_Surface*) (SDL_Extensions.cpp:42)
   by 0x457B20: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool, bool, bool) (CVideoHandler.cpp:182)
   by 0x457C60: CVideoPlayer::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, bool) (CVideoHandler.cpp:84)
   by 0x35B14E: CCampaignScreen::CCampaignButton::show(SDL_Surface*) (CCampaignScreen.cpp:126)
   by 0x25EC94: CIntObject::show(SDL_Surface*) [clone .part.0] (CIntObject.cpp:83)
   by 0x34E855: CMainMenu::update() (CMainMenu.cpp:319)
   by 0x25D589: CGuiHandler::renderFrame() (CGuiHandler.cpp:462)
   by 0x1F7450: mainLoop (CMT.cpp:1387)
   by 0x1F7450: main (CMT.cpp:513)
```

Signed-off-by: Sergei Trofimovich <slyfox@gentoo.org>
2021-07-31 14:35:31 +03:00

461 lines
10 KiB
C++

/*
* CVideoHandler.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include <SDL.h>
#include "CVideoHandler.h"
#include "gui/CGuiHandler.h"
#include "gui/SDL_Extensions.h"
#include "CPlayerInterface.h"
#include "../lib/filesystem/Filesystem.h"
extern CGuiHandler GH; //global gui handler
#ifndef DISABLE_VIDEO
//reads events and returns true on key down
static bool keyDown()
{
SDL_Event ev;
while(SDL_PollEvent(&ev))
{
if(ev.type == SDL_KEYDOWN || ev.type == SDL_MOUSEBUTTONDOWN)
return true;
}
return false;
}
#ifdef _MSC_VER
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "swscale.lib")
#endif // _MSC_VER
// Define a set of functions to read data
static int lodRead(void* opaque, uint8_t* buf, int size)
{
auto video = reinterpret_cast<CVideoPlayer *>(opaque);
return static_cast<int>(video->data->read(buf, size));
}
static si64 lodSeek(void * opaque, si64 pos, int whence)
{
auto video = reinterpret_cast<CVideoPlayer *>(opaque);
if (whence & AVSEEK_SIZE)
return video->data->getSize();
return video->data->seek(pos);
}
CVideoPlayer::CVideoPlayer()
{
stream = -1;
format = nullptr;
codecContext = nullptr;
codec = nullptr;
frame = nullptr;
sws = nullptr;
context = nullptr;
texture = nullptr;
dest = nullptr;
destRect = genRect(0,0,0,0);
pos = genRect(0,0,0,0);
refreshWait = 0;
refreshCount = 0;
doLoop = false;
// Register codecs. TODO: May be overkill. Should call a
// combination of av_register_input_format() /
// av_register_output_format() / av_register_protocol() instead.
av_register_all();
}
bool CVideoPlayer::open(std::string fname, bool scale)
{
return open(fname, true, false);
}
// loop = to loop through the video
// useOverlay = directly write to the screen.
bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scale)
{
close();
this->fname = fname;
refreshWait = 3;
refreshCount = -1;
doLoop = loop;
ResourceID resource(std::string("Video/") + fname, EResType::VIDEO);
if (!CResourceHandler::get()->existsResource(resource))
{
logGlobal->error("Error: video %s was not found", resource.getName());
return false;
}
data = CResourceHandler::get()->load(resource);
static const int BUFFER_SIZE = 4096;
unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek);
format = avformat_alloc_context();
format->pb = context;
// filename is not needed - file was already open and stored in this->data;
int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr);
if (avfopen != 0)
{
return false;
}
// Retrieve stream information
if (avformat_find_stream_info(format, nullptr) < 0)
return false;
// Find the first video stream
stream = -1;
for(ui32 i=0; i<format->nb_streams; i++)
{
if (format->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
stream = i;
break;
}
}
if (stream < 0)
// No video stream in that file
return false;
// Get a pointer to the codec context for the video stream
codecContext = format->streams[stream]->codec;
// Find the decoder for the video stream
codec = avcodec_find_decoder(codecContext->codec_id);
if (codec == nullptr)
{
// Unsupported codec
return false;
}
// Open codec
if ( avcodec_open2(codecContext, codec, nullptr) < 0 )
{
// Could not open codec
codec = nullptr;
return false;
}
// Allocate video frame
frame = av_frame_alloc();
//setup scaling
if(scale)
{
pos.w = screen->w;
pos.h = screen->h;
}
else
{
pos.w = codecContext->width;
pos.h = codecContext->height;
}
// Allocate a place to put our YUV image on that screen
if (useOverlay)
{
texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
}
else
{
dest = CSDL_Ext::newSurface(pos.w, pos.h);
destRect.x = destRect.y = 0;
destRect.w = pos.w;
destRect.h = pos.h;
}
if (texture == nullptr && dest == nullptr)
return false;
if (texture)
{ // Convert the image into YUV format that SDL uses
sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
pos.w, pos.h,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC, nullptr, nullptr, nullptr);
}
else
{
AVPixelFormat screenFormat = AV_PIX_FMT_NONE;
if (screen->format->Bshift > screen->format->Rshift)
{
// this a BGR surface
switch (screen->format->BytesPerPixel)
{
case 2: screenFormat = AV_PIX_FMT_BGR565; break;
case 3: screenFormat = AV_PIX_FMT_BGR24; break;
case 4: screenFormat = AV_PIX_FMT_BGR32; break;
default: return false;
}
}
else
{
// this a RGB surface
switch (screen->format->BytesPerPixel)
{
case 2: screenFormat = AV_PIX_FMT_RGB565; break;
case 3: screenFormat = AV_PIX_FMT_RGB24; break;
case 4: screenFormat = AV_PIX_FMT_RGB32; break;
default: return false;
}
}
sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
pos.w, pos.h, screenFormat,
SWS_BICUBIC, nullptr, nullptr, nullptr);
}
if (sws == nullptr)
return false;
return true;
}
// Read the next frame. Return false on error/end of file.
bool CVideoPlayer::nextFrame()
{
AVPacket packet;
int frameFinished = 0;
bool gotError = false;
if (sws == nullptr)
return false;
while(!frameFinished)
{
int ret = av_read_frame(format, &packet);
if (ret < 0)
{
// Error. It's probably an end of file.
if (doLoop && !gotError)
{
// Rewind
if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0)
break;
gotError = true;
}
else
{
break;
}
}
else
{
// Is this a packet from the video stream?
if (packet.stream_index == stream)
{
// Decode video frame
avcodec_decode_video2(codecContext, frame, &frameFinished, &packet);
// Did we get a video frame?
if (frameFinished)
{
AVPicture pict;
if (texture) {
avpicture_alloc(&pict, AV_PIX_FMT_YUV420P, pos.w, pos.h);
sws_scale(sws, frame->data, frame->linesize,
0, codecContext->height, pict.data, pict.linesize);
SDL_UpdateYUVTexture(texture, NULL, pict.data[0], pict.linesize[0],
pict.data[1], pict.linesize[1],
pict.data[2], pict.linesize[2]);
avpicture_free(&pict);
}
else
{
/* Avoid buffer overflow caused by sws_scale():
* http://trac.ffmpeg.org/ticket/9254
* Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
* has a few requirements for target data buffers on rescaling:
* 1. buffer has to be aligned to be usable for SIMD instructions
* 2. buffer has to be padded to allow small overflow by SIMD instructions
* Unfortunately SDL_Surface does not provide these guarantees.
* This means that atempt to rescale directly into SDL surface causes
* memory corruption. Usually it happens on campaign selection screen
* where short video moves start spinning on mouse hover.
*
* To fix [1.] we use av_malloc() for memory allocation.
* To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
* We have to use intermdiate buffer and then use memcpy() to land it
* to SDL_Surface.
*/
size_t pic_bytes = dest->pitch * dest->h;
size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
void * for_sws = av_malloc (pic_bytes + ffmped_pad);
pict.data[0] = (ui8 *)for_sws;
pict.linesize[0] = dest->pitch;
sws_scale(sws, frame->data, frame->linesize,
0, codecContext->height, pict.data, pict.linesize);
memcpy(dest->pixels, for_sws, pic_bytes);
av_free(for_sws);
}
}
}
av_free_packet(&packet);
}
}
return frameFinished != 0;
}
void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update )
{
if (sws == nullptr)
return;
pos.x = x;
pos.y = y;
CSDL_Ext::blitSurface(dest, &destRect, dst, &pos);
if (update)
SDL_UpdateRect(dst, pos.x, pos.y, pos.w, pos.h);
}
void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update )
{
show(x, y, dst, update);
}
void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update )
{
if (sws == nullptr)
return;
if (refreshCount <= 0)
{
refreshCount = refreshWait;
if (nextFrame())
show(x,y,dst,update);
else
{
open(fname);
nextFrame();
// The y position is wrong at the first frame.
// Note: either the windows player or the linux player is
// broken. Compensate here until the bug is found.
show(x, y--, dst, update);
}
}
else
{
redraw(x, y, dst, update);
}
refreshCount --;
}
void CVideoPlayer::close()
{
fname = "";
if (sws)
{
sws_freeContext(sws);
sws = nullptr;
}
if (texture)
{
SDL_DestroyTexture(texture);
texture = nullptr;
}
if (dest)
{
SDL_FreeSurface(dest);
dest = nullptr;
}
if (frame)
{
av_frame_free(&frame);//will be set to null
}
if (codec)
{
avcodec_close(codecContext);
codec = nullptr;
codecContext = nullptr;
}
if (format)
{
avformat_close_input(&format);
}
if (context)
{
av_free(context);
context = nullptr;
}
}
// Plays a video. Only works for overlays.
bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
{
// Note: either the windows player or the linux player is
// broken. Compensate here until the bug is found.
y--;
pos.x = x;
pos.y = y;
while(nextFrame())
{
if(stopOnKey && keyDown())
return false;
SDL_RenderCopy(mainRenderer, texture, nullptr, &pos);
SDL_RenderPresent(mainRenderer);
// Wait 3 frames
GH.mainFPSmng->framerateDelay();
GH.mainFPSmng->framerateDelay();
GH.mainFPSmng->framerateDelay();
}
return true;
}
bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, bool stopOnKey, bool scale)
{
open(name, false, true, scale);
bool ret = playVideo(x, y, stopOnKey);
close();
return ret;
}
CVideoPlayer::~CVideoPlayer()
{
close();
}
#endif