windows-ioring-sys 0.6.0

Memory-safe Rust over the Windows IoRing submission/completion ring, with the ring holding each in-flight buffer until its completion.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// Copyright (c) 2026 Mike Grier
use windows_sys::Win32::Storage::FileSystem::{
    FILE_FLUSH_DATA, FILE_FLUSH_DEFAULT, FILE_FLUSH_MIN_METADATA, FILE_FLUSH_NO_SYNC,
    FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH, IOSQE_FLAGS_DRAIN_PRECEDING_OPS,
    IOSQE_FLAGS_NONE,
};

use super::{Batch, FlushCoverage, FlushMode, PushOptions, WriteCaching};
use crate::IoRing;

#[test]
fn default_push_options_set_no_barrier() {
    assert_eq!(PushOptions::new().sqe_flags(), IOSQE_FLAGS_NONE);
}

#[test]
fn a_covering_flush_sets_the_barrier_flag_and_an_unordered_one_does_not() {
    // The mapping this whole type exists for (M12.1, D-23): a covering flush
    // must carry the barrier, because without it the flush's completion says
    // nothing about the writes before it. Pinned here so a future edit to
    // `sqe_flags` cannot silently turn every covering flush into an unordered
    // one -- which would compile, pass every other test, and lose data only
    // on power failure.
    assert_eq!(
        FlushCoverage::CoversPrecedingOperations.sqe_flags(),
        IOSQE_FLAGS_DRAIN_PRECEDING_OPS
    );
    assert_eq!(FlushCoverage::Unordered.sqe_flags(), IOSQE_FLAGS_NONE);
}

#[test]
fn write_caching_maps_to_the_kernels_write_flags() {
    // `Cached` is the default and must stay the no-flag value: silently
    // enabling write-through would change latency behaviour for every existing
    // caller without changing any call site (M12.3).
    assert_eq!(WriteCaching::Cached.raw(), FILE_WRITE_FLAGS_NONE);
    assert_eq!(
        WriteCaching::WriteThrough.raw(),
        FILE_WRITE_FLAGS_WRITE_THROUGH
    );
    assert_eq!(WriteCaching::default(), WriteCaching::Cached);
}

#[test]
fn flush_mode_maps_to_the_kernels_flush_modes() {
    // The mapping matters most for `NoSync`, which is the one mode that issues
    // no device sync: mixing it up with any other value would turn a commit
    // point into a no-op that still reports success (M12.4).
    assert_eq!(FlushMode::Default.raw(), FILE_FLUSH_DEFAULT);
    assert_eq!(FlushMode::Data.raw(), FILE_FLUSH_DATA);
    assert_eq!(FlushMode::MinMetadata.raw(), FILE_FLUSH_MIN_METADATA);
    assert_eq!(FlushMode::NoSync.raw(), FILE_FLUSH_NO_SYNC);
    assert_eq!(FlushMode::default(), FlushMode::Default);
}

#[test]
fn drain_preceding_sets_the_barrier_flag() {
    assert_eq!(
        PushOptions::new().drain_preceding(true).sqe_flags(),
        IOSQE_FLAGS_DRAIN_PRECEDING_OPS
    );
    assert_eq!(
        PushOptions::new()
            .drain_preceding(true)
            .drain_preceding(false)
            .sqe_flags(),
        IOSQE_FLAGS_NONE
    );
}

#[test]
fn dropping_a_batch_that_queued_nothing_submits_harmlessly() {
    let mut ring = IoRing::new(8, 8).expect("create ring");
    let batch = Batch::new(&mut ring);
    drop(batch);
}

// --- registration accessors and claim guards (M18.4) -------------------------
//
// Everything below closes a mutation-testing survivor from M18.3. These are
// unit tests rather than integration ones because the interesting cases -- an
// index past the end, a completion naming a different operation -- are cheap to
// construct directly and awkward to provoke through a real ring.

use super::{PendingFileRegistration, RegisteredFiles};
use crate::ring::Completion;

/// A registration whose base index is deliberately non-zero.
///
/// Every existing test registers one handle and reads index 0, where
/// `base_index + i` is indistinguishable from `base_index`, `base_index - i`
/// and `base_index * i`. All three arithmetic mutants survived on that.
fn registered_files(ring: &IoRing) -> RegisteredFiles {
    RegisteredFiles {
        base_index: 3,
        count: 2,
        ring_id: ring.ring_id(),
    }
}

