exec-sandbox 0.21.0__tar.gz → 0.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/PKG-INFO +1 -1
  2. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/pyproject.toml +1 -1
  3. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-qemu.sh +6 -2
  4. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/constants.py +17 -3
  5. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/migration_client.py +36 -18
  6. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/qemu_cmd.py +72 -5
  7. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/scheduler.py +13 -5
  8. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_migration_client.py +49 -16
  9. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_scheduler.py +301 -0
  10. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.editorconfig +0 -0
  11. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/actions/build-guest-agent/action.yml +0 -0
  12. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/actions/build-gvproxy-wrapper/action.yml +0 -0
  13. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/actions/build-tiny-init/action.yml +0 -0
  14. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/actions/build-vm-images/action.yml +0 -0
  15. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/actions/setup-vm-infra/action.yml +0 -0
  16. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/runs-on.yml +0 -0
  17. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/workflows/release.yml +0 -0
  18. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.github/workflows/test.yml +0 -0
  19. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/.gitignore +0 -0
  20. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/CLAUDE.md +0 -0
  21. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/LICENSE +0 -0
  22. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/Makefile +0 -0
  23. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/README.md +0 -0
  24. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/SECURITY.md +0 -0
  25. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/cicd/version.sh +0 -0
  26. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/.gitignore +0 -0
  27. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/Cargo.lock +0 -0
  28. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/Cargo.toml +0 -0
  29. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/Makefile +0 -0
  30. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/connection.rs +0 -0
  31. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/constants.rs +0 -0
  32. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/error.rs +0 -0
  33. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/file_io.rs +0 -0
  34. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/init.rs +0 -0
  35. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/main.rs +0 -0
  36. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/oom_guard.rs +0 -0
  37. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/packages.rs +0 -0
  38. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/execute.rs +0 -0
  39. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/mod.rs +0 -0
  40. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/scripts/js_repl.mjs +0 -0
  41. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/scripts/python_repl.py +0 -0
  42. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/scripts/shell_repl.sh +0 -0
  43. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/spawn.rs +0 -0
  44. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/repl/wrappers.rs +0 -0
  45. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/types.rs +0 -0
  46. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/guest-agent/src/validation.rs +0 -0
  47. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/gvproxy-wrapper/.gitignore +0 -0
  48. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/gvproxy-wrapper/Makefile +0 -0
  49. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/gvproxy-wrapper/go.mod +0 -0
  50. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/gvproxy-wrapper/go.sum +0 -0
  51. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/gvproxy-wrapper/main.go +0 -0
  52. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/images/kernel/alpine-virt-aarch64.config +0 -0
  53. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/images/kernel/alpine-virt-x86_64.config +0 -0
  54. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/images/kernel/exec-sandbox.config +0 -0
  55. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/benchmark_density.py +0 -0
  56. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/benchmark_latency.py +0 -0
  57. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/benchmark_optimizer.py +0 -0
  58. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-guest-agent.sh +0 -0
  59. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-images.sh +0 -0
  60. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-initramfs.sh +0 -0
  61. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-kernel.sh +0 -0
  62. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-qcow2.sh +0 -0
  63. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build-tiny-init.sh +0 -0
  64. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/build_package_catalogs.py +0 -0
  65. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/ci-resource-monitor.sh +0 -0
  66. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/ci_diagnose.py +0 -0
  67. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/extract-kernel.sh +0 -0
  68. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/extract-vmlinux.sh +0 -0
  69. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/scripts/upgrade-versions.sh +0 -0
  70. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/__init__.py +0 -0
  71. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/_imports.py +0 -0
  72. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/_logging.py +0 -0
  73. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/admission.py +0 -0
  74. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/aio_utils.py +0 -0
  75. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/asset_downloader.py +0 -0
  76. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/assets.py +0 -0
  77. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/balloon_client.py +0 -0
  78. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/base_cache_manager.py +0 -0
  79. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/cgroup.py +0 -0
  80. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/cli.py +0 -0
  81. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/config.py +0 -0
  82. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/disk_snapshot_manager.py +0 -0
  83. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/dns_filter.py +0 -0
  84. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/exceptions.py +0 -0
  85. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/guest_agent_protocol.py +0 -0
  86. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/guest_channel.py +0 -0
  87. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/gvproxy.py +0 -0
  88. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/hash_utils.py +0 -0
  89. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/lock_utils.py +0 -0
  90. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/memory_snapshot_manager.py +0 -0
  91. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/models.py +0 -0
  92. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/overlay_pool.py +0 -0
  93. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/package_validator.py +0 -0
  94. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/permission_utils.py +0 -0
  95. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/platform_utils.py +0 -0
  96. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/port_forward.py +0 -0
  97. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/process_registry.py +0 -0
  98. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/py.typed +0 -0
  99. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/qemu_storage_daemon.py +0 -0
  100. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/qemu_vm.py +0 -0
  101. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/qmp_exchange.py +0 -0
  102. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/resource_cleanup.py +0 -0
  103. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/resources/npm_top_10k.json +0 -0
  104. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/resources/pypi_top_10k.json +0 -0
  105. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/session.py +0 -0
  106. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/settings.py +0 -0
  107. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/socket_auth.py +0 -0
  108. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/subprocess_utils.py +0 -0
  109. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/system_probes.py +0 -0
  110. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/validation.py +0 -0
  111. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/vm_manager.py +0 -0
  112. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/vm_timing.py +0 -0
  113. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/vm_types.py +0 -0
  114. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/vm_working_directory.py +0 -0
  115. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/src/exec_sandbox/warm_vm_pool.py +0 -0
  116. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/stubs/backports/__init__.pyi +0 -0
  117. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/stubs/backports/zstd.pyi +0 -0
  118. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/__init__.py +0 -0
  119. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/benchmarks/__init__.py +0 -0
  120. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/benchmarks/test_bench_core.py +0 -0
  121. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/conftest.py +0 -0
  122. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/cpu_helpers.py +0 -0
  123. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_admission.py +0 -0
  124. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_aio_utils.py +0 -0
  125. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_asset_downloader.py +0 -0
  126. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_assets.py +0 -0
  127. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_asyncio_cleanup.py +0 -0
  128. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_balloon_client.py +0 -0
  129. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_balloon_integration.py +0 -0
  130. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_bun_ffi_security.py +0 -0
  131. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_cgroup.py +0 -0
  132. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_cli.py +0 -0
  133. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_config.py +0 -0
  134. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_consume_stream.py +0 -0
  135. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_dask_compatibility.py +0 -0
  136. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_device_security.py +0 -0
  137. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_disk_snapshot_manager.py +0 -0
  138. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_dns_filter.py +0 -0
  139. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_dns_filtering.py +0 -0
  140. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_dns_resolv_hijack.py +0 -0
  141. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_ech_proxy_behavior.py +0 -0
  142. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_env_var_security.py +0 -0
  143. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_etc_hardening.py +0 -0
  144. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_execution_edge_cases.py +0 -0
  145. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_file_io_buffers.py +0 -0
  146. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_file_io_edge_cases.py +0 -0
  147. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_file_io_integrity.py +0 -0
  148. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_file_io_security.py +0 -0
  149. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_file_io_streaming.py +0 -0
  150. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_fork_output.py +0 -0
  151. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_fork_stdout.py +0 -0
  152. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_guest_agent_integration.py +0 -0
  153. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_guest_agent_protocol.py +0 -0
  154. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_image_tools.py +0 -0
  155. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_int_str_digits.py +0 -0
  156. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_kernel_attack_surface.py +0 -0
  157. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_loopback.py +0 -0
  158. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_memory_leaks.py +0 -0
  159. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_memory_optimization.py +0 -0
  160. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_memory_snapshot_manager.py +0 -0
  161. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_multiprocessing.py +0 -0
  162. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_nonroot_repl.py +0 -0
  163. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_orphan_vm_cpu.py +0 -0
  164. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_overlay_pool.py +0 -0
  165. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_package_validator.py +0 -0
  166. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_payload_integrity.py +0 -0
  167. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_permission_utils.py +0 -0
  168. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_pid1_zombie_reaping.py +0 -0
  169. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_platform_utils.py +0 -0
  170. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_port_forward.py +0 -0
  171. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_process_registry.py +0 -0
  172. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_psutil_compatibility.py +0 -0
  173. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_qemu_attack_surface.py +0 -0
  174. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_qemu_storage_daemon.py +0 -0
  175. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_resource_cleanup.py +0 -0
  176. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_rng_l1_snapshot.py +0 -0
  177. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_rng_quality.py +0 -0
  178. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_session.py +0 -0
  179. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_session_guard_and_io.py +0 -0
  180. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_signal_isolation.py +0 -0
  181. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_socket_auth.py +0 -0
  182. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_speculative_execution.py +0 -0
  183. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_streaming.py +0 -0
  184. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_subprocess_utils.py +0 -0
  185. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_vm_capacity.py +0 -0
  186. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_vm_manager.py +0 -0
  187. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_vm_working_directory.py +0 -0
  188. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_warm_vm_pool.py +0 -0
  189. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_wasm_compatibility.py +0 -0
  190. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tests/test_write_worker.py +0 -0
  191. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/.gitignore +0 -0
  192. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/Cargo.lock +0 -0
  193. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/Cargo.toml +0 -0
  194. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/Makefile +0 -0
  195. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/src/device.rs +0 -0
  196. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/src/main.rs +0 -0
  197. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/tiny-init/src/sys.rs +0 -0
  198. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/uv.lock +0 -0
  199. {exec_sandbox-0.21.0 → exec_sandbox-0.22.0}/versions.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exec-sandbox
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: Secure code execution in microVMs with QEMU
5
5
  Project-URL: Homepage, https://github.com/dualeai/exec-sandbox
