Rust for Linux List
 help / color / mirror / Atom feed
* [PATCH 0/1] rust: block: mq: make GenDisk Send impl sound
@ 2026-06-05  4:49 Yuan Tan
  2026-06-05  4:49 ` [PATCH 1/1] " Yuan Tan
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Yuan Tan @ 2026-06-05  4:49 UTC (permalink / raw)
  To: a.hindborg, ojeda, boqun, rust-for-linux
  Cc: zhiyunq, ardalan, pgovind2, dzueck, Yuan Tan

Hi Linux kernel maintainers,

We are developing a tool called FerroLens to detect potential unsound
behavior in Rust code in the Linux kernel. FerroLens reported the following
bug in rust/kernel/gendisk.rs.

Gendisk is marked as Send although the fields it contains may not be.
Specifically, the QueueData held in the raw gendisk pointer may not be safe
to send across threads. Therefore, sending the Gendisk from one thread to
another and dropping on a different thread may cause unsound behavior.

Additionally, Gendisk contains an Arc<TagSet<T>>. This Arc would be Send
and Sync if the underlying TagSet<T> were Send and Sync. But this is not
explicitly derived, although it can be, since the API does not modify the
TagSet.

We're not Rust experts, so we may have gotten some things wrong. We'd greatly
appreciate any corrections.


I tried creating a PoC to trigger this bug and make our findings more
solid.

We actually hacked the kernel a bit to enable KCSAN for the Rust pieces,
and it did generate a few crashes, but they are highly unstable.

BUG: KCSAN: data-race in drop_in_place<...GenDisk...> / __srcu_check_read_flavor

Here is the PoC I used. Hopefully, sharing it here in case it can helps
anyone better understand the bug.

---
python3 guest_kcsan_inflight_teardown.py --rounds 20 --inflight-threads 2 --io-threads 2 --io-burst 8 --round-cooldown 0.02

#!/usr/bin/env python3
import os
import threading
import time
import traceback


BASE = "/sys/kernel/config/rnull"
SYS_BLOCK = "/sys/block"
DEVICE_SIZE_MIB = 64
BLOCK_SIZE = 4096
ROUNDS = 220
INFLIGHT_THREADS = 8
POWER_OFF_DELAY_SEC = 0.015
POST_POWEROFF_SPIN_SEC = 0.12


def write_file(path: str, data: str) -> None:
    with open(path, "w", encoding="ascii") as f:
        f.write(data)


def wait_for_path(path: str, timeout_sec: float) -> bool:
    deadline = time.time() + timeout_sec
    while time.time() < deadline:
        if os.path.exists(path):
            return True
        time.sleep(0.001)
    return os.path.exists(path)


def create_device(name: str) -> str:
    path = f"{BASE}/{name}"
    os.mkdir(path)
    write_file(f"{path}/size", f"{DEVICE_SIZE_MIB}\n")
    write_file(f"{path}/blocksize", f"{BLOCK_SIZE}\n")
    write_file(f"{path}/irqmode", "1\n")
    write_file(f"{path}/power", "1\n")
    return path


def inflight_path(name: str) -> str:
    return f"{SYS_BLOCK}/{name}/inflight"


def inflight_worker(name: str, running: threading.Event, errors: list[str]) -> None:
    path = inflight_path(name)
    fd = None
    try:
        if not wait_for_path(path, 1.0):
            errors.append(f"{name}: inflight path not ready")
            return

        fd = os.open(path, os.O_RDONLY)
        while running.is_set():
            try:
                os.lseek(fd, 0, os.SEEK_SET)
                os.read(fd, 128)
            except OSError:
                pass
    except Exception as exc:
        errors.append(f"inflight {name}: {exc!r}\n{traceback.format_exc()}")
    finally:
        if fd is not None:
            try:
                os.close(fd)
            except OSError:
                pass


def destroy_device(path: str, errors: list[str]) -> None:
    for _ in range(1000):
        try:
            os.rmdir(path)
            return
        except OSError:
            time.sleep(0.002)
    errors.append(f"rmdir failed for {path}")


def device_round(name: str, errors: list[str]) -> None:
    path = create_device(name)
    running = threading.Event()
    running.set()

    workers = []
    for _ in range(INFLIGHT_THREADS):
        workers.append(threading.Thread(target=inflight_worker, args=(name, running, errors)))

    for worker in workers:
        worker.start()

    time.sleep(POWER_OFF_DELAY_SEC)
    write_file(f"{path}/power", "0\n")
    time.sleep(POST_POWEROFF_SPIN_SEC)
    running.clear()

    for worker in workers:
        worker.join()

    destroy_device(path, errors)


def main() -> int:
    errors: list[str] = []
    start = time.time()

    for round_id in range(ROUNDS):
        name = f"gi{round_id}"
        try:
            device_round(name, errors)
        except Exception as exc:
            errors.append(f"round {name}: {exc!r}\n{traceback.format_exc()}")

    duration = time.time() - start
    print(f"kcsan_inflight_teardown_done rounds={ROUNDS} duration={duration:.2f}s")
    print(f"errors={len(errors)}")
    for err in errors[:20]:
        print(err)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())



Yuan Tan (1):
  rust: block: mq: make GenDisk Send impl sound

 rust/kernel/block/mq/gen_disk.rs |  8 +++++---
 rust/kernel/block/mq/tag_set.rs  | 11 +++++++++++
 2 files changed, 16 insertions(+), 3 deletions(-)

-- 
2.43.2


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-06-05 13:51 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-05  4:49 [PATCH 0/1] rust: block: mq: make GenDisk Send impl sound Yuan Tan
2026-06-05  4:49 ` [PATCH 1/1] " Yuan Tan
2026-06-05 12:04 ` [PATCH 0/1] " Andreas Hindborg
2026-06-05 13:50 ` Gary Guo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox