Copilot commented on code in PR #13020:
URL: https://github.com/apache/cloudstack/pull/13020#discussion_r3947534807


##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteDiskOnlyVMSnapshotCommandWrapper.java:
##########
@@ -53,6 +57,40 @@ public Answer execute(DeleteDiskOnlyVmSnapshotCommand 
command, LibvirtComputingR
                 return new Answer(command, e);
             }
         }
+
+        deleteNvramSnapshotIfNeeded(command, resource, storagePoolMgr, 
snapshotsToDelete);
         return new Answer(command, true, null);
     }
+
+    protected void deleteNvramSnapshotIfNeeded(DeleteDiskOnlyVmSnapshotCommand 
command, LibvirtComputingResource resource, KVMStoragePoolManager 
storagePoolMgr,
+            List<DataTO> snapshotsToDelete) {
+        if (StringUtils.isBlank(command.getNvramSnapshotPath())) {
+            return;
+        }
+
+        try {
+            KVMStoragePool storagePool;
+            if (command.getPrimaryDataStore() != null) {
+                PrimaryDataStoreTO dataStore = command.getPrimaryDataStore();
+                storagePool = 
storagePoolMgr.getStoragePool(dataStore.getPoolType(), dataStore.getUuid());
+            } else {
+                SnapshotObjectTO rootVolumeSnapshot = 
snapshotsToDelete.stream()
+                        .map(SnapshotObjectTO.class::cast)
+                        .filter(snapshotObjectTO -> 
Volume.Type.ROOT.equals(snapshotObjectTO.getVolume().getVolumeType()))
+                        .findFirst()
+                        .orElse(null);
+
+                if (rootVolumeSnapshot == null) {
+                    logger.warn("Unable to locate the root volume snapshot 
while deleting NVRAM snapshot [{}].", command.getNvramSnapshotPath());
+                    return;
+                }
+
+                storagePool = 
resource.getLibvirtUtilitiesHelper().getPrimaryPoolFromDataTo(rootVolumeSnapshot,
 storagePoolMgr);
+            }
+
+            
Files.deleteIfExists(Path.of(storagePool.getLocalPathFor(command.getNvramSnapshotPath())));

Review Comment:
   NVRAM sidecar deletion uses 
storagePool.getLocalPathFor(command.getNvramSnapshotPath()) without validating 
that the provided path is a safe relative path. Because getLocalPathFor() is a 
simple string concatenation, a crafted value containing ".." or an absolute 
path could lead to deleting files outside the pool mount.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertDiskOnlyVMSnapshotCommandWrapper.java:
##########
@@ -108,4 +116,88 @@ public Answer execute(RevertDiskOnlyVmSnapshotCommand cmd, 
LibvirtComputingResou
 
         return new RevertDiskOnlyVmSnapshotAnswer(cmd, volumeObjectTos);
     }
+
+    protected SnapshotObjectTO getRootVolumeSnapshot(List<SnapshotObjectTO> 
snapshotObjectTos) {
+        return snapshotObjectTos.stream()
+                .filter(snapshotObjectTO -> 
Volume.Type.ROOT.equals(snapshotObjectTO.getVolume().getVolumeType()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    protected void validateNvramRevertState(RevertDiskOnlyVmSnapshotCommand 
cmd, LibvirtComputingResource resource, SnapshotObjectTO rootVolumeSnapshot,
+            KVMStoragePoolManager storagePoolMgr) throws IOException, 
LibvirtException {
+        String activeNvramPath = resource.getUefiNvramPath(cmd.getVmUuid());
+        if (StringUtils.isBlank(cmd.getNvramSnapshotPath())) {
+            if (cmd.isUefiEnabled()) {
+                throw new IOException(String.format("Cannot safely revert 
disk-only VM snapshot for UEFI VM [%s] because the snapshot does not contain 
NVRAM state.",
+                        cmd.getVmName()));
+            }
+            return;
+        }
+
+        if (StringUtils.isBlank(activeNvramPath)) {
+            throw new IOException(String.format("Unable to determine the 
active UEFI NVRAM path for VM [%s].", cmd.getVmName()));
+        }
+
+        Path snapshotNvramPath = 
getNvramSnapshotAbsolutePath(cmd.getNvramSnapshotPath(), rootVolumeSnapshot, 
resource, storagePoolMgr);
+        if (!Files.exists(snapshotNvramPath)) {
+            throw new IOException(String.format("Unable to find the UEFI NVRAM 
snapshot [%s] for VM [%s].", cmd.getNvramSnapshotPath(), cmd.getVmName()));
+        }
+    }
+
+    protected void restoreNvramIfNeeded(RevertDiskOnlyVmSnapshotCommand cmd, 
LibvirtComputingResource resource, SnapshotObjectTO rootVolumeSnapshot,
+            KVMStoragePoolManager storagePoolMgr) throws IOException, 
LibvirtException {
+        if (StringUtils.isBlank(cmd.getNvramSnapshotPath())) {
+            return;
+        }
+
+        String activeNvramPath = resource.getUefiNvramPath(cmd.getVmUuid());
+        if (StringUtils.isBlank(activeNvramPath)) {
+            throw new IOException(String.format("Unable to determine the 
active UEFI NVRAM path for VM [%s].", cmd.getVmName()));
+        }
+
+        Path snapshotNvramPath = 
getNvramSnapshotAbsolutePath(cmd.getNvramSnapshotPath(), rootVolumeSnapshot, 
resource, storagePoolMgr);
+        if (!Files.exists(snapshotNvramPath)) {
+            throw new IOException(String.format("Unable to find the UEFI NVRAM 
snapshot [%s] for VM [%s].", cmd.getNvramSnapshotPath(), cmd.getVmName()));
+        }
+
+        replaceNvramAtomically(snapshotNvramPath, Path.of(activeNvramPath));
+    }
+
+    protected void replaceNvramAtomically(Path snapshotNvramPath, Path 
activeNvramPath) throws IOException {
+        Path targetDirectory = activeNvramPath.getParent();
+        if (targetDirectory != null) {
+            Files.createDirectories(targetDirectory);
+        }
+
+        Path temporaryNvramPath = Files.createTempFile(targetDirectory, 
activeNvramPath.getFileName().toString(), ".tmp");
+        try {
+            copyNvramSnapshotToTemporaryPath(snapshotNvramPath, 
temporaryNvramPath);
+            moveTemporaryNvramIntoPlace(temporaryNvramPath, activeNvramPath);
+        } finally {
+            Files.deleteIfExists(temporaryNvramPath);
+        }
+    }
+
+    protected void copyNvramSnapshotToTemporaryPath(Path snapshotNvramPath, 
Path temporaryNvramPath) throws IOException {
+        Files.copy(snapshotNvramPath, temporaryNvramPath, 
StandardCopyOption.REPLACE_EXISTING);
+    }
+
+    protected void moveTemporaryNvramIntoPlace(Path temporaryNvramPath, Path 
activeNvramPath) throws IOException {
+        try {
+            Files.move(temporaryNvramPath, activeNvramPath, 
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+        } catch (AtomicMoveNotSupportedException e) {
+            Files.move(temporaryNvramPath, activeNvramPath, 
StandardCopyOption.REPLACE_EXISTING);
+        }
+    }
+
+    protected Path getNvramSnapshotAbsolutePath(String nvramSnapshotPath, 
SnapshotObjectTO rootVolumeSnapshot, LibvirtComputingResource resource,
+            KVMStoragePoolManager storagePoolMgr) throws IOException, 
LibvirtException {
+        if (rootVolumeSnapshot == null) {
+            throw new IOException("Unable to locate the root volume snapshot 
while handling the UEFI NVRAM state.");
+        }
+
+        KVMStoragePool storagePool = 
resource.getLibvirtUtilitiesHelper().getPrimaryPoolFromDataTo(rootVolumeSnapshot,
 storagePoolMgr);
+        return Path.of(storagePool.getLocalPathFor(nvramSnapshotPath));
+    }

Review Comment:
   getNvramSnapshotAbsolutePath() passes nvramSnapshotPath directly into 
storagePool.getLocalPathFor(). Since getLocalPathFor() does not sanitize 
traversal (it concatenates strings), an unexpected nvramSnapshotPath containing 
".." or an absolute path could escape the pool mount and read/overwrite 
arbitrary files when restoring NVRAM.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java:
##########
@@ -88,44 +103,71 @@ protected Answer 
takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotComma
             dm = resource.getDomain(conn, vmName);
 
             if (dm == null) {
-                return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, 
String.format("Creation of disk-only VM Snapshot failed as we could not find 
the VM [%s].", vmName), null);
+                answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false,
+                        String.format("Creation of disk-only VM Snapshot 
failed as we could not find the VM [%s].", vmName), null, null);
+                return answer;
             }
 
             VMSnapshotTO target = cmd.getTarget();
             Pair<String, Map<String, Pair<Long, String>>> 
snapshotXmlAndVolumeToNewPathMap = 
createSnapshotXmlAndNewVolumePathMap(volumeObjectTOS, disks, target, resource);
+            if (shouldFreezeVmFilesystemsForSnapshot(cmd)) {
+                // The guest-agent freeze flushes guest filesystems; suspend 
below prevents concurrent UEFI NVRAM writes.
+                freezeVmFilesystems(dm, vmName);
+                filesystemsFrozenByThisWrapper = true;
+                verifyVmFilesystemsFrozen(dm, vmName);
+            }
+            if (shouldSuspendVmForSnapshot(cmd)) {
+                suspendedByThisWrapper = suspendVmIfNeeded(dm);
+            }
+            nvramSnapshotPath = backupNvramIfNeeded(cmd, resource);
 
-            dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), 
getFlagsToUseForRunningVmSnapshotCreation(target));
+            dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), 
getFlagsToUseForRunningVmSnapshotCreation(target, 
filesystemsFrozenByThisWrapper));
 
-            return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, 
snapshotXmlAndVolumeToNewPathMap.second());
-        } catch (LibvirtException e) {
+            postSnapshotCleanupIssue = recoverVmAfterSnapshot(dm, vmName, 
suspendedByThisWrapper, filesystemsFrozenByThisWrapper, 
postSnapshotCleanupIssue);
+            filesystemsFrozenByThisWrapper = false;
+            suspendedByThisWrapper = false;
+
+            answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, 
snapshotXmlAndVolumeToNewPathMap.second(), nvramSnapshotPath);
+        } catch (LibvirtException | IOException e) {
             String errorMsg = String.format("Creation of disk-only VM snapshot 
for VM [%s] failed due to %s.", vmName, e.getMessage());
             logger.error(errorMsg, e);
-            if (e.getMessage().contains("QEMU guest agent is not connected")) {
+            cleanupNvramSnapshotIfNeeded(cmd, resource, nvramSnapshotPath);
+            if (StringUtils.contains(e.getMessage(), "QEMU guest agent is not 
connected")) {
                 errorMsg = "QEMU guest agent is not connected. If the VM has 
been recently started, it might connect soon. Otherwise the VM does not have 
the" +
                         " guest agent installed; thus the QuiesceVM parameter 
is not supported.";
-                return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, 
errorMsg, null);
+                answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false, 
errorMsg, null, null);
+            } else {
+                answer = new CreateDiskOnlyVmSnapshotAnswer(cmd, false, 
e.getMessage(), null, null);

Review Comment:
   In the non-guest-agent error path, the failure Answer uses e.getMessage() 
directly. This can be null and also drops the VM context that was already 
assembled into errorMsg, making failures harder to diagnose from the management 
server/UI.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java:
##########
@@ -6334,6 +6336,15 @@ public String getSnapshotTemporaryPath(String diskPath, 
String snapshotName) {
         return String.join(File.separator, diskPathSplitted);
     }
 
+    public String getUefiNvramPath(String vmUuid) {
+        String nvramDirectory = 
uefiProperties.getProperty(LibvirtVMDef.GuestDef.GUEST_NVRAM_PATH);
+        if (StringUtils.isBlank(nvramDirectory) || 
StringUtils.isBlank(vmUuid)) {
+            return null;
+        }
+
+        return nvramDirectory + vmUuid + ".fd";

Review Comment:
   getUefiNvramPath() builds the NVRAM file path via string concatenation 
(nvramDirectory + vmUuid + ".fd"), which silently breaks if the configured 
directory does not end with a path separator. Using Path joining avoids relying 
on trailing slashes and keeps path handling consistent.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to