fix stack overread on truncated range in cpulist parse_entry - #431
fix stack overread on truncated range in cpulist parse_entry#431soma0212 wants to merge 1 commit into
Conversation
|
Hi @soma0212! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Do you have an example of this actually occuring? |
|
Not from a stock kernel, no. The cpu lists real sysfs emits are short and comma separated, so I don't have a device in the wild hitting this. I found it while exercising the parser's buffer refill boundary under ASan: the trigger is a cpu list file whose first 256 bytes (BUFFER_SIZE) contain no ',' and end in '-'. In that case the whole buffer is reparsed as a single entry with entry_end == &buffer[256], parse_number consumes nothing after the '-', and the warning branch reads buffer[256]. The read happens at any CPUINFO_LOG_LEVEL because cpuinfo_log_warning is a variadic function, so its arguments are evaluated even when the message is filtered out. Standalone repro against unpatched main (66ee79c): #include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <linux/api.h>
static bool cb(uint32_t start, uint32_t end, void* ctx) {
printf("callback: [%u, %u)\n", start, end);
return true;
}
int main(void) {
/* 255 digits followed by '-' == exactly BUFFER_SIZE (256) bytes, no comma,
* so parse_entry() is called with entry_end == &buffer[256]. */
char content[257];
memset(content, '0', 255);
content[255] = '-';
content[256] = '\0';
const char* path = "/tmp/cpuinfo_repro_cpulist.txt";
FILE* f = fopen(path, "wb");
fwrite(content, 1, 256, f);
fclose(f);
bool ok = cpuinfo_linux_parse_cpulist(path, cb, NULL);
printf("parse_cpulist -> %d\n", (int)ok);
return 0;
}ASan reports a 1-byte stack-buffer-overflow READ at cpulist.c:119 in parse_entry, with 'buffer' at frame offsets [32, 288) and the access at offset 288. With this patch the same harness runs clean and the entry is rejected with the usual warning, so I'd file it under hardening the parse boundary rather than something a real machine produces today. I also checked the callback sequence and return value stay byte identical across a set of valid and malformed lists ("0-3", "0-3,8-11", "3-1", "5-", "5-,7", etc). |
|
Not from a real kernel, no. The /sys cpu-list files never carry a 255-digit number, so this won't fire on healthy hardware. It's reachable through the parser itself though: when a full 256-byte read turns up no comma, Smallest trigger is a cpu-list file of exactly 256 bytes with no comma and a trailing #include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <linux/api.h>
static bool cb(uint32_t start, uint32_t end, void* ctx) {
printf("callback: [%u, %u)\n", start, end);
return true;
}
int main(void) {
/* 255 digits + '-' == exactly BUFFER_SIZE (256) bytes, no comma,
* so parse_entry() runs with entry_end == &buffer[256]. */
char content[256];
memset(content, '0', 255);
content[255] = '-';
const char* path = "/tmp/cpuinfo_repro_cpulist.txt";
FILE* f = fopen(path, "wb");
fwrite(content, 1, 256, f);
fclose(f);
printf("parse_cpulist -> %d\n", (int)cpuinfo_linux_parse_cpulist(path, cb, NULL));
return 0;
}Before (66ee79c): After the patch it's clean under ASan, logs There's no cpulist unit test in the tree today so I left this out of the PR, but I'm happy to wire the case into the mock setup if you want it covered. |
parse_entry dereferences number_start without checking that anything actually follows the '-':
Bailing out before the dereference leaves the entry rejected exactly as it already was, so the callback sequence and return value are the same on every input I tried, valid or malformed.