Run 1
October 20, 00:37 to 01:15 ET
50 of 100 completed
- 49 tasks: Lost the connection to Token Factory twice
- 1 task: Sandbox output could not be decoded
Reproof ran 100 real OSS-Fuzz crashes from ARVO through the vulnerable and the fixed build in Nebius sandboxes: 10 from the earlier 10-bug evaluation and 90 drawn at random by a rule committed before any of them ran. Of the 100:
$0.32 for all 100, about $0.0032 a bug, model and sandboxes together. 38 tasks did not run (the Nebius sandbox API answered HTTP 500) and still count against the 100.
Run 1 lost its last tasks to a dropped model connection. Before run 2 started, a plan was committed saying what would change, that run 2 would be published whatever it showed, and that there would be no run 3. Run 2 then hit a different failure, and those tasks count against it too.
42 tasks completed in both runs. All 42 got the same verdict both times.
October 20, 00:37 to 01:15 ET
50 of 100 completed
October 20, 01:58 to 02:26 ET
62 of 100 completed
Plan committed in 0383504 at 01:57 ET, before run 2 started. Read the plan
The 10-task historical convenience sample plus 90 prepared tasks from the pinned ARVO population in random.Random(20261019) draw order, with a 1,500,000,000-byte per-image cap and a three-task combined project cap. Prepare failures are recorded and the draw continues. Every prepared task remains in the run denominator.
1 drawn bug failed to prepare (ARVO 42487077, rnp, slice_exception), and the draw went on, as registered.
For the 10 historical bugs, Nemotron 3 Super got the report, the sanitizer output and the source around the crash, and wrote exact-text edits. Each edit was applied inside the bug's ARVO image, the project was rebuilt with arvo compile, and the stored input was run again, with up to 3 attempts and the build error or the new crash sent back each time. Code decided the result, by a rule committed before each run. Run 2 is run 1 with the harness defect below fixed; both are shown as recorded.
Every patch here was proposed and proven offline, in a local Docker container that was deleted afterwards. None was applied upstream, filed, or sent to a project.
October 21, 15:21 to 15:42 UTC. Every attempt rebuilt in the container that had already run the control build.
2 of 10 bugs got a patch that rebuilt and no longer crashed on the stored input.
29 model calls. Proven on attempt 2 and 3 of 3. $0.0731 on Token Factory for all 10, against a $3 cap.
2 proven, 8 not proven. Proven: jq ARVO 42530604 on attempt 2; libplist ARVO 42508390 on attempt 3.
Found during the run: 12 of the 29 attempts never tested their edit. In 5 images the build script makes a directory with plain mkdir, so its second run in one container stops, and the harness had already run it once as a control. Those tasks still count as not proven in run 1, under its registered rule. Only 8 attempts, in 4 tasks, built an edit; 2 of them were proven. Read the notes
Rule committed in bc162af before the run. Read the rule
October 21, 16:32 to 16:52 UTC. Every attempt rebuilt in a fresh container from a snapshot taken before the control build.
2 of 10 bugs got a patch that rebuilt and no longer crashed on the stored input.
28 model calls. Proven on attempt 3 and 1 of 3. $0.0914 on Token Factory for all 10, against a $3 cap.
2 proven, 8 not proven. Proven: libplist ARVO 42507851 on attempt 3; libplist ARVO 42508390 on attempt 1.
No attempt stopped at a build script's plain mkdir. 14 of the 28 attempts, in 7 tasks, built an edit. Read the notes
Rule committed in bbe6e9a before the run. Read the rule
Run 1PROVEN
1: edit did not apply · 2: proven
2 of 3 calls · $0.00598 · Run 1 card
What the edit does: Makes the buffer for a number's text in jv.c bigger: digits + 14 bytes becomes digits + 25. The upstream fix made it digits + 15.
Nemotron's reason: The previous attempt failed because the exact text to replace was not found in the provided source. Upon re-examining the source block for `/src/jq/src/jv.c`, the correct location of the allocation for `literal_data` is within the function `jvp_literal_number_literal`. The code there allocates memory using `jvp_dec_number_ptr(n)->digits + 14`, which is insufficient for large exponents, causing a heap buffer overflow in `decToString`. The fix increases this allocation size to `digits + 25` to safely accommodate the worst-case output of `decNumberToString`, including space for the exponent (up to 10 digits), sign, 'E', and null terminator.
--- a/src/jq/src/jv.c
+++ b/src/jq/src/jv.c
@@ -644,7 +644,7 @@
}
if (plit->literal_data == NULL) {
- int len = jvp_dec_number_ptr(n)->digits + 14;
+ int len = jvp_dec_number_ptr(n)->digits + 25;
plit->literal_data = jv_mem_alloc(len);
// Preserve the actual precision as we have parsed it
Checked by: PoC only: the image has no seed corpus.
Compare with the upstream fix, 71c2ab5. Proven means it rebuilt and stopped the crash, not that it is the upstream fix or safe to merge.
Run 2NOT PROVEN
1: no usable reply · 2: edit did not apply · 3: edit did not apply
3 of 3 calls · $0.01460 · Run 2 card
Run 1NOT PROVEN
1: still crashed · 2: still crashed · 3: edit did not apply
3 of 3 calls · $0.00797 · Run 1 card
Run 2PROVEN
1: still crashed · 2: seed corpus failed · 3: proven
3 of 3 calls · $0.00722 · Run 2 card
What the edit does: Copies the number into a new buffer that ends in a NUL byte and runs strtol and the float checks on the copy, so parsing stops at the end of the token. Floats still go to atof on the original text.
Nemotron's reason: The previous fix introduced a use-after-free because it freed the temporary buffer before using `endp` in the subsequent checks. The variable `endp` is a pointer into the temporarily allocated buffer, so freeing the buffer before checking `endp` leads to undefined behavior when `endp` is later dereferenced. The fix is to delay the free of the temporary buffer until after all uses of `endp` are complete. We will allocate a null-terminated copy of the string, use it for `strtol`, and then free it only after we have finished using `endp` for any further checks (such as the float detection logic).
--- a/src/libplist/src/jplist.c
+++ b/src/libplist/src/jplist.c
@@ -437,16 +437,23 @@
data->type = PLIST_NULL;
val = plist_new_node(data);
} else if (str_val[0] == '-' || isdigit(str_val[0])) {
+ char* buf = malloc(str_len + 1);
+ if (!buf) {
+ PLIST_JSON_ERR("%s: Out of memory\n", __func__);
+ return NULL;
+ }
+ memcpy(buf, str_val, str_len);
+ buf[str_len] = '\0';
char* endp = NULL;
- long long intpart = strtol(str_val, &endp, 10);
- if (endp >= str_end) {
+ long long intpart = strtol(buf, &endp, 10);
+ if (endp >= buf + str_len) {
/* integer */
val = plist_new_uint((uint64_t)intpart);
- } else if (*endp == '.' && endp+1 < str_end && isdigit(*(endp+1))) {
+ } else if (*endp == '.' && endp+1 < buf + str_len && isdigit(*(endp+1))) {
/* float */
char* fendp = endp+1;
- while (isdigit(*fendp) && fendp < str_end) fendp++;
- if ((fendp > endp+1 && fendp >= str_end) || (fendp+2 < str_end && (*fendp == 'e' || *fendp == 'E') && (*(fendp+1) == '+' || *(fendp+1) == '-') && isdigit(*(fendp+2)))) {
+ while (isdigit(*fendp) && fendp < buf + str_len) fendp++;
+ if ((fendp > endp+1 && fendp >= buf + str_len) || (fendp+2 < buf + str_len && (*fendp == 'e' || *fendp == 'E') && (*(fendp+1) == '+' || *(fendp+1) == '-') && isdigit(*(fendp+2)))) {
double dval = atof(str_val);
val = plist_new_real(dval);
} else {
@@ -455,6 +462,7 @@
} else {
PLIST_JSON_ERR("%s: invalid character at offset %d when parsing numerical value\n", __func__, (int)(endp - js));
}
+ free(buf);
} else {
PLIST_JSON_ERR("%s: invalid primitive value '%.*s' encountered\n", __func__, (int)str_len, str_val);
}
Checked by: seed corpus, 2 files.
Compare with the upstream fix, 8ea84ca. Proven means it rebuilt and stopped the crash, not that it is the upstream fix or safe to merge.
Run 1NOT PROVEN
1: build failed · 2: build failed · 3: build failed
3 of 3 calls · $0.00645 · Run 1 card
Attempts 1, 2 and 3 never tested the edit: the build script stopped at "mkdir: cannot create directory 'build': File exists" because the control build had already run it.
Run 2NOT PROVEN
1: still crashed · 2: no usable reply · 3: still crashed
3 of 3 calls · $0.01199 · Run 2 card
Run 1NOT PROVEN
1: still crashed · 2: still crashed · 3: still crashed
3 of 3 calls · $0.00554 · Run 1 card
Run 2NOT PROVEN
1: edit did not apply · 2: edit did not apply · 3: still crashed
3 of 3 calls · $0.00910 · Run 2 card
Run 1NOT PROVEN
1: edit did not apply · 2: build failed · 3: build failed
3 of 3 calls · $0.00619 · Run 1 card
Attempts 2 and 3 never tested the edit: the build script stopped at "mkdir: cannot create directory 'build': File exists" because the control build had already run it.
Run 2NOT PROVEN
1: edit did not apply · 2: still crashed · 3: no usable reply
3 of 3 calls · $0.00964 · Run 2 card
Run 1NOT PROVEN
1: build failed · 2: no usable reply · 3: build failed
3 of 3 calls · $0.01059 · Run 1 card
Attempts 1 and 3 never tested the edit: the build script stopped at "mkdir: cannot create directory 'build': File exists" because the control build had already run it.
Run 2NOT PROVEN
1: edit did not apply · 2: no usable reply · 3: edit did not apply
3 of 3 calls · $0.01018 · Run 2 card
Run 1PROVEN
1: still crashed · 2: build failed · 3: proven
3 of 3 calls · $0.00808 · Run 1 card
What the edit does: Rewrites the JSON number parser in jplist.c by hand, reading digits in a loop that stops at the end of the token instead of calling strtol. Floats still go to atof.
Nemotron's reason: The previous attempt had syntax errors due to incorrect JSON structure in the edit description, particularly mismatched quotes and braces. The fix must be applied cleanly within the existing function without introducing control-flow errors. We now provide a corrected version that replaces only the unsafe numeric parsing block with a safe, bounds-aware implementation using manual digit parsing, while preserving the original function structure and avoiding any use of `break` outside loops/switches.
--- a/src/libplist/src/jplist.c
+++ b/src/libplist/src/jplist.c
@@ -437,23 +437,77 @@
data->type = PLIST_NULL;
val = plist_new_node(data);
} else if (str_val[0] == '-' || isdigit(str_val[0])) {
- char* endp = NULL;
- long long intpart = strtol(str_val, &endp, 10);
- if (endp >= str_end) {
- /* integer */
- val = plist_new_uint((uint64_t)intpart);
- } else if (*endp == '.' && endp+1 < str_end && isdigit(*(endp+1))) {
- /* float */
- char* fendp = endp+1;
- while (isdigit(*fendp) && fendp < str_end) fendp++;
- if ((fendp > endp+1 && fendp >= str_end) || (fendp+2 < str_end && (*fendp == 'e' || *fendp == 'E') && (*(fendp+1) == '+' || *(fendp+1) == '-') && isdigit(*(fendp+2)))) {
- double dval = atof(str_val);
- val = plist_new_real(dval);
+ const char* p = str_val;
+ int neg = 0;
+ if (*p == '-') {
+ neg = 1;
+ p++;
+ if (p >= str_end) {
+ PLIST_JSON_ERR("%s: invalid number (minus without digits)\n", __func__);
} else {
- PLIST_JSON_ERR("%s: invalid character at offset %d when parsing floating point value\n", __func__, (int)(fendp - js));
+ if (!isdigit(*p)) {
+ PLIST_JSON_ERR("%s: invalid number (expected digit after minus)\n", __func__);
+ } else {
+ long long intpart = 0;
+ while (p < str_end && isdigit(*p)) {
+ intpart = intpart * 10 + (*p - '0');
+ p++;
+ }
+ if (p >= str_end) {
+ if (neg) intpart = -intpart;
+ val = plist_new_uint((uint64_t)intpart);
+ } else if (*p == '.' && p+1 < str_end && isdigit(*(p+1))) {
+ p++;
+ while (p < str_end && isdigit(*p)) p++;
+ if (p < str_end && (*p == 'e' || *p == 'E')) {
+ p++;
+ if (p < str_end && (*p == '+' || *p == '-')) p++;
+ if (p < str_end && isdigit(*p)) {
+ while (p < str_end && isdigit(*p)) p++;
+ } else {
+ PLIST_JSON_ERR("%s: invalid exponent in float\n", __func__);
+ }
+ }
+ if (p >= str_end) {
+ double dval = atof(str_val);
+ val = plist_new_real(dval);
+ } else {
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing float\n", __func__, (int)(p - js));
+ }
+ } else {
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing number\n", __func__, (int)(p - js));
+ }
+ }
}
} else {
- PLIST_JSON_ERR("%s: invalid character at offset %d when parsing numerical value\n", __func__, (int)(endp - js));
+ long long intpart = 0;
+ while (p < str_end && isdigit(*p)) {
+ intpart = intpart * 10 + (*p - '0');
+ p++;
+ }
+ if (p >= str_end) {
+ val = plist_new_uint((uint64_t)intpart);
+ } else if (*p == '.' && p+1 < str_end && isdigit(*(p+1))) {
+ p++;
+ while (p < str_end && isdigit(*p)) p++;
+ if (p < str_end && (*p == 'e' || *p == 'E')) {
+ p++;
+ if (p < str_end && (*p == '+' || *p == '-')) p++;
+ if (p < str_end && isdigit(*p)) {
+ while (p < str_end && isdigit(*p)) p++;
+ } else {
+ PLIST_JSON_ERR("%s: invalid exponent in float\n", __func__);
+ }
+ }
+ if (p >= str_end) {
+ double dval = atof(str_val);
+ val = plist_new_real(dval);
+ } else {
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing float\n", __func__, (int)(p - js));
+ }
+ } else {
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing number\n", __func__, (int)(p - js));
+ }
}
} else {
PLIST_JSON_ERR("%s: invalid primitive value '%.*s' encountered\n", __func__, (int)str_len, str_val);
Checked by: seed corpus, 2 files.
Compare with the upstream fix, 7d2cdc6. Proven means it rebuilt and stopped the crash, not that it is the upstream fix or safe to merge.
Run 2PROVEN
1: proven
1 of 3 calls · $0.00184 · Run 2 card
What the edit does: Rejects a number that is a lone minus sign, or a minus sign not followed by a digit, before strtol runs. The rest of the parser is unchanged, only indented one level deeper.
Nemotron's reason: The issue occurs because `strtol` reads beyond the bounds of the allocated string when processing numeric primitives. Specifically, when `strtol` parses a number like `-`, it may read past the null terminator into adjacent memory due to insufficient validation before calling `strtol`. The fix adds a check to ensure there is at least one digit following a minus sign before attempting conversion.
--- a/src/libplist/src/jplist.c
+++ b/src/libplist/src/jplist.c
@@ -437,23 +437,31 @@
data->type = PLIST_NULL;
val = plist_new_node(data);
} else if (str_val[0] == '-' || isdigit(str_val[0])) {
- char* endp = NULL;
- long long intpart = strtol(str_val, &endp, 10);
- if (endp >= str_end) {
- /* integer */
- val = plist_new_uint((uint64_t)intpart);
- } else if (*endp == '.' && endp+1 < str_end && isdigit(*(endp+1))) {
- /* float */
- char* fendp = endp+1;
- while (isdigit(*fendp) && fendp < str_end) fendp++;
- if ((fendp > endp+1 && fendp >= str_end) || (fendp+2 < str_end && (*fendp == 'e' || *fendp == 'E') && (*(fendp+1) == '+' || *(fendp+1) == '-') && isdigit(*(fendp+2)))) {
- double dval = atof(str_val);
- val = plist_new_real(dval);
+ /* Ensure we have at least one digit after optional minus sign */
+ if (str_val[0] == '-' && (str_end - str_val < 2 || !isdigit(str_val[1])))
+ {
+ PLIST_JSON_ERR("%s: invalid primitive value '%.*s' encountered\n", __func__, (int)str_len, str_val);
+ }
+ else
+ {
+ char* endp = NULL;
+ long long intpart = strtol(str_val, &endp, 10);
+ if (endp >= str_end) {
+ /* integer */
+ val = plist_new_uint((uint64_t)intpart);
+ } else if (*endp == '.' && endp+1 < str_end && isdigit(*(endp+1))) {
+ /* float */
+ char* fendp = endp+1;
+ while (isdigit(*fendp) && fendp < str_end) fendp++;
+ if ((fendp > endp+1 && fendp >= str_end) || (fendp+2 < str_end && (*fendp == 'e' || *fendp == 'E') && (*(fendp+1) == '+' || *(fendp+1) == '-') && isdigit(*(fendp+2)))) {
+ double dval = atof(str_val);
+ val = plist_new_real(dval);
+ } else {
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing floating point value\n", __func__, (int)(fendp - js));
+ }
} else {
- PLIST_JSON_ERR("%s: invalid character at offset %d when parsing floating point value\n", __func__, (int)(fendp - js));
- }
- } else {
- PLIST_JSON_ERR("%s: invalid character at offset %d when parsing numerical value\n", __func__, (int)(endp - js));
+ PLIST_JSON_ERR("%s: invalid character at offset %d when parsing numerical value\n", __func__, (int)(endp - js));
+ }
}
} else {
PLIST_JSON_ERR("%s: invalid primitive value '%.*s' encountered\n", __func__, (int)str_len, str_val);
Checked by: seed corpus, 2 files.
Compare with the upstream fix, 7d2cdc6. Proven means it rebuilt and stopped the crash, not that it is the upstream fix or safe to merge.
Run 1NOT PROVEN
1: edit did not apply · 2: build failed · 3: build failed
3 of 3 calls · $0.00856 · Run 1 card
Attempts 2 and 3 never tested the edit: the build script stopped at "mkdir: cannot create directory '/src/seeds': File exists" because the control build had already run it.
Run 2NOT PROVEN
1: still crashed · 2: still crashed · 3: still crashed
3 of 3 calls · $0.01014 · Run 2 card
Run 1NOT PROVEN
1: edit did not apply · 2: edit did not apply · 3: edit did not apply
3 of 3 calls · $0.00926 · Run 1 card
Run 2NOT PROVEN
1: edit did not apply · 2: edit did not apply · 3: edit did not apply
3 of 3 calls · $0.00956 · Run 2 card
Run 1NOT PROVEN
1: build failed · 2: build failed · 3: build failed
3 of 3 calls · $0.00452 · Run 1 card
Attempts 1, 2 and 3 never tested the edit: the build script stopped at "mkdir: cannot create directory 'build': File exists" because the control build had already run it.
Run 2NOT PROVEN
1: still crashed · 2: still crashed · 3: still crashed
3 of 3 calls · $0.00708 · Run 2 card
All 100 from run 2. The 67 that did not run or missed a check come first, each with the reason. Every task that ran links to the card the run wrote.