2023.09.05
CVE-2021-39623 | A-194105348 | EoP | 高 | 9、10、11、12 |
---|---|---|---|---|
patch
SimpleDecodingSource:Prevent OOB write in heap mem
doRead() doesn't handle situations when received byte do not fit into input buffer in case of vorbis audio compression. It results in OOB write in heap memory right after the allocated input buffer. Added code to copy kKeyValidSamples only if there was enough space. Otherwise, print a warning log.
若cpLen = in_buffer->capacity(),那么在后面的 vorbis 音频压缩的场景下,in_buffer没有空间再用于拷贝kKeyValidSamples(numPageSamples)的值了,于是,发生越界写4个字节。
修复方式:添加了仅在有足够空间时复制 kKeyValidSamples 的代码。 否则,打印警告日志。
Bug: 194105348
Test: post-submit media cts tests

分析
// frameworks/av/media/libstagefright/SimpleDecodingSource.cpp
// 这段代码是一个音视频解码器的读取函数,
// 主要功能是从输入源读取数据并进行解码。
status_t SimpleDecodingSource::doRead(
Mutexed<ProtectedState>::Locked &me, MediaBufferBase **buffer, const ReadOptions *options) {
// |me| is always locked on entry, but is allowed to be unlocked on exit
// 【1】首先检查当前状态是否是STARTED状态,如果不是则返回错误。
CHECK_EQ(me->mState, STARTED);
size_t out_ix, in_ix, out_offset, out_size;
int64_t out_pts;
uint32_t out_flags;
status_t res;
// flush codec on seek
// 【2】如果传入的options参数不为空,且包含seek操作,那么在进行seek操作前需要先flush解码器。
MediaSource::ReadOptions::SeekMode mode;
if (options != NULL && options->getSeekTo(&out_pts, &mode)) {
me->mQueuedInputEOS = false;
me->mGotOutputEOS = false;
mCodec->flush();
}
// 【3】如果已经获取到了输出的EOS(End of Stream)标志,说明已经读取到了输入源的末尾,直接返回ERROR_END_OF_STREAM。
if (me->mGotOutputEOS) {
return ERROR_END_OF_STREAM;
}
// 【4】进入一个循环,最多重试kTimeoutMaxRetries次。
for (int retries = 0; retries < kTimeoutMaxRetries; ++retries) {
// If we fill all available input buffers, we should expect that
// the codec produces at least one output buffer. Also, the codec
// should produce an output buffer in at most 1 seconds. Retry a
// few times nonetheless.
// 【4.1】首先判断是否还有可用的输入缓冲区。如果没有则退出循环。
while (!me->mQueuedInputEOS) {
// allow some time to get input buffer after flush
// 【4.2】获取一个可用的输入缓冲区in_buffer,并将输入数据读取到该缓冲区in_buffer中。
res = mCodec->dequeueInputBuffer(&in_ix, kTimeoutWaitForInputUs);
if (res == -EAGAIN) {
// no available input buffers
break;
}
sp<MediaCodecBuffer> in_buffer;
if (res == OK) {
// 调用 mCodec 对象的 getInputBuffer 方法来获取指定索引的输入缓冲区,并将其地址存储在 in_buffer 变量中
res = mCodec->getInputBuffer(in_ix, &in_buffer);
}
// 【4.3】如果读取失败,将状态设置为ERROR并返回UNKNOWN_ERROR。
if (res != OK || in_buffer == NULL) {
ALOGW("[%s] could not get input buffer #%zu",
mComponentName.c_str(), in_ix);
me->mState = ERROR;
return UNKNOWN_ERROR;
}
// 【4.4】如果读取成功,将读取到的数据拷贝到解码器的输入缓冲区in_buf中,
// 并将输入缓冲区提交给解码器进行解码。
MediaBufferBase *in_buf;
while (true) {
in_buf = NULL;
me.unlock();
res = mSource->read(&in_buf, options);
me.lock();
if (res != OK || me->mState != STARTED) {
if (in_buf != NULL) {
in_buf->release();
in_buf = NULL;
}
// queue EOS
me->mQueuedInputEOS = true;
if (mCodec->queueInputBuffer(
in_ix, 0 /* offset */, 0 /* size */,
0 /* pts */, MediaCodec::BUFFER_FLAG_EOS) != OK) {
ALOGI("[%s] failed to queue input EOS", mComponentName.c_str());
me->mState = ERROR;
return UNKNOWN_ERROR;
}
// don't stop on EOS, but report error or EOS on stop
if (res != ERROR_END_OF_STREAM) {
me->mState = ERROR;
return res;
}
if (me->mState != STARTED) {
return ERROR_END_OF_STREAM;
}
break;
}
if (in_buf == NULL) { // should not happen
continue;
} else if (in_buf->range_length() != 0) {
break;
}
in_buf->release();
}
// 判断 in_buf 是否为 NULL。如果不为 NULL,继续执行下面的逻辑。
if (in_buf != NULL) {
// 使用 meta_data() 方法从in_buf的元数据中查找名为 kKeyTime 的整数值,并将其存储在 timestampUs 变量中
int64_t timestampUs = 0;
CHECK(in_buf->meta_data().findInt64(kKeyTime, ×tampUs));
// 判断in_buf的有效数据长度(range_length())加上特定条件(如果是 Vorbis 格式,再加上 4 字节)是否超过了in_buffer的容量(capacity())。
// 如果超过了容量,代码会打印一个警告信息。
if (in_buf->range_length() + (mIsVorbis ? 4 : 0) > in_buffer->capacity()) {
ALOGW("'%s' received %zu input bytes for buffer of size %zu",
mComponentName.c_str(),
in_buf->range_length() + (mIsVorbis ? 4 : 0), in_buffer->capacity());
}
// 计算需要拷贝的数据长度 cpLen,取in_buf的有效数据长度和in_buffer的容量 这两者中的较小者
size_t cpLen = min(in_buf->range_length(), in_buffer->capacity()); // 【漏洞分析1】 cpLen是这两者中的最小者,如果cpLen = in_buffer->capacity()
// 将 in_buf 的数据拷贝到in_buffer 的基地址处,拷贝的数据长度为 cpLen 字节
memcpy(in_buffer->base(), (uint8_t *)in_buf->data() + in_buf->range_offset(),
cpLen );
// 如果输入缓冲区是 Vorbis 格式,代码会继续执行下面的逻辑。
if (mIsVorbis) {
int32_t numPageSamples;
// 使用 in_buf 对象的 meta_data() 方法来获取in_buf的元数据,并从中查找名为 kKeyValidSamples 的整数值,并将其存储在 numPageSamples 变量中
if (!in_buf->meta_data().findInt32(kKeyValidSamples, &numPageSamples)) {
// 如果找不到该键值,则将 numPageSamples 设置为 -1。
numPageSamples = -1;
}
// dest:in_buffer->base() + cpLen 表达式表示输入缓冲区的基地址加上 cpLen,即获取到从 cpLen 位置开始的一段内存地址
// source:numPageSamples 变量的地址
// 复制的字节数为 sizeof(numPageSamples)
//【漏洞分析2】如果cpLen = in_buffer->capacity(),那么此时in_buffer->base() + cpLen已经指向in_buffer的末尾了
// 那么也就是没有空间再用于拷贝kKeyValidSamples(numPageSamples)的值了。
// 此时就会发生OOB Write
memcpy(in_buffer->base() + cpLen, &numPageSamples, sizeof(numPageSamples)); // 【漏洞点】OOB Write
}
// 调用 mCodec 对象的 queueInputBuffer 方法将输入缓冲区 in_buf 排队到编解码器中。
// 传递给该方法的参数包括输入缓冲区的索引 in_ix、偏移量 offset、长度(输入缓冲区的有效数据长度加上特定条件)和时间戳 timestampUs。
res = mCodec->queueInputBuffer(
in_ix, 0 /* offset */, in_buf->range_length() + (mIsVorbis ? 4 : 0),
timestampUs, 0 /* flags */);
// 检查 queueInputBuffer 方法的返回值 res 是否为 OK。
// 如果不为 OK,代码会打印一个信息,并将当前状态设为 ERROR。
if (res != OK) {
ALOGI("[%s] failed to queue input buffer #%zu", mComponentName.c_str(), in_ix);
me->mState = ERROR;
}
// 调用输入缓冲区的 release 方法释放该缓冲区。
in_buf->release();
}
}
// 【4.5】解码器进行解码后,将解码后的数据放入输出缓冲区中。
me.unlock();
res = mCodec->dequeueOutputBuffer(
&out_ix, &out_offset, &out_size, &out_pts,
&out_flags, kTimeoutWaitForOutputUs /* timeoutUs */);
me.lock();
// abort read on stop
if (me->mState != STARTED) {
if (res == OK) {
mCodec->releaseOutputBuffer(out_ix);
}
return ERROR_END_OF_STREAM;
}
if (res == -EAGAIN) {
ALOGD("[%s] did not produce an output buffer. retry count: %d",
mComponentName.c_str(), retries);
continue;
} else if (res == INFO_FORMAT_CHANGED) {
if (mCodec->getOutputFormat(&me->mFormat) != OK) {
me->mState = ERROR;
res = UNKNOWN_ERROR;
}
return res;
} else if (res == INFO_OUTPUT_BUFFERS_CHANGED) {
ALOGV("output buffers changed");
continue;
} else if (res != OK) {
me->mState = ERROR;
return res;
}
sp<MediaCodecBuffer> out_buffer;
res = mCodec->getOutputBuffer(out_ix, &out_buffer);
if (res != OK) {
ALOGW("[%s] could not get output buffer #%zu",
mComponentName.c_str(), out_ix);
me->mState = ERROR;
return UNKNOWN_ERROR;
}
// 【4.6】如果解码器输出的数据中包含EOS标志,
if (out_flags & MediaCodec::BUFFER_FLAG_EOS) {
me->mGotOutputEOS = true;// 将状态设置为已获取到输出的EOS,
// return EOS immediately if last buffer is empty
if (out_size == 0) {
mCodec->releaseOutputBuffer(out_ix);
return ERROR_END_OF_STREAM; // 并立即返回ERROR_END_OF_STREAM(如果最后一个输出缓冲区为空)。
}
}
// 【4.7】如果使用的是Surface进行渲染,并且输出缓冲区中有数据
if (mUsingSurface && out_size > 0) {
*buffer = new MediaBuffer(0);// 将输出缓冲区的大小设置为0,
mCodec->renderOutputBufferAndRelease(out_ix);// 并调用解码器的renderOutputBufferAndRelease函数释放输出缓冲区。
} else { // 【4.8】如果不使用Surface进行渲染,
// 将输出缓冲区的数据拷贝到新创建的MediaBuffer中
*buffer = new MediaBuffer(out_size);
CHECK_LE(out_buffer->size(), (*buffer)->size());
memcpy((*buffer)->data(), out_buffer->data(), out_buffer->size());
// 并设置相关的元数据信息
(*buffer)->meta_data().setInt64(kKeyTime, out_pts);
// 然后释放输出缓冲区。
mCodec->releaseOutputBuffer(out_ix);
}
return OK; // 【4.9】返回OK表示成功读取并解码一帧数据。
}
return TIMED_OUT; // 【4.10】如果重试次数超过了kTimeoutMaxRetries,则返回TIMED_OUT表示超时。
}