6
6
  Project-URL: Documentation, https://github.com/dualeai/exec-sandbox#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "exec-sandbox"
7
- version = "0.21.0"
7
+ version = "0.22.0"
8
8
  description = "Secure code execution in microVMs with QEMU"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -101,8 +101,12 @@ cd "qemu-${QEMU_VERSION}"
101
101
  # --enable-fdt: device tree support, required for aarch64-softmmu (virt machine),
102
102
  # harmless for x86_64. Needs libfdt-dev.
103
103
  # --enable-kvm: KVM acceleration support.
104
- # --enable-linux-io-uring: io_uring async I/O backend for block drives (aio=io_uring).
105
- # Needs liburing-dev (>= 0.3). Falls back to aio=threads if unavailable at runtime.
104
+ # --enable-linux-io-uring: io_uring async I/O backend. Needs liburing-dev (>= 0.3).
105
+ # Two benefits: (1) block drives use aio=io_uring (falls back to aio=threads if
106
+ # unavailable at runtime), and (2) since QEMU 10.2 the main event loop itself is
107
+ # io_uring-based when liburing is linked — lower syscall/wakeup overhead on fd
108
+ # monitoring. Keeping this flag ensures BOTH; without it the main loop silently
109
+ # falls back to ppoll. (probe_io_uring_support() only gates the drive aio= mode.)
106
110
  # --enable-seccomp: seccomp sandbox for QEMU process (-sandbox on).
107
111
  # Needs libseccomp-dev (>= 2.3.0).
108
112
  # --enable-slirp: user-mode networking backend. Needs libslirp-dev.
@@ -402,17 +402,31 @@ Only used when socket activation is unavailable (sudo wraps QEMU, closing
402
402
  inherited fds). With socket activation, the socket is pre-created and no
403
403
  polling is needed."""
404
404
 
405
- MEMORY_SNAPSHOT_MIN_QEMU_VERSION: Final[tuple[int, int, int]] = (9, 0, 0)
405
+ MEMORY_SNAPSHOT_MIN_QEMU_VERSION: Final[tuple[int, int, int]] = (11, 0, 0)
406
406
  """Minimum QEMU version for mapped-ram + multifd migration capabilities.
407
- Changing capabilities requires bumping MEMORY_SNAPSHOT_FORMAT_VERSION."""
408
407
 
409
- MEMORY_SNAPSHOT_FORMAT_VERSION: Final[int] = 3
408
+ Floor is 11.0: it is the first release where mapped-ram coexists with
409
+ x-ignore-shared without a null-deref ("Bad address") crash, letting the template
410
+ and full-RAM paths share ONE unified capability list (no version fork). The
411
+ project pins QEMU 11.0.2, so this floor is free; older local installs fall back
412
+ to plain streaming migration. Changing capabilities requires bumping
413
+ MEMORY_SNAPSHOT_FORMAT_VERSION."""
414
+
415
+ MEMORY_SNAPSHOT_FORMAT_VERSION: Final[int] = 4
410
416
  """Migration format version. Bump when changing QMP capabilities or migration
411
417
  protocol to invalidate stale L1 cache entries. History:
412
418
  v1: Plain streaming format (no mapped-ram)
413
419
  v2: mapped-ram + multifd (QEMU >= 9.0)
414
420
  v3: x-ignore-shared VM templating — device-state-only vmstate + separate
415
421
  RAM file for COW sharing across VMs (QEMU >= 9.0)
422
+ v4: unified capability list — template path now also enables mapped-ram +
423
+ multifd alongside x-ignore-shared (QEMU >= 11.0 fixes the coexistence
424
+ crash), removing the version-forked branch.
425
+
426
+ NOTE: the migration stream layout is QEMU-version- AND parameter-specific — a
427
+ QEMU upgrade or a change to multifd-channels / direct-io (both baked into the
428
+ mapped-ram file layout) requires restore-QEMU == save-QEMU. Any such change must
429
+ bump this version so stale, incompatible snapshots are not restored.
416
430
  """
417
431
 
418
432
  # ============================================================================
@@ -197,28 +197,37 @@ class MigrationClient:
197
197
 
198
198
  Both sender (save) and receiver (restore) MUST set the same
199
199
  capabilities, otherwise QEMU rejects with "received capability is off".
200
+
201
+ NOTE (design constraints, verified QEMU 11.0):
202
+ - mapped-ram is INCOMPATIBLE with multifd-compression (seekable-fd
203
+ requirement) AND with migration TLS. Any snapshot compression or
204
+ encryption for a future S3 freeze/thaw path must therefore be done
205
+ EXTERNALLY (client-side zstd + AEAD), not via QEMU capabilities. The
206
+ sanctioned in-QEMU perf recovery is the `direct-io` migration parameter
207
+ (O_DIRECT), gated to the full-RAM path when that lands.
208
+ - CPR (cpr-reboot/transfer/exec) is NOT a resume-anywhere primitive: all
209
+ modes are same-host only (fd/RAM handoff to a peer QEMU), so they cannot
210
+ back S3 freeze/thaw. Their only use here would be in-place QEMU upgrades.
211
+ - The migration stream format is QEMU-version-specific; a QEMU bump
212
+ invalidates saved snapshots (see MEMORY_SNAPSHOT_FORMAT_VERSION).
200
213
  """
201
214
  if qemu_version is None or qemu_version < constants.MEMORY_SNAPSHOT_MIN_QEMU_VERSION:
202
215
  return
203
216
 
217
+ # Unified capability list (QEMU >= 11.0). Base is mapped-ram + multifd for
218
+ # seekable, parallel page I/O. Template mode adds x-ignore-shared to skip
219
+ # RAM pages backed by the shared memory-backend-file (device-state-only
220
+ # vmstate). mapped-ram + x-ignore-shared coexist safely only since 11.0
221
+ # (earlier releases null-deref on ignored blocks — the reason the floor is
222
+ # 11.0), so both paths share one branch-free list.
223
+ # Ref: QEMU commit "migration/mapped-ram: Fix x-ignore-shared snapshots"
224
+ # (Pawel Zmarzly, Nov 2025; reviewed by Peter Xu, Dec 2025)
225
+ capabilities = [
226
+ {"capability": "multifd", "state": True},
227
+ {"capability": "mapped-ram", "state": True},
228
+ ]
204
229
  if use_template:
205
- # VM Templating: x-ignore-shared skips RAM pages backed by shared
206
- # memory-backend-file. mapped-ram is INCOMPATIBLE with x-ignore-shared
207
- # in QEMU <= 10.x: mapped-ram's bitmap code dereferences null pointers
208
- # for ignored blocks, crashing with "Bad address". Fix merged for 11.0.
209
- # Ref: QEMU commit "migration/mapped-ram: Fix x-ignore-shared snapshots"
210
- # (Pawel Zmarzly, Nov 2025; reviewed by Peter Xu, Dec 2025)
211
- # TODO(qemu-11): Re-enable mapped-ram + multifd alongside x-ignore-shared
212
- # once QEMU >= 11.0 is the minimum version. This will give parallel I/O
213
- # for the device-state portion of the vmstate.
214
- capabilities = [
215
- {"capability": "x-ignore-shared", "state": True},
216
- ]
217
- else:
218
- capabilities = [
219
- {"capability": "multifd", "state": True},
220
- {"capability": "mapped-ram", "state": True},
221
- ]
230
+ capabilities.append({"capability": "x-ignore-shared", "state": True})
222
231
  resp = await self._execute(
223
232
  "migrate-set-capabilities",
224
233
  {"capabilities": capabilities},
@@ -263,6 +272,11 @@ class MigrationClient:
263
272
  # Valid = false). QEMU's hvf_handle_exception asserts isv==true, causing
264
273
  # SIGABRT. Stopping the vCPU first eliminates the race.
265
274
  # See: https://github.com/qemu/qemu/blob/master/target/arm/hvf/hvf.c
275
+ #
276
+ # NOTE (non-adoption): the background-snapshot capability (live snapshot
277
+ # without pausing the vCPU) is not used. It is KVM-only (needs userfaultfd
278
+ # write-protect) so excluded on HVF, and we snapshot idle sessions where a
279
+ # brief stop is cheap and full-fidelity capture wants a quiescent VM anyway.
266
280
  resp = await self._execute("stop")
267
281
  if "error" in resp:
268
282
  raise MigrationTransientError(f"stop command failed: {resp['error']}")
@@ -346,7 +360,11 @@ class MigrationClient:
346
360
  status = resp.get("return", {}).get("status", "")
347
361
  if status == "completed":
348
362
  return
349
- if status in ("failed", "cancelled"):
363
+ # "failing" (QEMU 11.0+): an error occurred and cleanup is underway;
364
+ # the status will transition to "failed" shortly. Treat it as terminal
365
+ # here so a mid-stream failure surfaces immediately instead of burning
366
+ # another poll interval waiting for the "failed" transition.
367
+ if status in ("failed", "cancelled", "failing"):
350
368
  error_desc = resp.get("return", {}).get("error-desc", "unknown")
351
369
  raise MigrationTransientError(f"Migration {status}: {error_desc}")
352
370
 
@@ -89,6 +89,10 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
89
89
 
90
90
  # Detect hardware acceleration type (centralized in detect_accel_type)
91
91
  accel_type = await detect_accel_type(force_emulation=settings.force_emulation)
92
+
93
+ # QEMU version gates several flags below (machine props, netdev reconnect,
94
+ # -run-with). Probe once up front; the result is cached in system_probes.
95
+ qemu_version = await probe_qemu_version()
92
96
  logger.info(
93
97
  "Hardware acceleration detection",
94
98
  extra={"vm_id": vm_id, "accel_type": accel_type.value, "is_macos": is_macos},
@@ -102,6 +106,14 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
102
106
  else:
103
107
  # TCG software emulation fallback (~5-8x slower than KVM/HVF)
104
108
  #
109
+ # NOTE (security envelope): QEMU's security policy grants the
110
+ # "virtualization use case" isolation guarantee only to microvm (x86) /
111
+ # virt (aarch64) *with* a hardware accelerator (KVM/HVF). The TCG path is
112
+ # explicitly NOT security-supported ("must not rely on QEMU for
113
+ # isolation"). It is a functional fallback for CI / trusted workloads
114
+ # only; prod isolation always runs on KVM or HVF.
115
+ # See: https://qemu-project.gitlab.io/qemu/system/security.html
116
+ #
105
117
  # thread=single: Disable MTTCG to reduce thread count per VM. Without this,
106
118
  # each VM creates multiple threads for parallel translation, exhausting
107
119
  # system thread limits when running parallel tests (qemu_thread_create:
@@ -149,10 +161,19 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
149
161
  # random.trust_cpu=on in kernel cmdline
150
162
  # mem-merge: KSM page dedup (Linux only; macOS HVF does not support it)
151
163
  mem_merge = "off" if is_macos else "on"
164
+ # virtio-mmio-transports=8: cap the number of virtio-mmio transport slots
165
+ # (QEMU virt board default is 32). We instantiate at most 6 virtio-mmio
166
+ # devices (blk-base + optional blk-snap + serial + balloon + rng +
167
+ # optional net); 8 leaves headroom. Fewer empty slots means the guest's
168
+ # virtio_mmio driver skips ~24 spurious MMIO magic/device-id probes at
169
+ # boot and the DTB is smaller. Keep >= the live device count or QEMU aborts.
170
+ # Machine property added in QEMU 11.0 — gated to avoid aborting older
171
+ # local installs. See docs/system/arm/virt.rst.
172
+ mmio_transports = ",virtio-mmio-transports=8" if qemu_version is not None and qemu_version >= (11, 0, 0) else ""
152
173
  machine_type = (
153
- f"virt,virtualization=off,highmem=off,gic-version=3,its=off,dtb-randomness=off,mem-merge={mem_merge}"
174
+ f"virt,virtualization=off,highmem=off,gic-version=3,its=off,dtb-randomness=off,mem-merge={mem_merge}{mmio_transports}"
154
175
  if is_macos
155
- else f"virt,virtualization=off,highmem=off,gic-version=3,its=off,dtb-randomness=off,mem-merge={mem_merge},dump-guest-core=off"
176
+ else f"virt,virtualization=off,highmem=off,gic-version=3,its=off,dtb-randomness=off,mem-merge={mem_merge}{mmio_transports},dump-guest-core=off"
156
177
  )
157
178
  else:
158
179
  arch_suffix = "x86_64"
@@ -294,6 +315,9 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
294
315
  # also lacks 256-bit vector ops (TCG_TARGET_HAS_v256=0), so AVX2
295
316
  # is unavailable — effective ceiling is SSE4.2. On x86 hosts the
296
317
  # effective ceiling is x86_64-v3 (AVX2/FMA).
318
+ # NOTE (non-adoption): ARM `max,sve=off,sme=off` (individually maskable
319
+ # on `max` since QEMU 11.0) is not set — boot never exercises SVE/SME,
320
+ # so the win is ~zero (only trims per-vCPU vector-state struct size).
297
321
  # See: https://www.qemu.org/docs/master/system/i386/cpu.html
298
322
  # See: https://gitlab.com/qemu-project/qemu/-/issues/844
299
323
  (
@@ -339,6 +363,9 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
339
363
  # (16-64KB per fault instead of 4KB), but transparent_hugepage=never
340
364
  # disables anonymous 2MB hugepages (no khugepaged, no compaction stalls).
341
365
  # File-backed large folios work regardless of this sysfs setting.
366
+ # NOTE (non-adoption): keeping anon THP off is deliberate for density —
367
+ # 2MB hugepages defeat free-page-reporting and KSM (both 4KB-granular)
368
+ # and add compaction stalls; enabling them would hurt host-RAM-per-VM.
342
369
  + " transparent_hugepage=never"
343
370
  # Boot verbosity: debug_boot enables full kernel/init logging for diagnostics
344
371
  # Note: loglevel=7 overrides loglevel=1 set in console_params (kernel uses last occurrence)
@@ -471,9 +498,8 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
471
498
  ]
472
499
  )
473
500
 
474
- # Determine AIO mode and QEMU version based on cached startup probes
501
+ # Determine AIO mode based on cached startup probes (qemu_version probed above)
475
502
  io_uring_available = await probe_io_uring_support()
476
- qemu_version = await probe_qemu_version()
477
503
  aio_mode = "io_uring" if io_uring_available else "threads"
478
504
  if not io_uring_available:
479
505
  logger.debug(
@@ -503,6 +529,13 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
503
529
 
504
530
  # Disk configuration (EROFS base drive, serial=base)
505
531
  # Uses qcow2 overlay backed by the EROFS base image
532
+ #
533
+ # NOTE (non-adoption): RWF_DSYNC FUA (QEMU 10.0 file-posix) is deliberately
534
+ # unused. It only helps cache=writethrough/directsync; cache=unsafe drops all
535
+ # flushes/FUA outright, which strictly dominates for discard-on-exit overlays
536
+ # (a host crash loses in-flight overlay writes, but the overlay is thrown away
537
+ # on VM exit). Switching to a flushing cache mode to gain FUA would be a large
538
+ # regression here.
506
539
  qemu_args.extend(
507
540
  [
508
541
  "-drive",
@@ -534,6 +567,13 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
534
567
  # rootfs drive, regardless of /dev/vdX assignment. ARM virt (MMIO) enumerates
535
568
  # virtio devices in reverse declaration order, so /dev/vda != first -device on ARM.
536
569
  # D2: event_idx=off reduces interrupt coalescing overhead during boot.
570
+ #
571
+ # NOTE (non-adoption): num-queues=1 is deliberate. virtio-blk multiqueue /
572
+ # iothread-vq-mapping (QEMU 9.0) only pays off with many vCPUs and an
573
+ # I/O-bound guest saturating a single iothread. Our VMs have 1-2 vCPUs, a
574
+ # read-only page-cache-hot EROFS base and a tiny cache=unsafe overlay — never
575
+ # iothread-CPU-bound. HVF has no ioeventfd (block already routed to main-loop
576
+ # above) and TCG is thread=single, so extra queues buy nothing here.
537
577
  qs = constants.VIRTIO_QUEUE_SIZE
538
578
  qemu_args.extend(
539
579
  [
@@ -622,6 +662,10 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
622
662
  # See: https://bugs.launchpad.net/qemu/+bug/1639791 (early virtio console lost)
623
663
  # See: https://gist.github.com/mcastelino/aa118275991d4f561ee22dc915b9345f
624
664
  # =============================================================
665
+ # NOTE (non-adoption): guest-agent transport stays on virtio-serial. vhost-vsock
666
+ # was evaluated as an alternative; no QEMU 10.1→11.0 reliability/perf change
667
+ # shifts the tradeoff, and virtio-serial's named ports (cmd/event) map cleanly
668
+ # onto the existing guest-agent protocol.
625
669
  qemu_args.extend(
626
670
  [
627
671
  "-device",
@@ -637,6 +681,13 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
637
681
  )
638
682
 
639
683
  # virtio-balloon for host memory efficiency (deflate/inflate for warm pool).
684
+ #
685
+ # NOTE (non-adoption): virtio-mem is deliberately not used in place of the
686
+ # balloon. It targets long-lived, resizable VMs; unplug is guest-cooperative,
687
+ # slow, and can partially fail, and its block size must be a power-of-two
688
+ # >= 2 MiB — coarse for our small -m VMs. It has no HVF support and yields no
689
+ # net RSS win over free-page-reporting + template-COW page sharing.
690
+ #
640
691
  # Disabled for ALL template VMs (save AND restore):
641
692
  # 1. Device topology must match between save and restore — QEMU migration
642
693
  # fails with "Unknown section" if one side has balloon and the other doesn't.
@@ -697,6 +748,9 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
697
748
  #
698
749
  # =============================================================
699
750
 
751
+ # NOTE (non-adoption): the in-tree passt netdev (QEMU 10.1) is not used. It
752
+ # lacks gvproxy's API-driven outbound-filtering and dynamic port-forward model
753
+ # that Modes 1-3 below depend on, so it is not a drop-in replacement.
700
754
  needs_network = allow_network or bool(expose_ports)
701
755
  if needs_network:
702
756
  # All modes use socket networking to gvproxy (fast ~300ms boot)
@@ -783,8 +837,21 @@ async def build_qemu_cmd( # noqa: PLR0912, PLR0915
783
837
 
784
838
  # Orphan protection: kill QEMU if parent process dies (QEMU 10.2+)
785
839
  # Uses PR_SET_PDEATHSIG on Linux, kqueue on macOS/FreeBSD
840
+ #
841
+ # async-teardown=on (Linux only): fork a teardown clone that unmaps guest RAM
842
+ # so the main QEMU can exit without blocking on munmap. Trims destroy_vm()
843
+ # tail latency under mass pool recycle. macOS QEMU rejects this sub-option
844
+ # (only chroot/exit-with-parent/user exist there), so it is Linux-gated.
845
+ #
846
+ # NOTE (non-adoption): -run-with chroot=/user= are not used. Both require a
847
+ # privileged launcher (QEMU starts root, then drops), which conflicts with our
848
+ # unprivileged model — sudo -u qemu-vm + fd-passing (socket-activated QMP).
849
+ # Filesystem confinement is already provided by `unshare --mount` on Linux.
786
850
  if qemu_version is not None and qemu_version >= (10, 2, 0):
787
- qemu_args.extend(["-run-with", "exit-with-parent=on"])
851
+ run_with_opts = "exit-with-parent=on"
852
+ if not is_macos:
853
+ run_with_opts += ",async-teardown=on"
854
+ qemu_args.extend(["-run-with", run_with_opts])
788
855
 
789
856
  # Run QEMU as unprivileged user if qemu-vm user is available (optional hardening)
790
857
  # Falls back to current user if qemu-vm doesn't exist - VM still provides isolation
@@ -217,6 +217,13 @@ class Scheduler:
217
217
 
218
218
  # Initialize WarmVMPool (optional)
219
219
  if self.config.warm_pool_size > 0:
220
+ # The pool pre-boots one VM per language, and its startup hashes
221
+ # each base image (check_cache -> compute_cache_key -> get_base_image).
222
+ # On a fresh cache ensure_assets() fetched only kernel/initramfs, so
223
+ # download every pooled base image first, else start() raises
224
+ # VmDependencyError. Mirrors the per-run fetch in _prepare_vm.
225
+ if self.config.auto_download_assets:
226
+ await asyncio.gather(*(fetch_base_image(language.value) for language in Language))
220
227
  self._warm_pool = WarmVMPool(
221
228
  self._vm_manager,
222
229
  self.config,
@@ -701,6 +708,12 @@ class Scheduler:
701
708
  if vm is None:
702
709
  is_cold_boot = True
703
710
 
711
+ # Base image must be on disk before the L1 cache key hashes it
712
+ # (check_cache -> compute_cache_key -> get_base_image) and before
713
+ # cold create_vm. Skip when snapshot_path already fetched it.
714
+ if self.config.auto_download_assets and not snapshot_path:
715
+ await fetch_base_image(language)
716
+
704
717
  admission_start = asyncio.get_running_loop().time()
705
718
  async with self._vm_manager.reservation_context(
706
719
  vm_id=f"{constants.SCHEDULER_TENANT_ID}-{task_id}",
@@ -747,11 +760,6 @@ class Scheduler:
747
760
 
748
761
  # Cold boot fallback: L1 miss (or L1 skipped for network/boot-log)
749
762
  if vm is None:
750
- # Auto-download base image if needed (skip when snapshot_path
751
- # is set — _get_or_create_snapshot already fetched it)
752
- if self.config.auto_download_assets and not snapshot_path:
753
- await fetch_base_image(language)
754
-
755
763
  vm = await self._vm_manager.create_vm(
756
764
  language=language,
757
765
  tenant_id=constants.SCHEDULER_TENANT_ID,
@@ -287,11 +287,11 @@ class TestMigrationClientExecuteEdgeCases:
287
287
  class TestMigrationClientSetCapabilities:
288
288
  """Tests for _set_capabilities (mapped-ram + multifd on QEMU >= 9.0)."""
289
289
 
290
- async def test_enables_mapped_ram_for_qemu_9(self) -> None:
291
- """QEMU >= 9.0 should enable mapped-ram + multifd."""
290
+ async def test_enables_mapped_ram_for_qemu_11(self) -> None:
291
+ """QEMU >= 11.0 should enable mapped-ram + multifd."""
292
292
  client, _reader, writer = _make_connected([{"return": {}}])
293
293
 
294
- await client._set_capabilities((9, 2, 0))
294
+ await client._set_capabilities((11, 2, 0))
295
295
 
296
296
  cmds = writer.commands
297
297
  assert len(cmds) == 1
@@ -299,12 +299,25 @@ class TestMigrationClientSetCapabilities:
299
299
  cap_names = {c["capability"] for c in cmds[0]["arguments"]["capabilities"]}
300
300
  assert "multifd" in cap_names
301
301
  assert "mapped-ram" in cap_names
302
+ # Non-template save: no x-ignore-shared (full RAM captured).
303
+ assert "x-ignore-shared" not in cap_names
304
+
305
+ async def test_template_adds_x_ignore_shared(self) -> None:
306
+ """Template mode appends x-ignore-shared to the unified mapped-ram list."""
307
+ client, _reader, writer = _make_connected([{"return": {}}])
308
+
309
+ await client._set_capabilities((11, 2, 0), use_template=True)
310
+
311
+ cmds = writer.commands
312
+ assert len(cmds) == 1
313
+ cap_names = {c["capability"] for c in cmds[0]["arguments"]["capabilities"]}
314
+ assert cap_names == {"multifd", "mapped-ram", "x-ignore-shared"}
302
315
 
303
316
  async def test_no_capabilities_for_old_qemu(self) -> None:
304
- """QEMU < 9.0 should not set capabilities."""
317
+ """QEMU < 11.0 should not set capabilities (falls back to plain streaming)."""
305
318
  client, _reader, writer = _make_connected()
306
319
 
307
- await client._set_capabilities((8, 2, 0))
320
+ await client._set_capabilities((10, 2, 0))
308
321
 
309
322
  assert len(writer.commands) == 0
310
323
 
@@ -325,25 +338,25 @@ class TestMigrationClientSetCapabilities:
325
338
  )
326
339
 
327
340
  # Should not raise
328
- await client._set_capabilities((9, 2, 0))
341
+ await client._set_capabilities((11, 2, 0))
329
342
 
330
343
 
331
344
  class TestMigrationClientSetCapabilitiesEdgeCases:
332
345
  """Edge cases for capability negotiation."""
333
346
 
334
- async def test_boundary_qemu_version_9_0_0(self) -> None:
335
- """Exactly QEMU 9.0.0 should enable capabilities (>= check)."""
347
+ async def test_boundary_qemu_version_11_0_0(self) -> None:
348
+ """Exactly QEMU 11.0.0 should enable capabilities (>= check)."""
336
349
  client, _reader, writer = _make_connected([{"return": {}}])
337
350
 
338
- await client._set_capabilities((9, 0, 0))
351
+ await client._set_capabilities((11, 0, 0))
339
352
 
340
353
  assert any(c["execute"] == "migrate-set-capabilities" for c in writer.commands)
341
354
 
342
- async def test_boundary_qemu_version_8_99_99(self) -> None:
343
- """QEMU 8.99.99 should NOT enable capabilities (< 9.0.0)."""
355
+ async def test_boundary_qemu_version_10_99_99(self) -> None:
356
+ """QEMU 10.99.99 should NOT enable capabilities (< 11.0.0)."""
344
357
  client, _reader, writer = _make_connected()
345
358
 
346
- await client._set_capabilities((8, 99, 99))
359
+ await client._set_capabilities((10, 99, 99))
347
360
 
348
361
  assert len(writer.commands) == 0
349
362
 
@@ -406,6 +419,26 @@ class TestMigrationClientPollMigration:
406
419
  ):
407
420
  await client._poll_migration(timeout=5.0)
408
421
 
422
+ async def test_poll_raises_on_failing(self) -> None:
423
+ """Poll should raise on 'failing' status (QEMU 11.0+ error-cleanup state).
424
+
425
+ 'failing' means an error occurred and cleanup is underway (it transitions
426
+ to 'failed' shortly). We treat it as terminal so the failure surfaces one
427
+ poll interval sooner instead of waiting for the 'failed' transition.
428
+ """
429
+ client, _reader, _writer = _make_connected(
430
+ [
431
+ {"return": {"status": "active"}},
432
+ {"return": {"status": "failing", "error-desc": "write error"}},
433
+ ]
434
+ )
435
+
436
+ with (
437
+ patch.object(constants, "MEMORY_SNAPSHOT_POLL_INTERVAL_SECONDS", 0.001),
438
+ pytest.raises(MigrationTransientError, match="failing"),
439
+ ):
440
+ await client._poll_migration(timeout=5.0)
441
+
409
442
  async def test_poll_raises_on_query_error(self) -> None:
410
443
  """Poll should raise MigrationTransientError on query-migrate error response."""
411
444
  client, _reader, _writer = _make_connected(
@@ -494,13 +527,13 @@ class TestMigrationClientSaveSnapshot:
494
527
  )
495
528
 
496
529
  with patch.object(constants, "MEMORY_SNAPSHOT_POLL_INTERVAL_SECONDS", 0.001):
497
- await client.save_snapshot(Path("/tmp/test.vmstate"), qemu_version=(9, 2, 0))
530
+ await client.save_snapshot(Path("/tmp/test.vmstate"), qemu_version=(11, 2, 0))
498
531
 
499
532
  cmds = [c["execute"] for c in writer.commands]
500
533
  assert cmds == ["migrate-set-capabilities", "stop", "migrate", "query-migrate", "quit"]
501
534
 
502
535
  async def test_save_command_sequence_old_qemu(self) -> None:
503
- """Save with QEMU < 9.0 should: stop → migrate → poll → quit (no capabilities)."""
536
+ """Save with QEMU < 11.0 should: stop → migrate → poll → quit (no capabilities)."""
504
537
  client, _reader, writer = _make_connected(
505
538
  [
506
539
  {"return": {}}, # stop
@@ -638,7 +671,7 @@ class TestMigrationClientRestoreSnapshot:
638
671
  )
639
672
 
640
673
  with patch.object(constants, "MEMORY_SNAPSHOT_POLL_INTERVAL_SECONDS", 0.001):
641
- await client.restore_snapshot(Path("/tmp/test.vmstate"), qemu_version=(9, 2, 0))
674
+ await client.restore_snapshot(Path("/tmp/test.vmstate"), qemu_version=(11, 2, 0))
642
675
 
643
676
  cmds = [c["execute"] for c in writer.commands]
644
677
  assert cmds == ["migrate-set-capabilities", "migrate-incoming", "query-migrate", "cont"]
@@ -655,7 +688,7 @@ class TestMigrationClientRestoreSnapshot:
655
688
  )
656
689
 
657
690
  with patch.object(constants, "MEMORY_SNAPSHOT_POLL_INTERVAL_SECONDS", 0.001):
658
- await client.restore_snapshot(Path("/tmp/test.vmstate"), qemu_version=(9, 2, 0))
691
+ await client.restore_snapshot(Path("/tmp/test.vmstate"), qemu_version=(11, 2, 0))
659
692
 
660
693
  cmds = [c["execute"] for c in writer.commands]
661
694
  assert "quit" not in cmds