#[test]
fn registered_files_report_their_extent() {
    let ring = IoRing::new(8, 8).expect("create ring");
    let files = registered_files(&ring);

    assert_eq!(files.len(), 2);
    assert!(!files.is_empty());

    let empty = RegisteredFiles {
        base_index: 0,
        count: 0,
        ring_id: ring.ring_id(),
    };
    assert_eq!(empty.len(), 0);
    assert!(empty.is_empty());
}

#[test]
fn registered_files_index_from_the_base_and_stop_at_the_end() {
    let ring = IoRing::new(8, 8).expect("create ring");
    let files = registered_files(&ring);

    assert_eq!(files.get(0).expect("index 0 is in range").index(), 3);
    assert_eq!(files.get(1).expect("index 1 is in range").index(), 4);
    assert!(
        files.get(2).is_none(),
        "index 2 is one past the end of a two-handle registration"
    );
    assert!(files.get(u32::MAX).is_none());
}

#[test]
fn a_pending_file_registration_claims_only_its_own_completion() {
    let ring = IoRing::new(8, 8).expect("create ring");
    let other = IoRing::new(8, 8).expect("create second ring");
    let user_data = 41_usize;

    let pending = || PendingFileRegistration {
        user_data,
        base_index: 0,
        count: 1,
        ring_id: ring.ring_id(),
    };

    // Right ring, wrong operation.
    let wrong_op = Completion::synthetic(user_data.wrapping_add(1), 0, ring.ring_id());
    assert!(
        pending().claim_if(&wrong_op).is_err(),
        "a completion naming another operation must be refused"
    );

    // Right operation, wrong ring -- the half a `||`-to-`&&` mutant hides in.
    let wrong_ring = Completion::synthetic(user_data, 0, other.ring_id());
    assert!(
        pending().claim_if(&wrong_ring).is_err(),
        "a completion from another ring must be refused even with a matching id"
    );

    let matching = Completion::synthetic(user_data, 0, ring.ring_id());
    let files = pending()
        .claim_if(&matching)
        .expect("its own completion is accepted")
        .expect("the registration succeeded");
    assert_eq!(files.len(), 1);
}

#[test]
fn a_pending_file_registration_reports_the_user_data_it_will_claim() {
    let ring = IoRing::new(8, 8).expect("create ring");
    let pending = PendingFileRegistration {
        user_data: 7,
        base_index: 0,
        count: 1,
        ring_id: ring.ring_id(),
    };
    assert_eq!(pending.user_data(), 7);
    // Claiming against exactly what was reported must work, which is what ties
    // the accessor to the guard rather than leaving it a free-floating number.
    let completion = Completion::synthetic(pending.user_data(), 0, ring.ring_id());
    assert!(pending.claim_if(&completion).is_ok());
}

