Skip to content

do_io: uninitialized comp_time can reach check_min_rate (assert in rel_time_since / SIGABRT) #2128

Description

@Vladyyy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions