scsidisk/
getlbastatus.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for the SCSI "Get LBA Status" command.
5//!
6//! Currently, this command just returns that all blocks are "mapped".
7
8use super::ScsiError;
9use super::SimpleScsiDisk;
10use disk_backend::Disk;
11use disk_backend::DiskError;
12use guestmem::MemoryWrite;
13use scsi::AdditionalSenseCode;
14use scsi_buffers::RequestBuffers;
15use scsi_core::Request;
16use scsi_defs as scsi;
17use zerocopy::FromBytes;
18use zerocopy::FromZeros;
19use zerocopy::IntoBytes;
20
21/// Result of a get LBA status request.
22#[derive(Debug, Default, Copy, Clone)]
23struct DeviceBlockIndexInfo {
24    /// The size of the first partial block.
25    first_partial_block_size: u32,
26    /// The index of the first full block.
27    first_full_block_index: u32,
28    /// The number of blocks.
29    block_count: u32,
30    /// The size of the last partial block.
31    #[expect(dead_code)]
32    last_partial_block_size: u32,
33    /// The number of LBAs per block.
34    lba_per_block: u64,
35}
36
37/// The LBA status of a block.
38#[derive(Debug, Copy, Clone, PartialEq, Eq)]
39enum LbaStatus {
40    /// The block is mapped.
41    Mapped,
42    /// The block is deallocated.
43    #[expect(dead_code)]
44    Deallocated,
45    /// The block is anchored.
46    #[expect(dead_code)]
47    Anchored,
48}
49
50/// Returns the block index information for the given file offset.
51fn file_offset_to_device_block_index_and_length(
52    disk: &Disk,
53    _start_offset: u64,
54    _get_lba_status_range_length: u64,
55    _block_size: u64,
56) -> DeviceBlockIndexInfo {
57    let sector_size = disk.sector_size() as u64;
58    let sector_count = disk.sector_count();
59    let disk_size = sector_size * sector_count;
60
61    // Treat fully allocation disk or fixed disk as one large block and just return
62    // enough descriptors from the LBA requested till the last LBA on disk.
63    //
64    // LbaPerBlock is a ULONG and technically with MAXULONG * 512 byte sectors,
65    // we can get upto 1.99 TB. The LBA descriptor also holds a ULONG
66    // LogicalBlockCount and can have an issue for larger than 2TB disks.
67    let lba_per_block = std::cmp::min(sector_count, u32::MAX.into());
68    let block_size_large = lba_per_block * sector_size;
69    let block_count = disk_size.div_ceil(block_size_large) as u32;
70    DeviceBlockIndexInfo {
71        first_partial_block_size: 0,
72        first_full_block_index: 0,
73        block_count,
74        last_partial_block_size: 0,
75        lba_per_block,
76    }
77}
78
79/// Returns the LBA status for the given block number.
80fn get_block_lba_status(
81    _block_number: u32,
82    _leaf_node_state_only: bool,
83) -> Result<LbaStatus, DiskError> {
84    Ok(LbaStatus::Mapped)
85}
86
87impl SimpleScsiDisk {
88    pub(crate) fn handle_get_lba_status(
89        &self,
90        external_data: &RequestBuffers<'_>,
91        request: &Request,
92        sector_count: u64,
93    ) -> Result<usize, ScsiError> {
94        let cdb = scsi::GetLbaStatus::read_from_prefix(&request.cdb[..])
95            .unwrap()
96            .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759)
97
98        // Validate the request parameters.
99        let start_lba = cdb.start_lba.get();
100        if start_lba >= sector_count {
101            return Err(ScsiError::IllegalRequest(
102                AdditionalSenseCode::ILLEGAL_BLOCK,
103            ));
104        }
105
106        let allocation_length = cdb.allocation_length.get() as usize;
107        if allocation_length != external_data.len() {
108            return Err(ScsiError::SrbError);
109        }
110
111        // A special case in the SCSI spec. Complete the command immediately.
112        if allocation_length == 0 {
113            return Ok(0);
114        }
115
116        // Must meet the minimum response size (a header and one descriptor entry).
117        let min = size_of::<scsi::LbaStatusListHeader>() + size_of::<scsi::LbaStatusDescriptor>();
118        if allocation_length < min {
119            tracelimit::error_ratelimited!(allocation_length, min, "invalid cdb");
120            return Err(ScsiError::IllegalRequest(AdditionalSenseCode::INVALID_CDB));
121        }
122
123        let sector_size = self.sector_size as u64;
124        let disk_size = sector_size * sector_count;
125        let block_size = sector_size; // TODO: get block_size from disk
126
127        let start_offset = start_lba << self.sector_shift;
128        let get_lba_status_range_length = disk_size - start_offset;
129        let total_lba_count_requested = get_lba_status_range_length / sector_size;
130
131        let mut lba_count_remaining = total_lba_count_requested;
132
133        let mut block_index_info = file_offset_to_device_block_index_and_length(
134            &self.disk,
135            start_offset,
136            get_lba_status_range_length,
137            block_size,
138        );
139
140        let mut block_number = block_index_info.first_full_block_index;
141        if block_index_info.first_partial_block_size != 0 {
142            block_number -= 1;
143            block_index_info.block_count += 1;
144        }
145
146        let block_number_max = block_number + block_index_info.block_count;
147
148        // at this point, we have
149        //
150        // BlockNumber:             First block number to query LBA status (may be partial).
151        // FirstPartialBlockSize:   Size of the first block to query LBA status.
152        // LastPartialBlockSize:   Size of the last block to query LBA status.
153        //
154        // BlockCount:              Total number of blocks to query status for, including the first
155        //                               and last partial blocks.
156
157        // the output buffer is to look like this
158        //
159        //  LBA_STATUS_LIST_HEADER
160        //  LBA_STATUS_DESCRIPTOR
161        //  LBA_STATUS_DESCRIPTOR
162        //  ...
163        //  LBA_STATUS_DESCRIPTOR
164        //
165        // Each LBA_STATUS_DESCRIPTOR describes a range of consecutive LBAs having the
166        // same LBA status. Since every LBA in a VHD block must have the same LBA status,
167        // we can always report all LBAs within the same block using exactly one
168        // LBA_STATUS_DESCRIPTOR.  We need as many LBA_STATUS_DESCRIPTORs as
169        // BlockCount. We end up using fewer LBA_STATUS_DESCRIPTORs when we
170        // realize some neighboring blocks have the same block status and we can group them
171        // into the same LBA_STATUS_DESCRIPTOR.
172
173        // Build additional LBA_STATUS_DESCRIPTORs and copy them to the output buffer as
174        // long as there is room and we have more block statuses to report.
175
176        let mut lba_descriptors_used = 0;
177
178        // Calculate how many descriptors are available in the buffer which does not exceed
179        // the amount expressible by the Parameter Length field.
180        // Maximum number of LBA_STATUS_DESCRIPTORs the SCSI-3 spec allows per request.
181        const LBA_STATUS_DESCRIPTOR_COUNT_MAX: u32 = (u32::MAX
182            - size_of::<scsi::LbaStatusListHeader>() as u32)
183            / (size_of::<scsi::LbaStatusDescriptor>() as u32);
184        let mut lba_descriptors_available = std::cmp::min(
185            LBA_STATUS_DESCRIPTOR_COUNT_MAX,
186            ((allocation_length - size_of::<scsi::LbaStatusListHeader>())
187                / size_of::<scsi::LbaStatusDescriptor>()) as u32,
188        );
189
190        // Get the LBA status for the very first block.
191        let leaf_node_state_only =
192            request.srb_flags & scsi::SRB_FLAGS_CONSOLIDATEABLE_BLOCKS_ONLY != 0;
193        let mut provisioning_status = match get_block_lba_status(block_number, leaf_node_state_only)
194        {
195            Ok(status) => status,
196            Err(e) => return Err(ScsiError::Disk(e)),
197        };
198
199        let mut provisioning_status_in_previous_block;
200        let mut lba_count_in_current_block;
201        let mut descriptor_lba_count;
202        let mut buffer: Vec<u8> = vec![0; allocation_length];
203        let mut next_lba_status_descriptor = size_of::<scsi::LbaStatusListHeader>();
204        let mut next_start_lba = start_lba;
205        while lba_descriptors_available != 0 && block_number < block_number_max {
206            provisioning_status_in_previous_block = provisioning_status;
207
208            // NextStartLba can be pointing to in the middle of this block.  Thus,
209            // the LBA count in this block can be less than a full block.
210            lba_count_in_current_block = std::cmp::min(
211                block_index_info.lba_per_block - next_start_lba % block_index_info.lba_per_block,
212                lba_count_remaining,
213            );
214            lba_count_remaining -= lba_count_in_current_block;
215
216            // Start a new accumulated LBA count for a new LBA_STATUS_DESCRIPTOR.
217            descriptor_lba_count = lba_count_in_current_block;
218            block_number += 1;
219
220            // Loop through all subsequent blocks until we see a different status and
221            // add it to the total LBA count.
222            while block_number < block_number_max {
223                // Get the LBA status for the next block.
224                // Usually it's a full block except when we get to the last block.
225                provisioning_status = match get_block_lba_status(block_number, leaf_node_state_only)
226                {
227                    Ok(status) => status,
228                    Err(e) => return Err(ScsiError::Disk(e)),
229                };
230
231                // This block has a different status from what we are looking for.
232                // Break out of here so we can composite a new LBA_STATUS_DESCRIPTOR.
233                if provisioning_status != provisioning_status_in_previous_block {
234                    break;
235                }
236
237                lba_count_in_current_block =
238                    std::cmp::min(block_index_info.lba_per_block, lba_count_remaining);
239
240                // Only a limited number of LBAs can share the same status descriptor. Finish
241                // this descriptor if it is full.
242                if descriptor_lba_count + lba_count_in_current_block > u32::MAX.into() {
243                    lba_count_remaining -= (u32::MAX as u64) - descriptor_lba_count;
244                    descriptor_lba_count = u32::MAX.into();
245                    break;
246                }
247
248                lba_count_remaining -= lba_count_in_current_block;
249                descriptor_lba_count += lba_count_in_current_block;
250                block_number += 1;
251            }
252
253            let provisioning_status = match provisioning_status_in_previous_block {
254                LbaStatus::Mapped => scsi::LBA_STATUS_MAPPED,
255                LbaStatus::Deallocated => scsi::LBA_STATUS_DEALLOCATED,
256                LbaStatus::Anchored => scsi::LBA_STATUS_ANCHORED,
257            };
258
259            // Composite the current LBA_STATUS_DESCRIPTOR and
260            // prepare for the next LBA_STATUS_DESCRIPTOR.
261            let lba_status_descriptor = scsi::LbaStatusDescriptor {
262                start_lba: next_start_lba.into(),
263                logical_block_count: (descriptor_lba_count as u32).into(),
264                provisioning_status,
265                reserved2: [0; 3],
266            };
267
268            tracing::trace!(
269                lba_status = ?lba_status_descriptor,
270                "get_lba_status"
271            );
272
273            let new_next_lba_status_descriptor =
274                next_lba_status_descriptor + size_of::<scsi::LbaStatusDescriptor>();
275            buffer[next_lba_status_descriptor..new_next_lba_status_descriptor]
276                .copy_from_slice(lba_status_descriptor.as_bytes());
277            lba_descriptors_used += 1;
278            lba_descriptors_available -= 1;
279            next_start_lba += descriptor_lba_count;
280            next_lba_status_descriptor = new_next_lba_status_descriptor;
281        }
282
283        // Fill out the header, including the number of contained descriptors.
284        let lba_status_descriptors_length =
285            lba_descriptors_used * size_of::<scsi::LbaStatusDescriptor>();
286        let mut lba_status_list_header = scsi::LbaStatusListHeader::new_zeroed();
287        lba_status_list_header.parameter_length = ((lba_status_descriptors_length
288            + size_of_val(&lba_status_list_header.reserved))
289            as u32)
290            .into();
291
292        buffer[0..size_of::<scsi::LbaStatusListHeader>()]
293            .copy_from_slice(lba_status_list_header.as_bytes());
294        let tx = lba_status_descriptors_length + size_of::<scsi::LbaStatusListHeader>();
295
296        external_data
297            .writer()
298            .write(&buffer[..tx])
299            .map_err(ScsiError::MemoryAccess)?;
300
301        Ok(tx)
302    }
303}