#[test]
fn a_pending_buffer_registration_claims_only_its_own_completion() {
    let mut ring = IoRing::new(8, 8).expect("create ring");
    let other = IoRing::new(8, 8).expect("create second ring");

    // Burn the first two `UserData` values so the registration below is neither
    // operation 0 nor operation 1. A fresh ring starts at zero, so burning none
    // leaves `user_data() -> 0` indistinguishable from the truth, and burning
    // one leaves `-> 1` indistinguishable -- both of which survived in turn
    // while this test was being written.
    for expected in 0..2 {
        let burned = ring
            .accounting_mut()
            .reserve_user_data()
            .expect("reserve a UserData");
        assert_eq!(
            burned, expected,
            "a fresh ring hands out UserData from zero, in order"
        );
        ring.record_completion(burned);
    }

    let mut batch = Batch::new(&mut ring);
    let pending = batch
        .register_buffers(vec![vec![0_u8; 64]])
        .expect("queue buffer registration");
    assert_ne!(
        pending.user_data(),
        0,
        "the registration is not operation zero, so a constant zero cannot pass"
    );
    let user_data = pending.user_data();
    batch.submit_and_wait(1, 5_000).expect("submit");

    // Matched rather than `expect_err`, because `RegisteredBuffers` is not
    // `Debug` -- it owns caller buffers whose type need not be.
    let wrong_ring = Completion::synthetic(user_data, 0, other.ring_id());
    let Err(pending) = pending.claim_if(&wrong_ring) else {
        panic!("a completion from another ring must be refused");
    };

    let wrong_op = Completion::synthetic(user_data.wrapping_add(1), 0, ring.ring_id());
    let Err(pending) = pending.claim_if(&wrong_op) else {
        panic!("a completion naming another operation must be refused");
    };

    let real = crate::ring::pop_within(&mut ring, "the registration's completion");
    // Ties the accessor to the operation it names, against an id obtained from
    // the ring rather than from the accessor itself -- a constant `user_data`
    // survives any comparison that starts from `user_data`.
    assert_eq!(
        pending.user_data(),
        real.user_data(),
        "the reported user_data must be the one the ring completed"
    );
    let mut buffers = pending
        .claim_if(&real)
        .expect("its own completion is accepted")
        .expect("the registration succeeded");

    assert_eq!(buffers.len(), 1);
    assert!(!buffers.is_empty());
    // `get` runs the bounds check `checked_index` owns; one past the end must
    // be refused rather than reaching into the registration's neighbour.
    assert!(buffers.get(0).is_ok());
    assert!(
        buffers.get(1).is_err(),
        "index 1 is one past the end of a one-buffer registration"
    );

    // `get` has its own bounds check; `checked_index` is the one the *span*
    // paths use, and it is only reachable by submitting against an index.
    let mut batch = Batch::new(&mut ring);
    let out_of_range = unsafe {
        batch.read_registered_raw_owned(
            std::ptr::null_mut(),
            &buffers,
            crate::RegisteredSpan {
                buffer_index: 1,
                offset: 0,
                len: 8,
            },
            (),
            0,
            PushOptions::new(),
        )
    };
    assert!(
        out_of_range.is_err(),
        "a span naming one past the last registered buffer must be refused"
    );
}

#[test]
fn submit_reports_how_many_operations_it_queued() {
    // `Batch::submit -> Ok(0)` and `-> Ok(1)` both survived M18.3: every caller
    // discarded the count, so nothing observed it.
    let path = std::env::temp_dir().join(format!("ioring-submit-count-{}.tmp", std::process::id()));
    std::fs::write(&path, vec![1_u8; 4096]).expect("write fixture");
    let file = std::fs::OpenOptions::new()
        .read(true)
        .open(&path)
        .expect("open fixture");
    let handle = std::os::windows::io::AsRawHandle::as_raw_handle(&file);

    let mut ring = IoRing::<Vec<u8>>::with_inventory(16, 16).expect("create ring");
    let mut batch = Batch::new(&mut ring);
    for index in 0..3_u64 {
        // SAFETY: `file` outlives every operation -- all three are drained
        // before this test returns.
        unsafe {
            batch.read_raw_owned(handle, vec![0_u8; 512], (), index * 512, PushOptions::new())
        }
        .expect("queue read");
    }
    assert_eq!(
        batch.submit().expect("submit"),
        3,
        "submit must report the number of operations it queued"
    );

    // Drain so the ring can be dropped with nothing outstanding.
    let mut popped = 0;
    while popped < 3 {
        while let Some((completion, held)) = ring.try_pop().expect("pop") {
            let _ = completion.result();
            assert!(held.is_some(), "the ring was holding this read's buffer");
            popped += 1;
        }
    }

    drop(file);
    let _ = std::fs::remove_file(&path);
}

// Debug-only: the guard it expects is a `debug_assert!`, which a release build
// compiles out, so under `cargo test --release` nothing would panic. Gated
// together with `ring::tests`' two `IoRing::drop` assert tests, the other two
// of that shape.
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "RegisteredBuffers dropped while an operation still references it")]
fn dropping_a_registration_with_work_outstanding_is_refused() {
    // M5.3's drop guard: freeing the backing memory while an
    // `IORING_BUFFER_REF` still points at it is the use-after-free this type
    // exists to prevent. `<impl Drop for RegisteredBuffers>::drop -> ()`
    // survived M18.3, so nothing observed the guard firing.
    let path = std::env::temp_dir().join(format!("ioring-drop-guard-{}.tmp", std::process::id()));
    std::fs::write(&path, vec![2_u8; 4096]).expect("write fixture");
    let file = std::fs::OpenOptions::new()
        .read(true)
        .open(&path)
        .expect("open fixture");
    let handle = std::os::windows::io::AsRawHandle::as_raw_handle(&file);

    let mut ring = IoRing::new(16, 16).expect("create ring");
    let mut batch = Batch::new(&mut ring);
    let pending = batch
        .register_buffers(vec![vec![0_u8; 512]])
        .expect("queue buffer registration");
    batch.submit_and_wait(1, 5_000).expect("submit");
    let completion = crate::ring::pop_within(&mut ring, "the registration's completion");
    let buffers = pending
        .claim_if(&completion)
        .expect("claims its own")
        .expect("registration succeeded");

    let mut batch = Batch::new(&mut ring);
    // SAFETY: `file` outlives this operation; the operation is deliberately
    // never popped below, so the ring keeps the buffer's lease for as long as
    // the kernel may write into it.
    unsafe {
        batch.read_registered_raw_owned(
            handle,
            &buffers,
            crate::RegisteredSpan {
                buffer_index: 0,
                offset: 0,
                len: 512,
            },
            (),
            0,
            PushOptions::new(),
        )
    }
    .expect("queue registered read");
    batch.submit_and_wait(1, 5_000).expect("submit");
    // Deliberately never popped, so the buffer stays outstanding: the ring
    // holds the lease until a pop retires the entry.

    // Dropped last and explicitly: a panic raised while some *other* unwind is
    // already in progress would abort instead of failing the test.
    drop(buffers);
}

#[test]
fn require_refuses_an_op_the_ring_does_not_support() {
    // `Batch::require -> Ok(())` survived: every op this crate names is
    // genuinely supported on any real host these tests run on, so nothing
    // distinguished the real check from one that always passes.
    // `set_supported_ops_for_test` constructs a ring that lacks one, so the
    // refusal has something to refuse.
    let mut ring = IoRing::new(8, 8).expect("create ring");
    ring.set_supported_ops_for_test(&[crate::Op::Nop]);
    let batch = Batch::new(&mut ring);

    let error = batch
        .require(crate::Op::Read)
        .expect_err("Read was left out of the constructed capability set");
    assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);

    batch
        .require(crate::Op::Nop)
        .expect("Nop is in the constructed capability set");
}

#[test]
fn windows_refuses_an_empty_buffer_registration() {
    // Written while chasing `RegisteredBuffers::is_empty -> false`, which a
    // mutation run reports as surviving. It survives because the state it would
    // misreport **cannot be reached**: nothing in this crate rejects an empty
    // vector -- `register_buffers` only checks that the count fits a `u32` and
    // that the ring has no prior table -- but the kernel refuses the submission
    // with `E_INVALIDARG`, so no caller ever holds an empty registration and
    // `is_empty` never has occasion to return `true`.
    //
    // That makes the mutant unreachable rather than untested, and manufacturing
    // an in-crate struct literal to kill it would assert a shape the API cannot
    // produce. What is worth pinning is the platform behaviour itself, because
    // it is undocumented, it is the reason the accessor looks untested, and a
    // future version that started accepting empty registrations would change
    // which states this crate can be in.
    let mut ring = IoRing::new(8, 8).expect("create ring");
    let mut batch = Batch::new(&mut ring);
    let pending = batch
        .register_buffers(Vec::<Vec<u8>>::new())
        .expect("this crate queues it; the refusal comes from the kernel");
    let user_data = pending.user_data();
    batch.submit_and_wait(1, 5_000).expect("submit");

    let completion = crate::ring::pop_within(&mut ring, "the registration's completion");
    assert_eq!(completion.user_data(), user_data);

    let Err(error) = pending
        .claim_if(&completion)
        .expect("its own completion is accepted")
    else {
        panic!("Windows accepted an empty buffer registration; is_empty is now reachable");
    };
    // Asserted through the crate's own downcast rather than against
    // `Display`'s text: the point is which HRESULT the kernel returned, and
    // matching a formatted string would also fail if the message were reworded,
    // reporting a documentation change as a platform change.
    let code = crate::IoRingErrorExt::as_ioring_error(&error)
        .expect("a kernel refusal carries an HRESULT")
        .code();
    assert_eq!(
        code,
        windows_sys::Win32::Foundation::E_INVALIDARG,
        "the kernel must refuse an empty registration with E_INVALIDARG, got {error}"
    );
}