Skip to content

createVMFromBackup throws NPE when the template has "Password Enabled" - the VM is created correctly but the API call fails #13816

Description

@DeviC3

problem

Creating an instance from a backup (createVMFromBackup with startvm=true) fails with a NullPointerException in the API response, even though the instance is created correctly, its volumes are fully restored from the backup and the VM boots and runs.

The user-visible effect is a red error in the UI with no indication that the operation actually succeeded, and the generated password is not returned.

2026-08-05 18:37:06,819 ERROR [c.c.a.ApiAsyncJobDispatcher] (API-Job-Executor-20:[ctx-f743d928, job-138]) (logid:5f51fca6) Unexpected exception while executing org.apache.cloudstack.api.command.admin.vm.CreateVMFromBackupCmdByAdmin java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because the return value of "com.cloud.utils.Pair.second()" is null
	at com.cloud.vm.UserVmManagerImpl.startVirtualMachine(UserVmManagerImpl.java:5342)
	at com.cloud.vm.UserVmManagerImpl.restoreVMFromBackup(UserVmManagerImpl.java:9848)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at jdk.proxy3/jdk.proxy3.$Proxy202.restoreVMFromBackup(Unknown Source)
	at org.apache.cloudstack.api.command.user.vm.CreateVMFromBackupCmd.execute(CreateVMFromBackupCmd.java:124)
	at com.cloud.api.ApiDispatcher.dispatch(ApiDispatcher.java:173)
	at com.cloud.api.ApiAsyncJobDispatcher.runJob(ApiAsyncJobDispatcher.java:110)
	at org.apache.cloudstack.framework.jobs.impl.AsyncJobManagerImpl$5.runInContext(AsyncJobManagerImpl.java:698)
	...
2026-08-05 18:37:06,820 DEBUG [o.a.c.f.j.i.AsyncJobManagerImpl] (API-Job-Executor-20:[ctx-f743d928, job-138]) (logid:5f51fca6) Complete async job-138, jobStatus: FAILED, resultCode: 530, result: {"uuidList":[],"errorcode":"530","errortext":"Cannot invoke \"java.util.Map.get(Object)\" because the return value of \"com.cloud.utils.Pair.second()\" is null"}

Root cause

The trigger is a source VM whose template has enable_password = 1 ("Password Enabled").

restoreVMFromBackup starts the VM twice:

  1. First with Param.ReturnAfterVolumePrepare = true - UserVmManagerImpl.java#L9808. On this run vm.isUpdateParameters() is still true, so the params map is built (L5795) and the flag is then cleared - vm.setUpdateParameters(false) at L5843.
  2. Then, when cmd.getStartVm() is true, it performs the real start - UserVmManagerImpl.java#L9848.

On the second run vm.isUpdateParameters() is already false, so params stays null and the returned Pair.second() is null. The private startVirtualMachine overload then dereferences it unconditionally whenever the template has a password enabled - UserVmManagerImpl.java#L5339-L5342:

VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId());
if (template.isEnablePassword()) {
    // this value is not being sent to the backend; need only for api display purposes
    vm.setPassword((String)vmParamPair.second().get(VirtualMachineProfile.Param.VmPassword));
}

The exception is thrown outside the try block at L9806-L9829, so the expunge() cleanup in the catch does not run. That is why the VM survives in a correct, running state and only the API response fails - the VM.CREATE.FROM.BACKUP event itself is recorded as Completed.

The same unguarded dereference is present on main - UserVmManagerImpl.java#L5597.

versions

  • CloudStack 4.22.1.0 (packages cloudstack-management-4.22.1.0-1, cloudstack-agent-4.22.1.0-1); code inspected on tag 4.22.1.0 and on main
  • Hypervisor: KVM on AlmaLinux 9.8, qemu-kvm 10.1.0, libvirt 11.10.0, OpenJDK 17.0.20
  • Primary storage: Ceph RBD (Ceph 20.2.2)
  • Backup provider: NAS Backup & Recovery (backup.framework.provider.plugin = nas), repository on an NFS export backed by CephFS
  • Database: MySQL 8.0.46
  • 3 management servers, single advanced zone

The steps to reproduce the bug

  1. Register a template with Password Enabled = true (enable_password = 1).
  2. Deploy an instance from that template.
  3. Enable the backup framework with the nas provider, assign a backup offering to the instance and take a backup.
  4. Go to Backups, select the backup, choose Create Instance from Backup and leave Start Instance = Yes.
  5. The job fails with the NPE above (errorcode 530), but listVirtualMachines shows the new instance in state Running with its volumes correctly restored.

The bug does not appear when the template has Password Enabled unset, or when Start Instance = No is selected.

What to do about it?

Workaround for users: create the instance with Start Instance = No and start it afterwards - the manual start path (StartVMCmd) only uses Pair.first(), so it is unaffected.

Minimal fix - guard the dereference at UserVmManagerImpl.java:5342:

if (template.isEnablePassword() && vmParamPair.second() != null) {
    vm.setPassword((String)vmParamPair.second().get(VirtualMachineProfile.Param.VmPassword));
}

A cleaner fix would be for restoreVMFromBackup to reuse the params map returned by the first startVirtualMachine call instead of letting the second call re-derive it from a flag the first call has already consumed - that would also make the generated password available in the API response, which is arguably the intended behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions