smbd/ioctl: add FSCTL_QUERY_ALLOCATED_RANGES support
authorDavid Disseldorp <ddiss@samba.org>
Thu, 19 Feb 2015 15:36:05 +0000 (16:36 +0100)
committerJeremy Allison <jra@samba.org>
Mon, 9 Mar 2015 20:27:07 +0000 (21:27 +0100)
This change implements support for FSCTL_QUERY_ALLOCATED_RANGES using
the SEEK_HOLE/SEEK_DATA functionality of lseek().

Files marked non-sparse are always reported by the ioctl as fully
allocated, regardless of any potential "strict allocate = no" savings.

Signed-off-by: David Disseldorp <ddiss@samba.org>
Reviewed-by: Jeremy Allison <jra@samba.org>
source3/smbd/smb2_ioctl_filesys.c

index 63ec19661efdddf958db195268e8c8a651dc2c88..187deaf118818c5630978cf09efc75eb69c82178 100644 (file)
@@ -225,6 +225,212 @@ static NTSTATUS fsctl_zero_data(TALLOC_CTX *mem_ctx,
        return NT_STATUS_OK;
 }
 
+static NTSTATUS fsctl_qar_buf_push(TALLOC_CTX *mem_ctx,
+                                  struct file_alloced_range_buf *qar_buf,
+                                  DATA_BLOB *qar_array_blob)
+{
+       DATA_BLOB new_slot;
+       enum ndr_err_code ndr_ret;
+       bool ok;
+
+       ndr_ret = ndr_push_struct_blob(&new_slot, mem_ctx, qar_buf,
+                       (ndr_push_flags_fn_t)ndr_push_file_alloced_range_buf);
+       if (ndr_ret != NDR_ERR_SUCCESS) {
+               DEBUG(0, ("failed to marshall QAR buf\n"));
+               return NT_STATUS_INVALID_PARAMETER;
+       }
+
+       /* TODO should be able to avoid copy by pushing into prealloced buf */
+       ok = data_blob_append(mem_ctx, qar_array_blob, new_slot.data,
+                             new_slot.length);
+       data_blob_free(&new_slot);
+       if (!ok) {
+               return NT_STATUS_NO_MEMORY;
+       }
+
+       return NT_STATUS_OK;
+}
+
+static NTSTATUS fsctl_qar_seek_fill(TALLOC_CTX *mem_ctx,
+                                   struct files_struct *fsp,
+                                   off_t curr_off,
+                                   off_t max_off,
+                                   DATA_BLOB *qar_array_blob)
+{
+       NTSTATUS status = NT_STATUS_NOT_SUPPORTED;
+
+#ifdef HAVE_LSEEK_HOLE_DATA
+       while (curr_off <= max_off) {
+               off_t data_off;
+               off_t hole_off;
+               struct file_alloced_range_buf qar_buf;
+
+               /* seek next data */
+               data_off = SMB_VFS_LSEEK(fsp, curr_off, SEEK_DATA);
+               if ((data_off == -1) && (errno == ENXIO)) {
+                       /* no data from curr_off to EOF */
+                       break;
+               } else if (data_off == -1) {
+                       status = map_nt_error_from_unix_common(errno);
+                       DEBUG(1, ("lseek data failed: %s\n", strerror(errno)));
+                       return status;
+               }
+
+               if (data_off > max_off) {
+                       /* found something, but passed range of interest */
+                       break;
+               }
+
+               hole_off = SMB_VFS_LSEEK(fsp, data_off, SEEK_HOLE);
+               if (hole_off == -1) {
+                       status = map_nt_error_from_unix_common(errno);
+                       DEBUG(1, ("lseek hole failed: %s\n", strerror(errno)));
+                       return status;
+               }
+
+               if (hole_off <= data_off) {
+                       DEBUG(1, ("lseek inconsistent: hole %lu at or before "
+                                 "data %lu\n", (unsigned long)hole_off,
+                                 (unsigned long)data_off));
+                       return NT_STATUS_INTERNAL_ERROR;
+               }
+
+               qar_buf.file_off = data_off;
+               /* + 1 to convert maximum offset to length */
+               qar_buf.len = MIN(hole_off, max_off + 1) - data_off;
+
+               status = fsctl_qar_buf_push(mem_ctx, &qar_buf, qar_array_blob);
+               if (!NT_STATUS_IS_OK(status)) {
+                       return NT_STATUS_NO_MEMORY;
+               }
+
+               curr_off = hole_off;
+       }
+       status = NT_STATUS_OK;
+#endif
+
+       return status;
+}
+
+static NTSTATUS fsctl_qar(TALLOC_CTX *mem_ctx,
+                         struct tevent_context *ev,
+                         struct files_struct *fsp,
+                         DATA_BLOB *in_input,
+                         size_t in_max_output,
+                         DATA_BLOB *out_output)
+{
+       struct fsctl_query_alloced_ranges_req qar_req;
+       struct fsctl_query_alloced_ranges_rsp qar_rsp;
+       DATA_BLOB qar_array_blob = data_blob_null;
+       uint64_t max_off;
+       enum ndr_err_code ndr_ret;
+       int ret;
+       NTSTATUS status;
+       SMB_STRUCT_STAT sbuf;
+
+       if (fsp == NULL) {
+               return NT_STATUS_FILE_CLOSED;
+       }
+
+       /* READ_DATA permission is required */
+       status = check_access(fsp->conn, fsp, NULL, FILE_READ_DATA);
+       if (!NT_STATUS_IS_OK(status)) {
+               return status;
+       }
+
+       ndr_ret = ndr_pull_struct_blob(in_input, mem_ctx, &qar_req,
+               (ndr_pull_flags_fn_t)ndr_pull_fsctl_query_alloced_ranges_req);
+       if (ndr_ret != NDR_ERR_SUCCESS) {
+               DEBUG(0, ("failed to unmarshall QAR req\n"));
+               return NT_STATUS_INVALID_PARAMETER;
+       }
+
+       /*
+        * XXX Windows Server 2008 & 2012 servers don't return lock-conflict
+        * for QAR requests over an exclusively locked range!
+        */
+
+       ret = SMB_VFS_FSTAT(fsp, &sbuf);
+       if (ret == -1) {
+               status = map_nt_error_from_unix_common(errno);
+               DEBUG(2, ("fstat failed: %s\n", strerror(errno)));
+               return status;
+       }
+
+       if ((qar_req.buf.len == 0)
+        || (sbuf.st_ex_size == 0)
+        || (qar_req.buf.file_off >= sbuf.st_ex_size)) {
+               /* zero length range or after EOF, no ranges to return */
+               return NT_STATUS_OK;
+       }
+
+       /* check for integer overflow */
+       if (qar_req.buf.file_off + qar_req.buf.len < qar_req.buf.file_off) {
+               return NT_STATUS_INVALID_PARAMETER;
+       }
+
+       /*
+        * Maximum offset is either the last valid offset _before_ EOF, or the
+        * last byte offset within the requested range. -1 converts length to
+        * offset, which is easier to work with for SEEK_DATA/SEEK_HOLE, E.g.:
+        *
+        * /off=0             /off=512K          /st_ex_size=1M
+        * |-------------------------------------|
+        * | File data                           |
+        * |-------------------------------------|
+        *                                                   QAR end\
+        *                    |=====================================|
+        *                    |    QAR off=512K, len=1M             |
+        *                    |=================^===================|
+        *                                   max_off=1M - 1
+        *             QAR end\
+        * |==================|
+        * |QAR off=0 len=512K|
+        * |==================|
+        *                   ^
+        *                max_off=512K - 1
+        */
+       max_off = MIN(sbuf.st_ex_size,
+                     qar_req.buf.file_off + qar_req.buf.len) - 1;
+
+       if (!fsp->is_sparse) {
+               struct file_alloced_range_buf qar_buf;
+
+               /* file is non-sparse, claim file_off->max_off is allocated */
+               qar_buf.file_off = qar_req.buf.file_off;
+               /* + 1 to convert maximum offset back to length */
+               qar_buf.len = max_off - qar_req.buf.file_off + 1;
+
+               status = fsctl_qar_buf_push(mem_ctx, &qar_buf, &qar_array_blob);
+       } else {
+               status = fsctl_qar_seek_fill(mem_ctx, fsp, qar_req.buf.file_off,
+                                            max_off, &qar_array_blob);
+       }
+       if (!NT_STATUS_IS_OK(status)) {
+               return status;
+       }
+
+       /* marshall response buffer. */
+       qar_rsp.far_buf_array = qar_array_blob;
+
+       ndr_ret = ndr_push_struct_blob(out_output, mem_ctx, &qar_rsp,
+               (ndr_push_flags_fn_t)ndr_push_fsctl_query_alloced_ranges_rsp);
+       if (ndr_ret != NDR_ERR_SUCCESS) {
+               DEBUG(0, ("failed to marshall QAR rsp\n"));
+               return NT_STATUS_INVALID_PARAMETER;
+       }
+
+       if (out_output->length > in_max_output) {
+               DEBUG(2, ("QAR output len %lu exceeds max %lu\n",
+                         (unsigned long)out_output->length,
+                         (unsigned long)in_max_output));
+               data_blob_free(out_output);
+               return NT_STATUS_BUFFER_TOO_SMALL;
+       }
+
+       return NT_STATUS_OK;
+}
+
 struct tevent_req *smb2_ioctl_filesys(uint32_t ctl_code,
                                      struct tevent_context *ev,
                                      struct tevent_req *req,
@@ -258,6 +464,16 @@ struct tevent_req *smb2_ioctl_filesys(uint32_t ctl_code,
                }
                return tevent_req_post(req, ev);
                break;
+       case FSCTL_QUERY_ALLOCATED_RANGES:
+               status = fsctl_qar(state, ev, state->fsp,
+                                  &state->in_input,
+                                  state->in_max_output,
+                                  &state->out_output);
+               if (!tevent_req_nterror(req, status)) {
+                       tevent_req_done(req);
+               }
+               return tevent_req_post(req, ev);
+               break;
        default: {
                uint8_t *out_data = NULL;
                uint32_t out_data_len = 0;