Skip to content

upstairs: validate subvolume coverage before IO dispatch#1956

Draft
SG-devel wants to merge 3 commits into
oxidecomputer:mainfrom
SG-devel:upstairs-volume-partial-coverage
Draft

upstairs: validate subvolume coverage before IO dispatch#1956
SG-devel wants to merge 3 commits into
oxidecomputer:mainfrom
SG-devel:upstairs-volume-partial-coverage

Conversation

@SG-devel

@SG-devel SG-devel commented Jul 6, 2026

Copy link
Copy Markdown

Summary

For requests that start on a valid block but extend past the end of the volume, the existing code finds the valid prefix of the request, but does not appear to check that the entire requested range is covered before dispatch.

I am marking this as a draft because it is a boundary-condition change in storage range handling, and I would appreciate confirmation that returning OffsetInvalid for partially covered volume IO is the intended contract. I also want to get the CI signal before marking this ready for review.

This draft PR proposes a defensive range-validation in upstairs/src/volume.rs. I tried to keep the change narrow:

  • the first commit adds failing regression tests;
  • the second commit changes the subvolume-backed range handling so partially covered read, write, and write_unwritten requests return OffsetInvalid;
  • the third commit adds focused property coverage for SubVolume::lba_range_coverage(), plus an explicit overflow case.

Semantic changes

This PR intends the following behavior changes:

  • SubVolume::lba_range_coverage() now computes the intersection of two half-open ranges directly, using checked arithmetic for the requested range end.
  • SubVolume::lba_range_coverage() returns coverage for requests that fully cover a subvolume while extending outside it, instead of panicking.
  • Volume::write() and Volume::write_unwritten() now reject partially covered requests before dispatching any write.
  • Volume::read() now rejects partially covered requests on the subvolume-backed path before dispatching any subvolume read.
  • The final read-buffer coverage check now returns OffsetInvalid as a defensive fallback instead of relying only on assert_eq!.

Details

The tests build a small volume with two subvolumes:

subvolume 0: [0, 2)  -> blocks 0, 1
subvolume 1: [2, 4)  -> blocks 2, 3
full volume: [0, 4)  -> blocks 0, 1, 2, 3

This setup is intentionally small, but it still exercises the multi-subvolume coverage path used by Volume.

For writes, the important boundary case is a request that starts at the final valid block and extends past the end:

write 2 blocks starting at block 3

requested range: [3, 5)  -> blocks 3, 4
covered range:   [3, 4)  -> block 3
missing range:   [4, 5)  -> block 4, outside the volume

The old write path could treat the valid prefix [3, 4) as sufficient, dispatch that one-block write, and return success for the full two-block request. This PR changes the write path to verify full coverage before dispatch, so the request is rejected before block 3 is modified.

For reads, the two-subvolume setup also exposes a related coverage-calculation issue:

read 4 blocks starting at block 1

requested range:      [1, 5)
subvolume 0 coverage: [1, 2)
subvolume 1 coverage: [2, 4)
missing range:              [4, 5)

This request has valid covered pieces, but it is still not fully covered by the volume. It should return OffsetInvalid.

The same read case also exercises SubVolume::lba_range_coverage() with this shape:

request:   [1, 5)
subvolume: [2, 4)
coverage:  [2, 4)

That is a valid case where the request fully covers the subvolume while extending outside it. The helper should return Some(2..4), not panic.

The proposal simplifies SubVolume::lba_range_coverage() to compute the intersection of two half-open ranges directly. On the subvolume-backed IO paths, Volume then checks that the collected coverage ranges exactly span the requested IO range before dispatching subvolume reads or writes. There is an explicit regression test for the request shape that previously reached the helper's panic path, and there is a property test that checks the helper against an independently derived block-by-block intersection over small bounded ranges. The property test is intended to document that the helper returns exactly the non-empty intersection of the requested half-open LBA range and the subvolume's half-open LBA range.

Testing

I ran the following locally on Ubuntu under WSL on Windows:

  • cargo fmt --check
  • cargo check --workspace
  • cargo check --workspace --release
  • cargo test --workspace
  • cargo test --workspace --release

Contribution

I submit this contribution under the repository’s existing license terms, the Mozilla Public License Version 2.0, without any warranty.

AI assistance disclosure: I used ChatGPT (GPT-5.5 Thinking) throughout development of this patch, including reasoning about the issue, writing code and tests, reviewing the implementation, and writing commit and PR text. I guided the work and ran the listed checks.

SG-devel added 3 commits July 6, 2026 11:12
Add regression tests for invalid Volume IO requests that start on a valid
block but extend past the final covered block. This is a test-only commit
that documents the current boundary behavior before changing it.

The Volume read, write, and write_unwritten tests cover requests with a valid
covered prefix and an out-of-range suffix. The current code does not verify
that collected subvolume coverage fully spans the requested IO before dispatch.

The write and write_unwritten tests document the most serious failure mode:
a request can be partially dispatched to the final subvolume and still return
success. The tests also verify that the already-valid final block is not
mutated when the full request is out of range.

Add a focused regression test for SubVolume::lba_range_coverage() as well.
A request that fully covers a subvolume while extending outside it, such as
request [1, 5) against subvolume [2, 4), should return Some(2..4), but
currently panics while computing coverage.
Make SubVolume::lba_range_coverage compute the half-open range intersection
directly, so requests that cover an entire subvolume while extending outside
it return the covered range instead of panicking.

Validate that subvolume coverage fully spans the requested Volume read/write
range before dispatch. This makes partially covered read, write, and
write_unwritten requests return OffsetInvalid instead of proceeding with only
the covered part of the request.

Return OffsetInvalid instead of relying on a final read assertion when the
destination buffer is not fully covered.
The helper is meant to return the non-empty intersection of the requested half-open LBA range and the subvolume's half-open LBA range. Add a property test for that contract over small bounded ranges.

The property test derives the expected coverage by enumerating covered block numbers, rather than by using the same `max`/`min` formula as the implementation. This keeps the test oracle independent from the implementation strategy.

Also add an explicit overflow case for the requested range end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant