This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch 4.19
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 066d5bcbff1b6b967fa21cf1013225437249bb97
Author: Daniel Augusto Veronezi Salvador <[email protected]>
AuthorDate: Fri Aug 30 19:03:28 2024 -0300

    Validate QCOW2 on upload and register
---
 .../storage/formatinspector/Qcow2HeaderField.java  |  51 ++++
 .../storage/formatinspector/Qcow2Inspector.java    | 267 +++++++++++++++++++++
 .../resource/NfsSecondaryStorageResource.java      |  14 +-
 .../storage/template/DownloadManagerImpl.java      |  39 ++-
 4 files changed, 360 insertions(+), 11 deletions(-)

diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2HeaderField.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2HeaderField.java
new file mode 100644
index 00000000000..4a8e8b51a47
--- /dev/null
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2HeaderField.java
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.storage.formatinspector;
+
+public enum Qcow2HeaderField {
+    MAGIC(0, 4),
+    VERSION(4, 4),
+    BACKING_FILE_OFFSET(8, 8),
+    BACKING_FILE_NAME_LENGTH(16, 4),
+    CLUSTER_BITS(20, 4),
+    SIZE(24, 8),
+    CRYPT_METHOD(32, 4),
+    L1_SIZE(36, 4),
+    LI_TABLE_OFFSET(40, 8),
+    REFCOUNT_TABLE_OFFSET(48, 8),
+    REFCOUNT_TABLE_CLUSTERS(56, 4),
+    NB_SNAPSHOTS(60, 4),
+    SNAPSHOTS_OFFSET(64, 8),
+    INCOMPATIBLE_FEATURES(72, 8);
+
+    private final int offset;
+    private final int length;
+
+    Qcow2HeaderField(int offset, int length) {
+        this.offset = offset;
+        this.length = length;
+    }
+
+    public int getLength() {
+        return length;
+    }
+
+    public int getOffset() {
+        return offset;
+    }
+}
diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2Inspector.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2Inspector.java
new file mode 100644
index 00000000000..1ad2076a12d
--- /dev/null
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/formatinspector/Qcow2Inspector.java
@@ -0,0 +1,267 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.storage.formatinspector;
+
+import com.cloud.utils.NumbersUtil;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.log4j.Logger;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class to inspect QCOW2 files/objects. In our context, a QCOW2 might be a 
threat to the environment if it meets one of the following criteria when coming 
from external sources
+ * (like registering or uploading volumes and templates):
+ * <ul>
+ *   <li>has a backing file reference;</li>
+ *   <li>has an external data file reference;</li>
+ *   <li>has unknown incompatible features.</li>
+ * </ul>
+ *
+ * The implementation was done based on the <a 
href="https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt";>
 QEMU's official interoperability documentation</a>
+ * and on the <a 
href="https://review.opendev.org/c/openstack/cinder/+/923247/2/cinder/image/format_inspector.py";>OpenStack's
 Cinder implementation for Python</a>.
+ */
+public class Qcow2Inspector {
+    protected static Logger LOGGER = Logger.getLogger(Qcow2Inspector.class);
+
+    private static final byte[] QCOW_MAGIC_STRING = 
ArrayUtils.add("QFI".getBytes(), (byte) 0xfb);
+    private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT = 4;
+    private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE = 0;
+    private static final int EXTERNAL_DATA_FILE_BYTE_POSITION = 7;
+    private static final int EXTERNAL_DATA_FILE_BIT = 2;
+    private static final byte EXTERNAL_DATA_FILE_BITMASK = (byte) (1 << 
EXTERNAL_DATA_FILE_BIT);
+
+    private static final Set<Qcow2HeaderField> SET_OF_HEADER_FIELDS_TO_READ = 
Set.of(Qcow2HeaderField.MAGIC,
+            Qcow2HeaderField.VERSION,
+            Qcow2HeaderField.SIZE,
+            Qcow2HeaderField.BACKING_FILE_OFFSET,
+            Qcow2HeaderField.INCOMPATIBLE_FEATURES);
+
+    /**
+     * Validates if the file is a valid and allowed QCOW2 (i.e.: does not 
contain external references).
+     * @param filePath Path of the file to be validated.
+     * @throws RuntimeException If the QCOW2 file meets one of the following 
criteria:
+     * <ul>
+     *   <li>has a backing file reference;</li>
+     *   <li>has an external data file reference;</li>
+     *   <li>has unknown incompatible features.</li>
+     * </ul>
+     */
+    public static void validateQcow2File(String filePath) throws 
RuntimeException {
+        LOGGER.info(String.format("Verifying if [%s] is a valid and allowed 
QCOW2 file .", filePath));
+
+        Map<String, byte[]> headerFieldsAndValues;
+        try (InputStream inputStream = new FileInputStream(filePath)) {
+            headerFieldsAndValues = unravelQcow2Header(inputStream, filePath);
+        } catch (IOException ex) {
+            throw new RuntimeException(String.format("Unable to validate file 
[%s] due to: ", filePath), ex);
+        }
+
+        validateQcow2HeaderFields(headerFieldsAndValues, filePath);
+
+        LOGGER.info(String.format("[%s] is a valid and allowed QCOW2 file.", 
filePath));
+    }
+
+    /**
+     * Unravels the QCOW2 header in a serial fashion, iterating through the 
{@link Qcow2HeaderField}, reading the fields specified in
+     * {@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ} and skipping the 
others.
+     * @param qcow2InputStream InputStream of the QCOW2 being unraveled.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @return A map of the header fields and their values according to the 
{@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ}.
+     * @throws IOException If the field cannot be read or skipped.
+     */
+    public static Map<String, byte[]> unravelQcow2Header(InputStream 
qcow2InputStream, String qcow2LogReference) throws IOException {
+        Map<String, byte[]> result = new HashMap<>();
+
+        LOGGER.debug(String.format("Unraveling QCOW2 [%s] headers.", 
qcow2LogReference));
+        for (Qcow2HeaderField qcow2Header : Qcow2HeaderField.values()) {
+            if (!SET_OF_HEADER_FIELDS_TO_READ.contains(qcow2Header)) {
+                skipHeader(qcow2InputStream, qcow2Header, qcow2LogReference);
+                continue;
+            }
+
+            byte[] headerValue = readHeader(qcow2InputStream, qcow2Header, 
qcow2LogReference);
+            result.put(qcow2Header.name(), headerValue);
+        }
+
+        return result;
+    }
+
+    /**
+     * Skips the field's length in the InputStream.
+     * @param qcow2InputStream InputStream of the QCOW2 being unraveled.
+     * @param field Field being skipped (name and length).
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws IOException If the bytes skipped do not match the field length.
+     */
+    protected static void skipHeader(InputStream qcow2InputStream, 
Qcow2HeaderField field, String qcow2LogReference) throws IOException {
+        LOGGER.trace(String.format("Skipping field [%s] of QCOW2 [%s].", 
field, qcow2LogReference));
+
+        if (qcow2InputStream.skip(field.getLength()) != field.getLength()) {
+            throw new IOException(String.format("Unable to skip field [%s] of 
QCOW2 [%s].", field, qcow2LogReference));
+        }
+    }
+
+    /**
+     * Reads the field's length in the InputStream.
+     * @param qcow2InputStream InputStream of the QCOW2 being unraveled.
+     * @param field Field being read (name and length).
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws IOException If the bytes read do not match the field length.
+     */
+    protected static byte[] readHeader(InputStream qcow2InputStream, 
Qcow2HeaderField field, String qcow2LogReference) throws IOException {
+        byte[] readBytes = new byte[field.getLength()];
+
+        LOGGER.trace(String.format("Reading field [%s] of QCOW2 [%s].", field, 
qcow2LogReference));
+        if (qcow2InputStream.read(readBytes) != field.getLength()) {
+            throw new IOException(String.format("Unable to read field [%s] of 
QCOW2 [%s].", field, qcow2LogReference));
+        }
+
+        LOGGER.trace(String.format("Read %s as field [%s] of QCOW2 [%s].", 
ArrayUtils.toString(readBytes), field, qcow2LogReference));
+        return readBytes;
+    }
+
+    /**
+     * Validates the values of the header fields {@link 
Qcow2HeaderField#MAGIC}, {@link Qcow2HeaderField#BACKING_FILE_OFFSET}, and 
{@link Qcow2HeaderField#INCOMPATIBLE_FEATURES}.
+     * @param headerFieldsAndValues A map of the header fields and their 
values.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the QCOW2 does not contain the QCOW magic 
string or contains a backing file reference or incompatible features.
+     */
+    public static void validateQcow2HeaderFields(Map<String, byte[]> 
headerFieldsAndValues, String qcow2LogReference) throws SecurityException{
+        byte[] fieldValue = 
headerFieldsAndValues.get(Qcow2HeaderField.MAGIC.name());
+        validateQcowMagicString(fieldValue, qcow2LogReference);
+
+        fieldValue = 
headerFieldsAndValues.get(Qcow2HeaderField.BACKING_FILE_OFFSET.name());
+        
validateAbsenceOfBackingFileReference(NumbersUtil.bytesToLong(fieldValue), 
qcow2LogReference);
+
+        fieldValue = 
headerFieldsAndValues.get(Qcow2HeaderField.INCOMPATIBLE_FEATURES.name());
+        validateAbsenceOfIncompatibleFeatures(fieldValue, qcow2LogReference);
+    }
+
+    /**
+     * Verifies if the first 4 bytes of the header are the QCOW magic string. 
Throws an exception if not.
+     * @param headerMagicString The first 4 bytes of the header.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the header's magic string is not the QCOW 
magic string.
+     */
+    private static void validateQcowMagicString(byte[] headerMagicString, 
String qcow2LogReference) throws SecurityException {
+        LOGGER.debug(String.format("Verifying if [%s] has a valid QCOW magic 
string.", qcow2LogReference));
+
+        if (!Arrays.equals(QCOW_MAGIC_STRING, headerMagicString)) {
+            throw new SecurityException(String.format("[%s] is not a valid 
QCOW2 because its first 4 bytes are not the QCOW magic string.", 
qcow2LogReference));
+        }
+
+        LOGGER.debug(String.format("[%s] has a valid QCOW magic string.", 
qcow2LogReference));
+    }
+
+    /**
+     * Verifies if the QCOW2 has a backing file and throws an exception if so.
+     * @param backingFileOffset The backing file offset value of the QCOW2 
header.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the QCOW2 has a backing file reference.
+     */
+    private static void validateAbsenceOfBackingFileReference(long 
backingFileOffset, String qcow2LogReference) throws SecurityException {
+        LOGGER.debug(String.format("Verifying if [%s] has a backing file 
reference.", qcow2LogReference));
+
+        if (backingFileOffset != 0) {
+            throw new SecurityException(String.format("[%s] has a backing file 
reference. This can be an attack to the infrastructure; therefore, we will not 
accept" +
+                    " this QCOW2.", qcow2LogReference));
+        }
+
+        LOGGER.debug(String.format("[%s] does not have a backing file 
reference.", qcow2LogReference));
+    }
+
+    /**
+     * Verifies if the QCOW2 has incompatible features and throw an exception 
if it has an external data file reference or unknown incompatible features.
+     * @param incompatibleFeatures The incompatible features bytes of the 
QCOW2 header.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the QCOW2 has an external data file 
reference or unknown incompatible features.
+     */
+    private static void validateAbsenceOfIncompatibleFeatures(byte[] 
incompatibleFeatures, String qcow2LogReference) throws SecurityException {
+        LOGGER.debug(String.format("Verifying if [%s] has incompatible 
features.", qcow2LogReference));
+
+        if (NumbersUtil.bytesToLong(incompatibleFeatures) == 0) {
+            LOGGER.debug(String.format("[%s] does not have incompatible 
features.", qcow2LogReference));
+            return;
+        }
+
+        LOGGER.debug(String.format("[%s] has incompatible features.", 
qcow2LogReference));
+
+        validateAbsenceOfExternalDataFileReference(incompatibleFeatures, 
qcow2LogReference);
+        validateAbsenceOfUnknownIncompatibleFeatures(incompatibleFeatures, 
qcow2LogReference);
+    }
+
+    /**
+     * Verifies if the QCOW2 has an external data file reference and throw an 
exception if so.
+     * @param incompatibleFeatures The incompatible features bytes of the 
QCOW2 header.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the QCOW2 has an external data file 
reference.
+     */
+    private static void validateAbsenceOfExternalDataFileReference(byte[] 
incompatibleFeatures, String qcow2LogReference) throws SecurityException {
+        LOGGER.debug(String.format("Verifying if [%s] has an external data 
file reference.", qcow2LogReference));
+
+        if ((incompatibleFeatures[EXTERNAL_DATA_FILE_BYTE_POSITION] & 
EXTERNAL_DATA_FILE_BITMASK) != 0) {
+            throw new SecurityException(String.format("[%s] has an external 
data file reference. This can be an attack to the infrastructure; therefore, we 
will discard" +
+                    " this file.", qcow2LogReference));
+        }
+
+        LOGGER.info(String.format("[%s] does not have an external data file 
reference.", qcow2LogReference));
+    }
+
+    /**
+     * Verifies if the QCOW2 has unknown incompatible features and throw an 
exception if so.
+     * <br/><br/>
+     * Unknown incompatible features are those with bit greater than
+     * {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT}, which will 
be the represented by bytes in positions greater than
+     * {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE} (in Big 
Endian order). Therefore, we expect that those bytes are always zero. If not, 
an exception is thrown.
+     * @param incompatibleFeatures The incompatible features bytes of the 
QCOW2 header.
+     * @param qcow2LogReference A reference (like the filename) of the QCOW2 
being unraveled to print in the logs and exceptions.
+     * @throws SecurityException If the QCOW2 has unknown incompatible 
features.
+     */
+    private static void validateAbsenceOfUnknownIncompatibleFeatures(byte[] 
incompatibleFeatures, String qcow2LogReference) throws SecurityException {
+        LOGGER.debug(String.format("Verifying if [%s] has unknown incompatible 
features [%s].", qcow2LogReference, ArrayUtils.toString(incompatibleFeatures)));
+
+        for (int byteNum = incompatibleFeatures.length - 1; byteNum >= 0; 
byteNum--) {
+            int bytePosition = incompatibleFeatures.length - 1 - byteNum;
+            LOGGER.trace(String.format("Looking for unknown incompatible 
feature bit in position [%s].", bytePosition));
+
+            byte bitmask = 0;
+            if (byteNum == INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE) {
+                bitmask = ((1 << INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT) - 1);
+            }
+
+            LOGGER.trace(String.format("Bitmask for byte in position [%s] is 
[%s].", bytePosition, Integer.toBinaryString(bitmask)));
+
+            int featureBit = incompatibleFeatures[bytePosition] & ~bitmask;
+            if (featureBit != 0) {
+                throw new SecurityException(String.format("Found unknown 
incompatible feature bit [%s] in byte [%s] of [%s]. This can be an attack to 
the infrastructure; " +
+                        "therefore, we will discard this QCOW2.", featureBit, 
bytePosition + Qcow2HeaderField.INCOMPATIBLE_FEATURES.getOffset(), 
qcow2LogReference));
+            }
+
+            LOGGER.trace(String.format("Did not find unknown incompatible 
feature in position [%s].", bytePosition));
+        }
+
+        LOGGER.info(String.format("[%s] does not have unknown incompatible 
features.", qcow2LogReference));
+    }
+
+}
diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
index 41cf11025a1..ceaa37926e9 100644
--- 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java
@@ -71,6 +71,7 @@ import 
org.apache.cloudstack.storage.command.UploadStatusCommand;
 import 
org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand;
 import org.apache.cloudstack.storage.configdrive.ConfigDrive;
 import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder;
+import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector;
 import org.apache.cloudstack.storage.template.DownloadManager;
 import org.apache.cloudstack.storage.template.DownloadManagerImpl;
 import org.apache.cloudstack.storage.template.UploadEntity;
@@ -3482,8 +3483,19 @@ public class NfsSecondaryStorageResource extends 
ServerResourceBase implements S
             return result;
         }
 
+        String finalFilename = resourcePath + "/" + templateFilename;
+
+        if (ImageStoreUtil.isCorrectExtension(finalFilename, "qcow2")) {
+            try {
+                Qcow2Inspector.validateQcow2File(finalFilename);
+            } catch (RuntimeException e) {
+                s_logger.error(String.format("Uploaded file [%s] is not a 
valid QCOW2.", finalFilename), e);
+                return "The uploaded file is not a valid QCOW2. Ask the 
administrator to check the logs for more details.";
+            }
+        }
+
         // Set permissions for the downloaded template
-        File downloadedTemplate = new File(resourcePath + "/" + 
templateFilename);
+        File downloadedTemplate = new File(finalFilename);
         _storage.setWorldReadableAndWriteable(downloadedTemplate);
 
         // Set permissions for template/volume.properties
diff --git 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java
 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java
index 11878ae6400..c946614934b 100644
--- 
a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java
+++ 
b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java
@@ -16,8 +16,6 @@
 // under the License.
 package org.apache.cloudstack.storage.template;
 
-import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -51,9 +49,11 @@ import 
org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType
 import org.apache.cloudstack.storage.resource.IpTablesHelper;
 import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource;
 import org.apache.cloudstack.storage.resource.SecondaryStorageResource;
+import org.apache.cloudstack.storage.formatinspector.Qcow2HeaderField;
+import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector;
 import org.apache.cloudstack.utils.security.ChecksumValue;
 import org.apache.cloudstack.utils.security.DigestHelper;
-import org.apache.commons.lang3.StringUtils;
+
 import org.apache.log4j.Logger;
 
 import com.cloud.agent.api.storage.DownloadAnswer;
@@ -91,7 +91,9 @@ import com.cloud.utils.component.ManagerBase;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.Proxy;
 import com.cloud.utils.script.Script;
-import com.cloud.utils.storage.QCOW2Utils;
+import com.cloud.utils.StringUtils;
+
+import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
 
 public class DownloadManagerImpl extends ManagerBase implements 
DownloadManager {
     private String _name;
@@ -365,11 +367,17 @@ public class DownloadManagerImpl extends ManagerBase 
implements DownloadManager
             // The QCOW2 is the only format with a header,
             // and as such can be easily read.
 
-            try (InputStream inputStream = td.getS3ObjectInputStream();) {
-                dnld.setTemplatesize(QCOW2Utils.getVirtualSize(inputStream, 
false));
-            }
-            catch (IOException e) {
-                result = "Couldn't read QCOW2 virtual size. Error: " + 
e.getMessage();
+            try (InputStream inputStream = td.getS3ObjectInputStream()) {
+                Map<String, byte[]> qcow2HeaderFieldsAndValues = 
Qcow2Inspector.unravelQcow2Header(inputStream, td.getDownloadUrl());
+                
Qcow2Inspector.validateQcow2HeaderFields(qcow2HeaderFieldsAndValues, 
td.getDownloadUrl());
+
+                
dnld.setTemplatesize(NumbersUtil.bytesToLong(qcow2HeaderFieldsAndValues.get(Qcow2HeaderField.SIZE.name())));
+            } catch (IOException ex) {
+                result = String.format("Unable to read QCOW2 metadata. Error: 
%s", ex.getMessage());
+                LOGGER.error(result, ex);
+            } catch (SecurityException ex) {
+                result = String.format("[%s] is not a valid QCOW2:", 
td.getDownloadUrl());
+                LOGGER.error(result, ex);
             }
 
         }
@@ -515,8 +523,19 @@ public class DownloadManagerImpl extends ManagerBase 
implements DownloadManager
             return result;
         }
 
+        String finalFilename = resourcePath + "/" + templateFilename;
+
+        if (ImageFormat.QCOW2.equals(dnld.getFormat())) {
+            try {
+                Qcow2Inspector.validateQcow2File(finalFilename);
+            } catch (RuntimeException e) {
+                LOGGER.error(String.format("The downloaded file [%s] is not a 
valid QCOW2.", finalFilename), e);
+                return "The downloaded file is not a valid QCOW2. Ask the 
administrator to check the logs for more details.";
+            }
+        }
+
         // Set permissions for the downloaded template
-        File downloadedTemplate = new File(resourcePath + "/" + 
templateFilename);
+        File downloadedTemplate = new File(finalFilename);
 
         _storage.setWorldReadableAndWriteable(downloadedTemplate);
         setPermissionsForTheDownloadedTemplate(resourcePath, resourceType);

Reply via email to