pwn 0.5.784 → 0.5.787

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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +6 -6
  4. data/README.md +37 -0
  5. data/bin/pwn +2 -1
  6. data/bin/pwn_web_cache_deception +35 -7
  7. data/build_gem.sh +1 -1
  8. data/documentation/AI-Integration.md +7 -5
  9. data/documentation/Agent-Tool-Registry.md +2 -2
  10. data/documentation/Backup-Retention.md +69 -15
  11. data/documentation/Configuration.md +10 -6
  12. data/documentation/Contributing.md +1 -1
  13. data/documentation/Cron.md +2 -2
  14. data/documentation/Diagrams.md +2 -2
  15. data/documentation/General-PWN-Usage.md +4 -3
  16. data/documentation/Harness-Evaluation.md +1 -1
  17. data/documentation/Home.md +6 -5
  18. data/documentation/How-PWN-Works.md +6 -5
  19. data/documentation/Installation.md +12 -9
  20. data/documentation/Mission-Cancellation.md +134 -0
  21. data/documentation/Operability.md +18 -0
  22. data/documentation/Persistence.md +7 -5
  23. data/documentation/Plugin-Degradation.md +14 -0
  24. data/documentation/Plugins.md +3 -2
  25. data/documentation/Policy-Benchmark.md +32 -33
  26. data/documentation/Runtime-Source-Protection.md +162 -0
  27. data/documentation/Transparent-Browser.md +11 -0
  28. data/documentation/Troubleshooting.md +3 -2
  29. data/documentation/What-is-PWN.md +2 -2
  30. data/documentation/YubiKey-Vaults.md +185 -0
  31. data/documentation/YubiKey.md +218 -0
  32. data/documentation/diagrams/cron-scheduling.svg +130 -130
  33. data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
  34. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  35. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  36. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
  37. data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
  38. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  39. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  40. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
  41. data/documentation/diagrams/reinforcement-learning.svg +89 -89
  42. data/documentation/pwn-ai-Agent.md +10 -4
  43. data/etc/default_cron/session_cleanup.rb +43 -27
  44. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  45. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  46. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  47. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  48. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  49. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  50. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  51. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  52. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  61. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  62. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  63. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  64. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  65. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  66. data/lib/pwn/ai/agent/learning.rb +14 -18
  67. data/lib/pwn/ai/agent/loop.rb +17 -1
  68. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  69. data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
  70. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  71. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  72. data/lib/pwn/ai/agent/reward.rb +5 -3
  73. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  74. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  75. data/lib/pwn/ai/agent/swarm.rb +4 -0
  76. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  77. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  79. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  80. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  81. data/lib/pwn/ai/agent.rb +1 -0
  82. data/lib/pwn/ai/anthropic.rb +8 -33
  83. data/lib/pwn/ai/cli.rb +34 -5
  84. data/lib/pwn/ai/gemini.rb +1 -0
  85. data/lib/pwn/ai/grok.rb +39 -28
  86. data/lib/pwn/ai/ollama.rb +4 -2
  87. data/lib/pwn/ai/open_ai.rb +11 -32
  88. data/lib/pwn/ai/open_web_ui.rb +62 -4
  89. data/lib/pwn/ai/router.rb +8 -1
  90. data/lib/pwn/ai/server_tools.rb +231 -0
  91. data/lib/pwn/ai.rb +1 -0
  92. data/lib/pwn/backups.rb +349 -0
  93. data/lib/pwn/config.rb +70 -39
  94. data/lib/pwn/migrate.rb +85 -280
  95. data/lib/pwn/module_skills.rb +7 -0
  96. data/lib/pwn/plugins/ai_sandbox.rb +538 -1
  97. data/lib/pwn/plugins/authentication_helper.rb +74 -4
  98. data/lib/pwn/plugins/basic_auth.rb +2 -1
  99. data/lib/pwn/plugins/google_workspace.rb +4 -21
  100. data/lib/pwn/plugins/jobs.rb +1 -1
  101. data/lib/pwn/plugins/log.rb +3 -1
  102. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  103. data/lib/pwn/plugins/recon.rb +4 -1
  104. data/lib/pwn/plugins/repl/ai.rb +293 -26
  105. data/lib/pwn/plugins/repl/ai_console.rb +268 -88
  106. data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
  107. data/lib/pwn/plugins/repl/mesh.rb +14 -19
  108. data/lib/pwn/plugins/repl/vault.rb +4 -15
  109. data/lib/pwn/plugins/repl.rb +9 -216
  110. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  111. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  112. data/lib/pwn/plugins/vault.rb +412 -135
  113. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  114. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  115. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  116. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  117. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  118. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  119. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  120. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  121. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  122. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  123. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  124. data/lib/pwn/plugins/yubikey.rb +844 -0
  125. data/lib/pwn/plugins.rb +1 -0
  126. data/lib/pwn/redaction.rb +69 -12
  127. data/lib/pwn/session_trace.rb +4 -0
  128. data/lib/pwn/setup.rb +114 -24
  129. data/lib/pwn/version.rb +1 -1
  130. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  131. data/lib/pwn/www/bug_crowd.rb +16 -5
  132. data/lib/pwn/www/coinbase_pro.rb +16 -5
  133. data/lib/pwn/www/github.rb +30 -14
  134. data/lib/pwn/www/paypal.rb +15 -4
  135. data/lib/pwn/www/synack.rb +16 -5
  136. data/lib/pwn/www/twitter.rb +17 -6
  137. data/lib/pwn.rb +1 -0
  138. data/spec/documentation/installation_md_spec.rb +1 -1
  139. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  140. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  141. data/spec/integration/controller_spill_spec.rb +48 -0
  142. data/spec/integration/harness_process_spec.rb +97 -0
  143. data/spec/integration/mission_cancel_spec.rb +79 -0
  144. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  145. data/spec/integration/readme_security_spec.rb +47 -0
  146. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  147. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  148. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  149. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  150. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  151. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  152. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  153. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  154. data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
  155. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  156. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  157. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  158. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  159. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  160. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  161. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  162. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  163. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  164. data/spec/lib/pwn/ai/cli_spec.rb +8 -16
  165. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  166. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  167. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  168. data/spec/lib/pwn/ai/router_spec.rb +1 -1
  169. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  170. data/spec/lib/pwn/backups_spec.rb +129 -0
  171. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  172. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  173. data/spec/lib/pwn/migrate_spec.rb +29 -81
  174. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  175. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  176. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  177. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  178. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  179. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  180. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  181. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  182. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  183. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  184. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  185. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
  186. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  187. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
  188. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  189. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  190. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  191. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  192. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  193. data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
  194. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  195. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  196. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  197. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  198. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  199. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  200. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  201. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  202. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  203. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  204. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  205. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  206. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  207. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  208. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  209. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  210. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  211. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  212. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  213. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  214. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  215. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  216. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  217. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  218. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  219. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  220. data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
  221. data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
  222. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
  223. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  224. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  225. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  226. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  227. data/spec/spec_helper.rb +15 -0
  228. data/spec/support/harness/cli_faults.rb +13 -9
  229. data/spec/support/harness/controller_spill.rb +26 -0
  230. data/spec/support/harness/controller_state.rb +54 -0
  231. data/spec/support/harness/evaluate.rb +12 -1
  232. data/spec/support/harness/experiments.rb +1 -1
  233. data/spec/support/harness/fake_provider.rb +22 -6
  234. data/spec/support/harness/fault_preload.rb +3 -1
  235. data/spec/support/harness/mission_console.rb +54 -0
  236. data/spec/support/harness/mission_curses_pty.rb +41 -0
  237. data/spec/support/harness/mission_dispatch.rb +30 -0
  238. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  239. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  240. data/spec/support/harness/mission_legacy.rb +25 -0
  241. data/spec/support/harness/mission_normal_exit.rb +34 -0
  242. data/spec/support/harness/mission_swarm.rb +23 -0
  243. data/spec/support/harness/mission_unsupported.rb +29 -0
  244. data/spec/support/harness/package_acceptance.rb +33 -5
  245. data/spec/support/harness/session_acceptance.rb +4 -4
  246. data/spec/support/harness/swarm_acceptance.rb +1 -1
  247. data/spec/support/harness/swarm_control.rb +44 -0
  248. data/spec/support/mission_namespace.rb +14 -0
  249. data/spec/support/mission_pipe_queue.rb +33 -0
  250. data/third_party/pwn_rdoc.jsonl +605 -19
  251. metadata +129 -16
  252. data/scripts/benchmark_policy.rb +0 -376
  253. data/scripts/build_gate.rb +0 -58
  254. data/spec/integration/build_gate_spec.rb +0 -73
@@ -990,6 +990,15 @@ module PWN
990
990
  text.sub(%r{\s*\[/UNTRUSTED TOOL OUTPUT\]\s*\z}m, '')
991
991
  end
992
992
 
993
+ private_class_method def self.terminal_effect_blocker(opts = {})
994
+ runtime = opts[:runtime]
995
+ diagnostic = runtime&.blocker
996
+ return nil unless diagnostic
997
+
998
+ runtime.finish!(status: 'blocked', unmet: runtime.unmet + [diagnostic])
999
+ "[pwn-ai] blocked: #{diagnostic}"
1000
+ end
1001
+
993
1002
  private_class_method def self.declared_deliverables(opts = {})
994
1003
  Array(declared_contract(request: opts[:request])[:paths])
995
1004
  end
@@ -1004,7 +1013,7 @@ module PWN
1004
1013
  end
1005
1014
  # Paths are user-owned. Inference cannot invent a destination or
1006
1015
  # convert the source document into a write obligation.
1007
- contract = contract.merge(paths: literals.any? ? literals : contract[:paths])
1016
+ contract = contract.merge(paths: literals)
1008
1017
  Thread.current[:pwn_loop_deliverables] = contract if Thread.current[:pwn_loop_active]
1009
1018
  contract
1010
1019
  end
