Skip to content

Commit fbd701c

Browse files
pfmooneyPatrick Mooney
authored andcommitted
NVMe admin commands should better mind their PRPs
The Identify and GetLogPage admin commands in NVMe should not assume that the output buffers provided to them in the PRPs consist of a single page-sized page-aligned entry. Guest (such as Linux) can and will issue those commands with a page offset in PRP1, splitting the output into another page. Fixes #427
1 parent 83d2b5f commit fbd701c

2 files changed

Lines changed: 92 additions & 23 deletions

File tree

lib/propolis/src/hw/nvme/admin.rs

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use std::cmp::min;
22
use std::mem::size_of;
33

4-
use crate::common::GuestAddr;
5-
use crate::common::PAGE_SIZE;
4+
use crate::common::{GuestAddr, GuestRegion, PAGE_SIZE};
65
use crate::vmm::MemCtx;
76

87
use super::bits::*;
@@ -186,14 +185,19 @@ impl NvmeCtrl {
186185
cmd: &cmds::GetLogPageCmd,
187186
mem: &MemCtx,
188187
) -> cmds::Completion {
189-
assert!((cmd.len as usize) < PAGE_SIZE);
190-
let buf = cmd
188+
if let Some(regions) = cmd
191189
.data(mem)
192-
.next()
193-
.expect("missing prp entry for log page response");
194-
// TODO: actually keep a log that we can write back instead of all zeros
195-
assert!(mem.write_byte(buf.0, 0, cmd.len as usize));
196-
cmds::Completion::success()
190+
.map(|r| mem.writable_region(&r))
191+
.collect::<Option<Vec<_>>>()
192+
{
193+
// TODO: Keep a log to write back instead of 0s
194+
for region in regions {
195+
let _ = region.write_byte(0, region.len());
196+
}
197+
cmds::Completion::success()
198+
} else {
199+
cmds::Completion::generic_err(STS_DATA_XFER_ERR)
200+
}
197201
}
198202

199203
/// Service Identify command.
@@ -208,12 +212,16 @@ impl NvmeCtrl {
208212
IDENT_CNS_NAMESPACE => match cmd.nsid {
209213
1 => {
210214
assert!(size_of::<IdentifyNamespace>() <= PAGE_SIZE);
211-
let buf = cmd
212-
.data(mem)
213-
.next()
214-
.expect("missing prp entry for ident response");
215-
assert!(mem.write(buf.0, &self.ns_ident));
216-
cmds::Completion::success()
215+
match Self::write_admin_result(
216+
cmd.data(mem),
217+
&self.ns_ident,
218+
mem,
219+
) {
220+
Some(_) => cmds::Completion::success(),
221+
None => {
222+
cmds::Completion::generic_err(STS_DATA_XFER_ERR)
223+
}
224+
}
217225
}
218226
// 0 is not a valid NSID (See NVMe 1.0e, Section 6.1 Namespaces)
219227
// We also don't currently support namespace management
@@ -224,12 +232,15 @@ impl NvmeCtrl {
224232
},
225233
IDENT_CNS_CONTROLLER => {
226234
assert!(size_of::<IdentifyController>() <= PAGE_SIZE);
227-
let buf = cmd
228-
.data(mem)
229-
.next()
230-
.expect("missing prp entry for ident response");
231-
assert!(mem.write(buf.0, &self.ctrl_ident));
232-
cmds::Completion::success()
235+
236+
match Self::write_admin_result(
237+
cmd.data(mem),
238+
&self.ctrl_ident,
239+
mem,
240+
) {
241+
Some(_) => cmds::Completion::success(),
242+
None => cmds::Completion::generic_err(STS_DATA_XFER_ERR),
243+
}
233244
}
234245
// We currently present NVMe version 1.0 in which CNS is a 1-bit field
235246
// and hence only need to support the NAMESPACE and CONTROLLER cases
@@ -282,4 +293,58 @@ impl NvmeCtrl {
282293
}
283294
}
284295
}
296+
297+
/// Write result data from an admin command into host memory
298+
///
299+
/// The `data` type must be `repr(packed(1))`
300+
///
301+
/// Returns `Some(())` if successful, else None
302+
fn write_admin_result<T: Copy>(
303+
prp: cmds::PrpIter,
304+
data: &T,
305+
mem: &MemCtx,
306+
) -> Option<()> {
307+
let bufs: Vec<GuestRegion> = prp.collect();
308+
if size_of::<T>() > bufs.iter().map(|r| r.1).sum::<usize>() {
309+
// Not enough space
310+
return None;
311+
}
312+
let regions = bufs
313+
.into_iter()
314+
.map(|r| mem.writable_region(&r))
315+
.collect::<Option<Vec<_>>>()?;
316+
if regions.len() == 1 {
317+
// Can be copied to one contiguous page
318+
regions[0].write(data).ok()?;
319+
Some(())
320+
} else {
321+
// Split across multiple pages
322+
323+
// Safety:
324+
//
325+
// We expect and demand that the resulting structs written through
326+
// this function are packed, such that there is no padding to risk
327+
// UB through the [u8] slice creation.
328+
let mut raw = unsafe {
329+
std::slice::from_raw_parts(
330+
data as *const T as *const u8,
331+
size_of::<T>(),
332+
)
333+
};
334+
let mut copied = 0;
335+
for region in regions {
336+
let write_len = usize::min(region.len(), raw.len());
337+
338+
let to_copy;
339+
(to_copy, raw) = raw.split_at(write_len);
340+
copied += region.write_bytes(&to_copy).ok()?;
341+
342+
if raw.is_empty() {
343+
break;
344+
}
345+
}
346+
assert_eq!(copied, size_of::<T>());
347+
Some(())
348+
}
349+
}
285350
}

lib/propolis/src/hw/nvme/cmds.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,13 @@ pub struct GetLogPageCmd {
244244
}
245245

246246
impl GetLogPageCmd {
247-
/// Returns an Iterator that yields [`GuestRegion`]'s to write the log page data to.
247+
/// Returns an Iterator that yields [`GuestRegion`]'s to write the log page
248+
/// data to.
249+
///
250+
/// The expected size of the memory covered by the PRPs is defined by
251+
/// `NUMD`, stored as bytes (rather than number Dwords) in [`Self::len`]
248252
pub fn data<'a>(&self, mem: &'a MemCtx) -> PrpIter<'a> {
249-
PrpIter::new(PAGE_SIZE as u64, self.prp1, self.prp2, mem)
253+
PrpIter::new(self.len as u64, self.prp1, self.prp2, mem)
250254
}
251255
}
252256

0 commit comments

Comments
 (0)