Please acknowledge the following before creating a ticket
Description of the bug:
do_io() declares comp_time without initializing it:
struct timespec comp_time;
It is only ever written on two paths:
wait_for_completions() — called only when the queue is full or when
polling (full || io_in_polling(td)), and
io_queue_event()'s FIO_Q_COMPLETED path — only for inline
completions.
There is a common workload shape that takes neither path before
check_min_rate() reads the variable: an async engine on a block device
with a rate limit and rate_iops_min/ratemin set, e.g.
[global]
ioengine=libaio
rw=randwrite
bs=16k
direct=1
time_based=1
runtime=60
iodepth=16
rate_iops=500
rate_iops_min=500
[job]
filename=/dev/nvme1n1
write_lat_log=job
At 500 IOPS with iodepth=16 the queue is never full, submissions return
FIO_Q_QUEUED, and completions are reaped during the rate-throttle
sleep via io_u_quiesce(), which does not touch comp_time. Once
td->bytes_done is non-zero, the loop bottom calls
check_min_rate(td, &comp_time) and the settle check in
__check_min_rate() computes
if (mtime_since(&td->start, now) < 2000) /* now == &comp_time */
from whatever bytes happened to be on the stack. When the garbage
tv_nsec is out of range, rel_time_since() aborts:
fio: gettime.c:530: rel_time_since: Assertion `0 <= nsec && nsec < 1000ULL * 1000 * 1000' failed.
fio: pid=..., got signal=6
When the garbage happens to be numerically benign, there is no crash but
the settle/rate arithmetic is still computed from garbage.
We hit the crashing variant in CI with fio 3.42 : every run of the
job above SIGABRT'd ~100 ms after start, on every instance,
deterministically (same code path, same stack leftovers). The same
binary and job on a different host runs fine — which is exactly what an
uninitialized stack read predicts. The code is the same in current
master and at least as far back as 3.38; the 3.38 -> 3.42 upgrade merely
changed the stack layout enough to make the leftover bytes poisonous for
this workload.
Backtrace from a production core (fio-3.42, x86_64):
#4 0x000000000040e274 in rel_time_since (...) at gettime.c:530
#5 0x000000000041f148 in rel_time_since (...) at gettime.c:540
#6 mtime_since (...) at gettime.c:541
#7 mtime_since_now (...) at gettime.c:512
#8 time_since_now (...) at gettime.c:546
#9 0x000000000040f562 in __check_min_rate (ddir=DDIR_WRITE, now=0x7ffedd67e580, td=0x7fe7a43ba000) at backend.c:176
#10 check_min_rate (...) at backend.c:226
#11 do_io (...) at backend.c:1315
#12 0x0000000000475c9e in thread_main (...) at backend.c:2096
In the core, td->start is sane and comp_time is stack garbage:
(gdb) frame 11
(gdb) print comp_time
$1 = {tv_sec = 140632831934736, tv_nsec = 4835703278458516699}
(gdb) print td->start
$2 = {tv_sec = 0, tv_nsec = 108700203}
Deterministic reproduction (since the natural trigger depends on
stack leftovers): poison comp_time at do_io() entry with the values
observed in the core,
struct timespec comp_time = { .tv_sec = 140632831934736, .tv_nsec = 4835703278458516699LL };
then run the job file above against any block device (a loop device
works). Reproduces the exact assertion and SIGABRT on the first rate
check, on current master.
Suggested fix:
Minimal crash fix: initialize comp_time at do_io() entry
(fio_gettime(&comp_time, NULL);). During the 2s settle window a stale
value is harmless (mtime_since clamps negative to 0 and the settle
check just stays in the settle period).
Worth noting while fixing: on this workload shape comp_time is never
updated at all (completions are only ever reaped by io_u_quiesce()),
so even with the crash fixed, spent = mtime_since(&td->last_rate_check_time, now)
stays near zero and the min-rate check never fires. If min-rate
enforcement is intended to work with quiesce-reaped completions, the
rate path needs to stamp a completion time too (or check_min_rate()
should take a fresh fio_gettime() when the passed time predates the
last check).
Environment:
Amazon Linux 2023, x86_64 (EC2); also reproduced (with the poison
above) on an Amazon Linux 2 dev host.
fio version:
fio-3.42 (crash observed in production); code identical in current
master and present since at least fio-3.38.
Was blktrace being run in parallel?:
No.
Please acknowledge the following before creating a ticket
Description of the bug:
do_io()declarescomp_timewithout initializing it:It is only ever written on two paths:
wait_for_completions()— called only when the queue is full or whenpolling (
full || io_in_polling(td)), andio_queue_event()'sFIO_Q_COMPLETEDpath — only for inlinecompletions.
There is a common workload shape that takes neither path before
check_min_rate()reads the variable: an async engine on a block devicewith a rate limit and
rate_iops_min/rateminset, e.g.At 500 IOPS with iodepth=16 the queue is never full, submissions return
FIO_Q_QUEUED, and completions are reaped during the rate-throttlesleep via
io_u_quiesce(), which does not touchcomp_time. Oncetd->bytes_doneis non-zero, the loop bottom callscheck_min_rate(td, &comp_time)and the settle check in__check_min_rate()computesfrom whatever bytes happened to be on the stack. When the garbage
tv_nsecis out of range,rel_time_since()aborts:When the garbage happens to be numerically benign, there is no crash but
the settle/rate arithmetic is still computed from garbage.
We hit the crashing variant in CI with fio 3.42 : every run of the
job above SIGABRT'd ~100 ms after start, on every instance,
deterministically (same code path, same stack leftovers). The same
binary and job on a different host runs fine — which is exactly what an
uninitialized stack read predicts. The code is the same in current
master and at least as far back as 3.38; the 3.38 -> 3.42 upgrade merely
changed the stack layout enough to make the leftover bytes poisonous for
this workload.
Backtrace from a production core (fio-3.42, x86_64):
In the core,
td->startis sane andcomp_timeis stack garbage:Deterministic reproduction (since the natural trigger depends on
stack leftovers): poison
comp_timeatdo_io()entry with the valuesobserved in the core,
then run the job file above against any block device (a loop device
works). Reproduces the exact assertion and SIGABRT on the first rate
check, on current master.
Suggested fix:
Minimal crash fix: initialize
comp_timeatdo_io()entry(
fio_gettime(&comp_time, NULL);). During the 2s settle window a stalevalue is harmless (
mtime_sinceclamps negative to 0 and the settlecheck just stays in the settle period).
Worth noting while fixing: on this workload shape
comp_timeis neverupdated at all (completions are only ever reaped by
io_u_quiesce()),so even with the crash fixed,
spent = mtime_since(&td->last_rate_check_time, now)stays near zero and the min-rate check never fires. If min-rate
enforcement is intended to work with quiesce-reaped completions, the
rate path needs to stamp a completion time too (or
check_min_rate()should take a fresh
fio_gettime()when the passed time predates thelast check).
Environment:
Amazon Linux 2023, x86_64 (EC2); also reproduced (with the poison
above) on an Amazon Linux 2 dev host.
fio version:
fio-3.42 (crash observed in production); code identical in current
master and present since at least fio-3.38.
Was blktrace being run in parallel?:
No.