@@ -2091,6 +2100,10 @@ module PWN
2091
2100
  model = opts[:model] || Thread.current[:pwn_swarm_model] || config[:model]
2092
2101
  cwt_opts[:model] = model unless model.to_s.strip.empty?
2093
2102
  cwt_opts[:temp] = opts[:temp] unless opts[:temp].nil?
2103
+ if PWN::AI::ServerTools.mixable?(engine: engine, model: cwt_opts[:model])
2104
+ hosted = Array(config[:server_tools]).map(&:to_s).reject(&:empty?)
2105
+ cwt_opts[:server_tools] = hosted if hosted.any?
2106
+ end
2094
2107
  if tools && !tools.empty?
2095
2108
  env_tc = begin
2096
2109
  config[:tool_choice]
@@ -3637,6 +3650,9 @@ module PWN
3637
3650
  return "[pwn-ai] #{status}: explicit request verification did not pass."
3638
3651
  end
3639
3652
  end
3653
+ effect_blocker = terminal_effect_blocker(runtime: runtime)
3654
+ return effect_blocker if effect_blocker
3655
+
3640
3656
  # Completion is an evidence decision, never a prose classifier.
3641
3657
  unless may_finalize?(
3642
3658
  request: request,
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'fiddle'
5
+ require 'json'
6
+ require 'date'
7
+ require 'rbconfig'
8
+ require 'timeout'
9
+
10
+ # Load root autoloads before opening nested namespaces on direct require.
11
+ require 'pwn'
12
+
13
+ module PWN
14
+ module AI
15
+ module Agent
16
+ # Exec-based user namespace bootstrap; the terminal never enters a namespace.
17
+ # Only typed descriptors cross the private socket. Ruby closures are permitted
18
+ # below an existing owner, where fork does not need a new user namespace.
19
+ class MissionNamespace # rubocop:disable Metrics/ClassLength -- one exec handshake and process ownership protocol
20
+ class Unavailable < StandardError; end
21
+ CONTEXT_KEYS = %i[pwn_swarm_engine pwn_swarm_model pwn_swarm_id pwn_swarm_depth pwn_solve_workspace pwn_solve_tools
22
+ pwn_session_id pwn_artifact_contract pwn_active_steering_request pwn_turn_finalizer_depth
23
+ pwn_reflect_depth pwn_request_intent pwn_loop_budget_s pwn_loop_mutations pwn_loop_tokens].freeze
24
+ attr_reader :status
25
+
26
+ class << self
27
+ attr_accessor :owned, :bootstrap_requires, :force_exec
28
+
29
+ def prepared?
30
+ rows = File.readlines('/proc/self/uid_map').map { |line| line.split.map(&:to_i) }
31
+ count = rows.length == 1 ? rows.first.last : nil
32
+ # The initial host map is the full 32-bit range. Any smaller single
33
+ # line is a user namespace, including a root test bootstrap that must
34
+ # still include a non-root account.
35
+ count.is_a?(Integer) && count.positive? && count < 4_294_967_295
36
+ rescue SystemCallError
37
+ false
38
+ end
39
+
40
+ def io(number)
41
+ stream = Thread.current[:pwn_mission_fds]&.fetch(number, nil) || IO.for_fd(number)
42
+ stream.close_on_exec = true if number > 2
43
+ stream
44
+ end
45
+
46
+ def native(name, arguments)
47
+ Fiddle::Function.new(Fiddle::Handle::DEFAULT[name], arguments, Fiddle::TYPE_INT)
48
+ end
49
+
50
+ def checked(result, operation)
51
+ raise Unavailable, "#{operation}: #{SystemCallError.new(operation, Fiddle.last_error).message}; install util-linux unshare and enable Linux user/PID namespaces and pidfds" if result.negative?
52
+
53
+ result
54
+ end
55
+
56
+ # Tagged JSON preserves Symbol hash keys and values without deserializing
57
+ # executable Ruby objects or placing credentials in argv or temporary files.
58
+ def encode(value, depth = 0)
59
+ raise ArgumentError, 'mission snapshot is too deep or cyclic' if depth > 30
60
+
61
+ case value
62
+ when Hash then ['hash', value.map { |key, item| [encode(key, depth + 1), encode(item, depth + 1)] }]
63
+ when Array then ['array', value.map { |item| encode(item, depth + 1) }]
64
+ when Time then ['time', value.to_i, value.nsec, value.utc_offset, value.utc?]
65
+ when DateTime then ['datetime', value.iso8601(9)]
66
+ when Date then ['date', value.iso8601]
67
+ when Symbol then ['symbol', value.to_s]
68
+ when String then ['string', [value.b].pack('m0'), value.encoding.name]
69
+ when NilClass, TrueClass, FalseClass, Numeric then ['scalar', value]
70
+ else raise ArgumentError, "mission snapshot cannot transfer #{value.class}; use a serializable descriptor"
71
+ end
72
+ end
73
+
74
+ def decode(value)
75
+ type, body, encoding = value
76
+ case type
77
+ when 'hash' then body.to_h { |key, item| [decode(key), decode(item)] }
78
+ when 'array' then body.map { |item| decode(item) }
79
+ when 'time' then value[4] ? Time.at(body, encoding, :nanosecond).utc : Time.at(body, encoding, :nanosecond).getlocal(value[3])
80
+ when 'datetime' then DateTime.iso8601(body)
81
+ when 'date' then Date.iso8601(body)
82
+ when 'symbol' then body.to_sym
83
+ when 'string' then body.unpack1('m0').force_encoding(encoding)
84
+ when 'scalar' then body
85
+ else raise ArgumentError, 'invalid mission snapshot'
86
+ end
87
+ end
88
+
89
+ def supervise
90
+ channel = UNIXSocket.for_fd(3)
91
+ snapshot = decode(JSON.parse(channel.gets))
92
+ checked(native('unshare', [Fiddle::TYPE_INT]).call(0x20000000), 'unshare PID namespace')
93
+ reader, writer = IO.pipe
94
+ armed, arm = IO.pipe
95
+ init = fork do
96
+ armed.close
97
+ checked(native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
98
+ arm.write('1')
99
+ arm.close
100
+ channel.close
101
+ writer.close
102
+ exit! unless reader.read(1) == '1'
103
+
104
+ reader.close
105
+ self.owned = true
106
+ self.bootstrap_requires = snapshot.fetch(:requires, [])
107
+ PWN::Env.replace(decode(encode(snapshot.fetch(:env))))
108
+ bootstrap_requires.each { |path| require path }
109
+ PWN::Env.replace(snapshot.fetch(:env))
110
+ PWN.const_set(:Skills, snapshot[:skills]) if snapshot[:skills] && !PWN.const_defined?(:Skills)
111
+ snapshot.fetch(:locals).each { |key, value| Thread.current[key] = value }
112
+ begin
113
+ Thread.handle_interrupt(Exception => :immediate) { execute(snapshot.fetch(:descriptor)) }
114
+ completed = true
115
+ ensure
116
+ exit!(completed ? 0 : 1)
117
+ end
118
+ end
119
+ arm.close
120
+ raise Unavailable, 'namespace init exited before arming parent-death protection' unless armed.read(1) == '1'
121
+
122
+ armed.close
123
+ reader.close
124
+ fd = IO.new(checked(native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(init, 0), 'pidfd_open'))
125
+ channel.send_io(fd)
126
+ exit! unless channel.read(1) == '1'
127
+
128
+ writer.write('1')
129
+ writer.close
130
+ readable = IO.select([channel, fd]).first
131
+ native('pidfd_send_signal', [Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT]).call(fd.fileno, Signal.list.fetch('KILL'), 0, 0) if readable.include?(channel)
132
+ _pid, status = Process.wait2(init)
133
+ init = nil
134
+ fd.close
135
+ status.exitstatus || (128 + status.termsig)
136
+ rescue Exception => e # rubocop:disable Lint/RescueException -- startup must fail closed
137
+ warn "Mission namespace unavailable: #{e.class}: #{e.message}"
138
+ ensure
139
+ if init
140
+ begin
141
+ Process.kill('KILL', init)
142
+ rescue Errno::ESRCH
143
+ nil
144
+ end
145
+ begin
146
+ Process.wait(init)
147
+ rescue Errno::ECHILD
148
+ nil
149
+ end
150
+ end
151
+ channel&.close unless channel&.closed?
152
+ end
153
+
154
+ def execute(descriptor)
155
+ case descriptor.fetch(:operation).to_sym
156
+ when :command
157
+ _pid, status = Process.wait2(Process.spawn(*descriptor.fetch(:argv)))
158
+ exit!(status.exitstatus || (128 + status.termsig))
159
+ when :loop
160
+ require 'pwn/ai/cli'
161
+ PWN::AI::CLI.send(:mission_entry, descriptor)
162
+ when :protected
163
+ PWN::Plugins::AISandbox.send(:mission_entry, descriptor)
164
+ when :console, :slash
165
+ PWN::Plugins::REPL::AIConsole::Console.send(:mission_entry, descriptor: descriptor)
166
+ when :legacy, :steerable
167
+ PWN::Plugins::REPL.pwn_ai_mission_entry(descriptor)
168
+ when :swarm
169
+ PWN::Plugins::REPL::AISwarm::Controller.send(:mission_entry, descriptor: descriptor)
170
+ else
171
+ raise ArgumentError, 'unknown mission operation'
172
+ end
173
+ end
174
+ end
175
+
176
+ def initialize(opts = {}, &operation)
177
+ @mutex = Mutex.new
178
+ raise Unavailable, 'mission containment requires Linux' unless RUBY_PLATFORM.include?('linux')
179
+
180
+ if self.class.prepared? && !self.class.force_exec && !(opts[:inherit_namespace] && self.class.owned)
181
+ start_prepared(opts, &operation)
182
+ return
183
+ end
184
+
185
+ if operation
186
+ raise Unavailable, 'host closures cannot cross an exec boundary; use a mission descriptor or explicitly bootstrap under unshare --user --map-current-user --keep-caps' unless self.class.owned
187
+
188
+ @pid = fork do
189
+ Thread.handle_interrupt(Exception => :immediate) { operation.call }
190
+ ensure
191
+ exit!
192
+ end
193
+ @pidfd = IO.new(self.class.checked(self.class.native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(@pid, 0), 'pidfd_open'))
194
+ return
195
+ end
196
+ snapshot = { requires: Array(self.class.bootstrap_requires), descriptor: opts.fetch(:descriptor), env: PWN::Env, locals: CONTEXT_KEYS.to_h { |key| [key, Thread.current[key]] } }
197
+ if PWN.const_defined?(:Skills)
198
+ snapshot[:skills] = PWN::Skills
199
+ ruby_skills = PWN::Skills.values.select { |skill| skill[:loaded] && skill[:type] == :ruby }
200
+ snapshot[:requires] += ruby_skills.flat_map do |skill|
201
+ skill[:format] == :legacy ? [skill.fetch(:path)] : Dir.glob(File.join(skill.fetch(:dir), 'scripts', '*.rb'))
202
+ end
203
+ snapshot[:requires].uniq!
204
+ end
205
+ payload = JSON.generate(self.class.encode(snapshot))
206
+ @channel, child = UNIXSocket.pair
207
+ mapping = opts.fetch(:fds, {}).merge(3 => child)
208
+ @pid = Process.spawn('unshare', '--user', '--map-current-user', '--keep-caps', RbConfig.ruby, '-I', $LOAD_PATH.join(File::PATH_SEPARATOR), '-rpwn', '-e', 'exit!(PWN::AI::Agent::MissionNamespace.supervise || 1)', mapping.merge(close_others: true))
209
+ child.close
210
+ Timeout.timeout(15) do
211
+ @channel.puts(payload)
212
+ @pidfd = @channel.recv_io
213
+ @channel.write('1')
214
+ end
215
+ rescue Exception => e # rubocop:disable Lint/RescueException -- failed startup cannot leak a child
216
+ close
217
+ raise Unavailable, "mission not started: #{e.message}; install util-linux unshare and enable Linux user/PID namespaces and pidfds"
218
+ ensure
219
+ child&.close unless child&.closed?
220
+ end
221
+
222
+ def wait
223
+ @pidfd.wait_readable if @pidfd && !@pidfd.closed?
224
+ @mutex.synchronize { reap }
225
+ rescue IOError, Errno::EBADF
226
+ @mutex.synchronize { reap }
227
+ end
228
+
229
+ def close
230
+ return :reaped unless @mutex
231
+
232
+ @mutex.synchronize do
233
+ if @pidfd && !@pidfd.closed?
234
+ result = self.class.native('pidfd_send_signal', [Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT]).call(@pidfd.fileno, Signal.list.fetch('KILL'), 0, 0)
235
+ self.class.checked(result, 'pidfd_send_signal') unless result.negative? && Fiddle.last_error == Errno::ESRCH::Errno
236
+ elsif @pid
237
+ begin
238
+ Process.kill('KILL', @pid)
239
+ rescue Errno::ESRCH
240
+ nil
241
+ end
242
+ end
243
+ reap
244
+ end
245
+ end
246
+
247
+ public_class_method def self.authors
248
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
249
+ end
250
+
251
+ public_class_method def self.help
252
+ puts <<~HELP
253
+ # Identify module authors.
254
+ #{self}.authors
255
+
256
+ # Launch a private Linux mission and retain its cancellation owner.
257
+ #{self}.new(
258
+ descriptor: 'required - serializable operation and request parameters',
259
+ fds: 'optional - explicitly inherited descriptor map'
260
+ )
261
+ HELP
262
+ end
263
+
264
+ private
265
+
266
+ def start_prepared(opts, &operation)
267
+ ready, announce = IO.pipe
268
+ release, gate = IO.pipe
269
+ owner = Process.pid
270
+ @pid = fork do
271
+ ready.close
272
+ gate.close
273
+ begin
274
+ self.class.checked(self.class.native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
275
+ exit! unless Process.ppid == owner
276
+ self.class.checked(self.class.native('unshare', [Fiddle::TYPE_INT]).call(0x20000000), 'unshare PID namespace')
277
+ armed, arm = IO.pipe
278
+ init = fork do
279
+ armed.close
280
+ announce.close
281
+ self.class.checked(self.class.native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
282
+ arm.write('1')
283
+ arm.close
284
+ exit! unless release.read(1) == '1'
285
+ release.close
286
+ self.class.owned = true
287
+ Thread.current[:pwn_mission_fds] = opts.fetch(:fds, {})
288
+ begin
289
+ Thread.handle_interrupt(Exception => :immediate) { operation ? operation.call : self.class.execute(opts.fetch(:descriptor)) }
290
+ completed = true
291
+ ensure
292
+ exit!(completed ? 0 : 1)
293
+ end
294
+ end
295
+ arm.close
296
+ raise Unavailable, 'namespace init exited before arming parent-death protection' unless armed.read(1) == '1'
297
+
298
+ armed.close
299
+ announce.puts(JSON.generate(pid: init))
300
+ announce.close
301
+ release.close
302
+ _pid, status = Process.wait2(init)
303
+ exit!(status.exitstatus || (128 + status.termsig))
304
+ rescue Exception => e # rubocop:disable Lint/RescueException -- report child startup errors
305
+ announce.puts(JSON.generate(error: e.message)) unless announce.closed?
306
+ ensure
307
+ exit!
308
+ end
309
+ end
310
+ announce.close
311
+ release.close
312
+ message = JSON.parse(ready.gets || '{}')
313
+ raise Unavailable, message.fetch('error', 'namespace supervisor exited') unless message['pid']
314
+
315
+ @pidfd = IO.new(self.class.checked(self.class.native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(message.fetch('pid'), 0), 'pidfd_open'))
316
+ gate.write('1')
317
+ ensure
318
+ [ready, announce, release, gate].compact.each { |io| io.close unless io.closed? }
319
+ end
320
+
321
+ def reap
322
+ _pid, @status = Process.wait2(@pid) if @pid
323
+ @pid = nil
324
+ @channel&.close unless @channel&.closed?
325
+ @pidfd&.close unless @pidfd&.closed?
326
+ :reaped
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end