YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Paddle-Lite naive_buffer/.nb loader -- unchecked prog_size OOB read
Target pin: 28fe23f222de1865e01f0ab41d2494a1222be4f0 (tag v2.14-rc, PaddlePaddle/Paddle-Lite)
Sink (source-confirmed at pin)
lite/model_parser/model_parser.cc:990-996 (LoadModelFbsFromMemory):
uint64_t prog_size;
reader->Read(&prog_size, sizeof(uint64_t)); // attacker-controlled, unvalidated
model_parser::Buffer prog_data(prog_size); // CWE-789: eager alloc of attacker-chosen size
reader->Read(prog_data.data(), prog_size); // CWE-125: no check vs remaining buffer length
lite/core/model/base/io.cc:72-76 (CharBufferReader::Read, the reader used for the in-memory .nb load path):
void CharBufferReader::Read(void* dst, size_t size) const {
CHECK(dst);
lite::TargetCopy(TargetType::kHost, dst, buf_ + cur_, size); // memcpy-equivalent
cur_ += size; // NO check that cur_+size <= length_
}
There is no bounds check anywhere between the untrusted prog_size read at model_parser.cc:991 and the raw copy at model_parser.cc:996 / io.cc:74. prog_size is a raw uint64_t taken directly from attacker-supplied model bytes.
Reachable surface
MobileConfig::set_model_from_buffer(const std::string&) (lite/api/paddle_api.cc:815) stores the entire input as lite_model_file_ with model_from_memory_=true. LightPredictorImpl::Init (lite/api/light_api_impl.cc:35) sees !lite_model_file().empty() && is_model_from_memory()==true and constructs LightPredictor(lite_model_file().c_str(), lite_model_file().length(), ...) (lite/api/light_api_impl.cc:47-49) -> LightPredictor::Build(const char*, size_t) (lite/api/light_api.cc:59-73) -> LoadModelNaiveFromMemory -> for meta_version==1 -> LoadModelFbsFromMemory -> sink above.
Note: this is the sibling in-memory entry point of set_model_from_file (the file-based set_model_from_file path uses BinaryFileReader, whose Read() calls fread + CHECK_EQ, which aborts cleanly on truncated reads instead of reading OOB -- see io.cc:52-57). The pure memory-safety OOB read requires the buffer entry point, set_model_from_buffer, which is the documented, commonly used sibling API (e.g. for models bundled as in-app resources/assets rather than loose files) and exists on every MobileConfig consumer, mobile or Python.
PoC
Payload (47 bytes total): meta_version=1 (2B) + opt_version (16B, ignored)
- prog_size=0x40000000 (1 GiB, 8B LE) + 21 bytes of real trailing content. prog_size is 1073741803 bytes larger than the real remaining buffer.
Verified against the pip paddlelite==2.13rc0 wheel (pip3 install --user paddlelite), after confirming via 'git diff v2.13-rc 28fe23f2 -- lite/model_parser/model_parser.cc lite/core/model/base/io.cc' that both sink files are byte-identical between the wheel's version tag (v2.13-rc) and the requested pin (v2.14-rc / 28fe23f2).
Negative control (poc_negctrl.py)
A truthful prog_size (matches the real remaining bytes) with garbage flatbuffer content fails cleanly via a deliberate CHECK/LOG(FATAL) in flatbuffer verification (param_desc.h:102, 'CombinedParamsDesc verification failed') -> SIGABRT, exit 134. This is the intended, safe failure mode for malformed-but- honest input.
Exploit (poc_oob_read.py)
Same structure, but prog_size lies about the buffer length. Result: SIGSEGV, exit 139, si_code=SEGV_MAPERR.
strace -f -e trace=memory shows the causal chain directly:
brk(0x5825b5433000) = 0x5825b5433000 # top of heap just before the sink runs
...
mmap(NULL, 1073745920, PROT_READ|PROT_WRITE, ...) = 0x71e7045ff000 # eager alloc of prog_data (CWE-789), succeeds
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x5825b54333aa} ---
The fault address (0x5825b54333aa) is 938 bytes past the immediately preceding brk() boundary (0x5825b5433000) -- i.e. the unbounded TargetCopy read started inside the small (47-byte) heap-resident source buffer and walked forward past the top of the mapped heap into unmapped address space, exactly matching the missing cur_ + size <= length_ check in CharBufferReader::Read. Reproduced twice (strace_oob_read.log), same offset-from-brk each time.
Files
- poc_oob_read.py -- the exploit; run with 'python3 poc_oob_read.py'
- poc_negctrl.py -- negative control (truthful prog_size -> clean CHECK-abort, not memory-unsafe)
- strace_oob_read.log -- strace of the exploit run showing the mmap(prog_data) + SEGV_MAPERR sequence
- negctrl.log -- output of the negative control run (SIGABRT via deliberate CHECK)