pwn 0.5.742 → 0.5.743

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aae26246e4b23381c5f727d27100f6a10d37d955f2c9a875ad63274d3397219b
4
- data.tar.gz: 1a0abc7df30321bd4618a8db3d356fa72e9b6cc13f6055b5f71bf4fb1b4ec90a
3
+ metadata.gz: 8089c1c616fe3d19f70170b55204b0dd762a14faa40432e6acf421bd6d55197d
4
+ data.tar.gz: 6633cdc11ab8b811913d6b6e30608a79fa85c06c5bde599d4155c9c63d5a3884
5
5
  SHA512:
6
- metadata.gz: 5b680d2d2d3eb756642d68a6309a36a5d551100131b58323954bd402c73f554515304ecfb005a42e96ab15a385f16d717495cf527c2bf17c87434567401d0dd0
7
- data.tar.gz: 7b51570ad19e664e45ca69925619fdbdda6272f985d8627f4c247815b469de19cb11bdfa942433480bbc38792127a8b39e45e56c08c8c73ffe517318ad004396
6
+ metadata.gz: 83c1b09ae27724b7e5df7972f018f11b7bee7f77e38d295c45395bf4c828f6c57e9736306687ae243e1fc05dc4dd63e4b33407d4101b916c2e32a8b241f39958
7
+ data.tar.gz: 19ad087ec133a1194b2fd60d7348edff887b94940647b5a0e34792fa5084742dcb7a04f213ee17b404e4fffda5fc1b8db4821fd1bc6c047c0867c96a2752d8af
@@ -49,10 +49,12 @@ module PWN
49
49
  'PWN::Plugins::Jobs.supervise(root: ARGV[0], id: ARGV[1])', root, id,
50
50
  in: File::NULL, out: File::NULL, err: File::NULL, close_others: true)
51
51
  Process.detach(worker)
52
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
52
+ row[:worker_pid] = worker
53
+ persist(root: root, row: row)
54
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 8
53
55
  loop do
54
56
  row = load_job(id: id)
55
- break if row[:worker_pid] || row[:status] != 'RUNNING' || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
57
+ break if row[:worker_identity] || row[:status] != 'RUNNING' || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
56
58
 
57
59
  sleep 0.02
58
60
  end
@@ -372,7 +374,11 @@ module PWN
372
374
  next unless row[:status] == 'RUNNING'
373
375
 
374
376
  begin
375
- Process.setsid
377
+ begin
378
+ Process.setsid
379
+ rescue Errno::EPERM
380
+ nil
381
+ end
376
382
  row.merge!(worker_pid: Process.pid, worker_identity: SecureRandom.hex(16), started_at: Time.now.utc.iso8601(6))
377
383
  pid = spawn(row[:env].transform_keys(&:to_s), '/bin/sh', '-c', row[:command], chdir: row[:cwd],
378
384
  in: File::NULL, %i[out err] => [row[:log], 'ab'], pgroup: true, close_others: true)
@@ -181,7 +181,7 @@ module PWN
181
181
  current = nil
182
182
  dump.each_line do |line|
183
183
  current = Regexp.last_match(1) if line =~ /<([^>]+)>:/
184
- next unless current && (line =~ /call\w*\s+.*<([^>@]+)/ || line =~ /call\w*\s+([A-Za-z_]\w*)(?:@plt)?/)
184
+ next unless current && (line =~ /\b(?:call\w*|blx?r?|jalr?)\b.*<([^>@]+)/ || line =~ /\b(?:call\w*|blx?r?|jalr?)\s+([A-Za-z_]\w*)(?:@plt)?/)
185
185
 
186
186
  db.execute('INSERT INTO xrefs(src, dst) VALUES(?,?)', [current, Regexp.last_match(1)])
187
187
  end
@@ -614,31 +614,49 @@ module PWN
614
614
  end
615
615
 
616
616
  def mesh_tx_row_ready?(opts = {})
617
- return false unless opts.is_a?(Hash)
617
+ mesh_tx_outcome(opts) == :ok
618
+ end
619
+
620
+ def mesh_tx_outcome(opts = {})
621
+ return :idle unless opts.is_a?(Hash)
618
622
 
619
623
  row = opts[:row]
620
- return false unless row.is_a?(Hash)
624
+ return :idle unless row.is_a?(Hash)
621
625
 
622
626
  packet = row[:packet] || row['packet']
623
- return false unless packet.is_a?(Hash)
627
+ return :idle unless packet.is_a?(Hash)
624
628
 
625
629
  decoded = packet[:decoded] || packet['decoded']
626
- return false unless decoded.is_a?(Hash)
630
+ return :idle unless decoded.is_a?(Hash)
627
631
 
628
632
  port = decoded[:portnum] || decoded['portnum']
629
- %w[5 ROUTING_APP].include?(port.to_s) || port == :ROUTING_APP
633
+ return :idle unless %w[5 ROUTING_APP].include?(port.to_s) || port == :ROUTING_APP
634
+
635
+ want = opts[:request_id]
636
+ got = decoded[:request_id] || decoded['request_id']
637
+ return :idle if want && got && want.to_i != got.to_i
638
+
639
+ routing = decoded[:payload] || decoded['payload']
640
+ routing = Meshtastic::Routing.decode(routing).to_h if routing.is_a?(String)
641
+ reason = routing[:error_reason] if routing.is_a?(Hash)
642
+ reason = Meshtastic::Routing::Error.lookup(reason) || reason if reason.is_a?(Integer)
643
+ name = reason.to_s
644
+ return :ok if reason.nil? || %w[0 NONE].include?(name)
645
+ return :rate_limited if name == 'RATE_LIMIT_EXCEEDED'
646
+
647
+ :error
630
648
  end
631
649
 
632
650
  def mesh_wait_tx_slot(opts = {})
633
- return unless opts.is_a?(Hash)
651
+ return :ok unless opts.is_a?(Hash)
634
652
 
635
653
  obj = opts[:obj]
636
- return unless obj.is_a?(Hash)
654
+ return :ok unless obj.is_a?(Hash)
637
655
 
638
656
  timeout = opts[:timeout]
639
657
  timeout = 8 if timeout.nil?
640
658
  timeout = Float(timeout)
641
- return if timeout <= 0
659
+ return :ok if timeout <= 0
642
660
 
643
661
  since = opts[:since].to_i
644
662
  deadline = Time.now + timeout
@@ -648,11 +666,11 @@ module PWN
648
666
  else
649
667
  Array(obj[:proto_data])
650
668
  end
651
- ready = rows.drop(since).any? do |row|
652
- mesh_tx_row_ready?(row: row)
669
+ rows.drop(since).each do |row|
670
+ outcome = mesh_tx_outcome(row: row, request_id: opts[:request_id])
671
+ return outcome unless outcome == :idle
653
672
  end
654
- return if ready
655
- return if Time.now >= deadline
673
+ return :timeout if Time.now >= deadline
656
674
 
657
675
  sleep 0.05
658
676
  end
@@ -682,6 +700,13 @@ module PWN
682
700
  end
683
701
  bodies << piece
684
702
  offset += piece.length
703
+ next unless offset < text.length && piece.match?(/[^[:space:]]\z/)
704
+
705
+ cut = piece.rindex(/[[:space:]]/)
706
+ next unless cut&.positive?
707
+
708
+ bodies[-1] = piece[0, cut + 1]
709
+ offset = offset - piece.length + bodies[-1].length
685
710
  end
686
711
  if bodies.size <= n
687
712
  total = bodies.size
@@ -772,69 +797,77 @@ module PWN
772
797
  dm_key = mesh_dm_key(obj: obj, kind: kind, to: dest, radio: radio, timeout: opts.fetch(:key_timeout, 15))
773
798
  end
774
799
  chunks = mesh_text_chunks(text: opts[:text])
775
- chunks.each_with_index do |piece, idx|
776
- packet_id = Random.rand(2...0xffffffff)
777
- since = obj.is_a?(Hash) ? Array(obj[:proto_data]).size : 0
778
- if dm_key
779
- bytes = Meshtastic::MeshInterface.new.send_text(from: obj[:my_node_num] || 0, to: dest, channel: radio || 0, text: piece, want_ack: true, psks: nil, last_packet_id: packet_id - 1)
780
- packet = Meshtastic::ToRadio.decode(bytes)
781
- packet.packet.pki_encrypted = true
782
- packet.packet.public_key = dm_key
783
- transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
784
- transport.send_to_radio({ "#{kind}_obj": obj, to_radio: packet.to_proto })
785
- else
786
- case kind
787
- when :serial
788
- tx = { serial_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
789
- tx[:channel] = radio unless radio.nil?
790
- Meshtastic::Serial.send_text(tx)
791
- when :bluetooth
792
- tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
793
- tx[:channel] = radio unless radio.nil?
794
- Meshtastic::Bluetooth.send_text(tx)
795
- when :tcp
796
- tx = { tcp_obj: obj, to: dest, text: piece, last_packet_id: packet_id - 1 }
797
- tx[:channel] = radio unless radio.nil?
798
- Meshtastic::TCP.send_text(tx)
800
+ chunks.each_with_index do |piece, _idx|
801
+ echoed = false
802
+ attempts = 0
803
+ loop do
804
+ attempts += 1
805
+ packet_id = Random.rand(2...0xffffffff)
806
+ since = obj.is_a?(Hash) ? Array(obj[:proto_data]).size : 0
807
+ if dm_key
808
+ bytes = Meshtastic::MeshInterface.new.send_text(from: obj[:my_node_num] || 0, to: dest, channel: radio || 0, text: piece, want_ack: true, psks: nil, last_packet_id: packet_id - 1)
809
+ packet = Meshtastic::ToRadio.decode(bytes)
810
+ packet.packet.pki_encrypted = true
811
+ packet.packet.public_key = dm_key
812
+ transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
813
+ transport.send_to_radio({ "#{kind}_obj": obj, to_radio: packet.to_proto })
799
814
  else
800
- send_psks = psks
801
- send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
802
- Meshtastic::MQTT.send_text(
803
- mqtt_obj: obj,
804
- last_packet_id: packet_id - 1,
805
- from: from,
806
- to: dest,
807
- region: mesh_mqtt_region(region: opts[:region], env: env),
808
- topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
809
- channel: channel,
810
- text: piece,
811
- psks: send_psks
815
+ case kind
816
+ when :serial
817
+ tx = { serial_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
818
+ tx[:channel] = radio unless radio.nil?
819
+ Meshtastic::Serial.send_text(tx)
820
+ when :bluetooth
821
+ tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
822
+ tx[:channel] = radio unless radio.nil?
823
+ Meshtastic::Bluetooth.send_text(tx)
824
+ when :tcp
825
+ tx = { tcp_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
826
+ tx[:channel] = radio unless radio.nil?
827
+ Meshtastic::TCP.send_text(tx)
828
+ else
829
+ send_psks = psks
830
+ send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
831
+ Meshtastic::MQTT.send_text(
832
+ mqtt_obj: obj,
833
+ last_packet_id: packet_id - 1,
834
+ from: from,
835
+ to: dest,
836
+ region: mesh_mqtt_region(region: opts[:region], env: env),
837
+ topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
838
+ channel: channel,
839
+ text: piece,
840
+ psks: send_psks
841
+ )
842
+ end
843
+ end
844
+ unless echoed
845
+ from_id = from.to_s
846
+ from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
847
+ from_id = mesh_format_node_id(id: from_id)
848
+ PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
849
+ PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
850
+ mesh_handle_rx(
851
+ local: true,
852
+ channel_name: channel_name,
853
+ msg: {
854
+ packet: {
855
+ id: packet_id,
856
+ channel: radio,
857
+ pki_encrypted: !dm_key.nil?,
858
+ node_id_from: from_id,
859
+ node_id_to: dest,
860
+ decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
861
+ }
862
+ }
812
863
  )
864
+ echoed = true
813
865
  end
814
- end
815
- from_id = from.to_s
816
- from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
817
- from_id = mesh_format_node_id(id: from_id)
818
- PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
819
- PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
820
- mesh_handle_rx(
821
- local: true,
822
- channel_name: channel_name,
823
- msg: {
824
- packet: {
825
- id: packet_id,
826
- channel: radio,
827
- pki_encrypted: !dm_key.nil?,
828
- node_id_from: from_id,
829
- node_id_to: dest,
830
- decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
831
- }
832
- }
833
- )
834
- next unless idx + 1 < chunks.size
835
- next unless %i[serial bluetooth tcp].include?(kind)
866
+ break unless %i[serial bluetooth tcp].include?(kind)
836
867
 
837
- mesh_wait_tx_slot(obj: obj, since: since)
868
+ slot = mesh_wait_tx_slot(obj: obj, since: since, request_id: packet_id)
869
+ break if slot == :ok || slot == :timeout || slot == :error || attempts >= 8
870
+ end
838
871
  end
839
872
  return unless dm_key && PWN.const_defined?(:MeshPendingDm)
840
873
 
@@ -1016,7 +1049,8 @@ module PWN
1016
1049
  next if event[:obj] && (!PWN.const_defined?(:MeshObj) || !event[:obj].equal?(PWN::MeshObj))
1017
1050
 
1018
1051
  if event[:notice]
1019
- mesh_notice(text: event[:notice])
1052
+ notice = mesh_notice_text(text: event[:notice])
1053
+ mesh_notice(text: notice) if notice
1020
1054
  elsif event[:msg]
1021
1055
  mesh_handle_rx(msg: event[:msg])
1022
1056
  else
@@ -1102,6 +1136,14 @@ module PWN
1102
1136
  text
1103
1137
  end
1104
1138
 
1139
+ def mesh_notice_text(opts = {})
1140
+ text = opts[:text].to_s.dup.force_encoding(Encoding::UTF_8).scrub
1141
+ text.gsub!(/\e\[[0-9;?]*[A-Za-z]/, '')
1142
+ text.gsub!(/[\x00-\x08\x0b\x0c\x0e-\x1f]/, '')
1143
+ text.strip!
1144
+ text.empty? ? nil : text
1145
+ end
1146
+
1105
1147
  def mesh_capture_output(opts = {})
1106
1148
  original_out = $stdout
1107
1149
  original_err = $stderr
@@ -1116,7 +1158,8 @@ module PWN
1116
1158
  rescue EOFError
1117
1159
  # Flush the last diagnostic even when the TUI is closing.
1118
1160
  end
1119
- events << { notice: text.force_encoding(Encoding::UTF_8).scrub }
1161
+ notice = mesh_notice_text(text: text)
1162
+ events << { notice: notice } if notice
1120
1163
  end
1121
1164
  rescue IOError
1122
1165
  nil
@@ -1173,7 +1216,7 @@ module PWN
1173
1216
  end
1174
1217
  end
1175
1218
 
1176
- private :mesh_capture_output, :mesh_notice
1219
+ private :mesh_capture_output, :mesh_notice, :mesh_notice_text
1177
1220
 
1178
1221
  # Drop the submitted TX buffer so the next prompt is empty (Reline keeps the old line).
1179
1222
  def mesh_reset_input!(opts = {})
@@ -1551,7 +1594,7 @@ module PWN
1551
1594
  routing = Meshtastic::Routing.decode(routing).to_h if routing.is_a?(String)
1552
1595
  reason = routing[:error_reason] if routing.is_a?(Hash)
1553
1596
  reason = Meshtastic::Routing::Error.lookup(reason) || reason if reason.is_a?(Integer)
1554
- mesh_ui_puts(text: "TX failed: packet #{decoded[:request_id]}: #{reason}") if reason && !%w[0 NONE].include?(reason.to_s)
1597
+ mesh_ui_puts(text: "TX failed: packet #{decoded[:request_id]}: #{reason}") if reason && !%w[0 NONE RATE_LIMIT_EXCEEDED].include?(reason.to_s)
1555
1598
  return
1556
1599
  end
1557
1600
  return unless mesh_text_app?(portnum: decoded[:portnum])
@@ -2185,7 +2228,7 @@ module PWN
2185
2228
 
2186
2229
  private :mesh_transport, :mesh_bound_transport, :mesh_layout, :mesh_box!, :mesh_mqtt_region, :mesh_mqtt_topic, :mesh_active_psks, :mesh_device_channel_meta, :mesh_device_channels
2187
2230
  private :mesh_radio_channel, :mesh_radio_index_for_name, :mesh_channel_name_for_index, :mesh_unassigned_slot_map, :mesh_psk_b64, :mesh_psk_same?, :mesh_env_channel_name_for_psk, :mesh_whitelist_name_for_index, :mesh_channel_name_from_topic, :mesh_link_label
2188
- private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_text_payload_max, :mesh_payload_fits?, :mesh_text_chunks, :mesh_tx_row_ready?, :mesh_wait_tx_slot, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
2231
+ private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_text_payload_max, :mesh_payload_fits?, :mesh_text_chunks, :mesh_tx_row_ready?, :mesh_tx_outcome, :mesh_wait_tx_slot, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
2189
2232
  private :mesh_submit, :mesh_console_loop, :mesh_drain_events, :mesh_draw_input, :mesh_wrap_text, :mesh_ui_puts, :mesh_menu_root, :mesh_list_devices
2190
2233
  private :mesh_start_rx!, :mesh_channel_psks, :mesh_text_app?, :mesh_rx_text, :mesh_self_node_id, :mesh_decorate_local_id, :mesh_public_psk?, :mesh_channel_securely_encrypted?
2191
2234
  private :mesh_ai_whitelisted?, :mesh_ai_prompt, :mesh_broadcast?, :mesh_reply_target_label, :mesh_handle_rx, :mesh_maybe_dispatch_to_pwn_ai, :mesh_refresh_ui!, :mesh_reconnect!
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.742'
4
+ VERSION = '0.5.743'
5
5
  end
@@ -15,7 +15,7 @@ describe 'PWN::AI::Agent::Tools exploitdev' do
15
15
  expect(PWN::AI::Agent::Registry.lookup(name: 'exploitdev')).not_to be_nil
16
16
  end
17
17
 
18
- it 'builds a ret2libc chain end-to-end through Dispatch' do
18
+ it 'builds a ret2libc chain end-to-end through Dispatch', :x86_64_binary do
19
19
  Dir.mktmpdir('pwn-ret2libc-tool-') do |dir|
20
20
  src = File.join(dir, 'vuln.c')
21
21
  bin = File.join(dir, 'vuln')
@@ -60,7 +60,7 @@ describe PWN::Plugins::ExploitDev do
60
60
  expect(row[:gadgets].map { |g| g[:gadget] }).to include(a_string_matching(/pop rdi/i))
61
61
  end
62
62
 
63
- it 'ret2libc builds a packed chain from ELF resolve + pop rdi' do
63
+ it 'ret2libc builds a packed chain from ELF resolve + pop rdi', :x86_64_binary do
64
64
  Dir.mktmpdir('pwn-ret2libc-') do |dir|
65
65
  src = File.join(dir, 'vuln.c')
66
66
  bin = File.join(dir, 'vuln')
@@ -64,7 +64,16 @@ describe PWN::Plugins::Jobs do
64
64
 
65
65
  it 'pages binary logs with byte offsets and distinguishes EOF from completion' do
66
66
  row = described_class.start(command: "printf 'abc\\377def'; sleep 1", max_runtime: 5)
67
- first = described_class.job_tail(handle: row[:id], offset: 0, length: 4)
67
+ first = Timeout.timeout(8) do
68
+ loop do
69
+ page = described_class.job_tail(handle: row[:id], offset: 0, length: 4)
70
+ break page if page[:bytes].to_i.positive?
71
+
72
+ raise 'job ended before log bytes' unless page[:status] == 'RUNNING'
73
+
74
+ sleep 0.05
75
+ end
76
+ end
68
77
  expect(first).to include(bytes: 4, next_offset: 4, encoding: 'base64', eof: false, status: 'RUNNING')
69
78
  expect(Base64.strict_decode64(first[:data])).to eq("abc\xFF".b)
70
79
  last = described_class.tail(id: row[:id], offset: 4, length: 100)
@@ -13,13 +13,31 @@ describe PWN::Plugins::REDB do
13
13
  expect(described_class).to respond_to :help
14
14
  end
15
15
 
16
+ it 'records ARM bl strcpy xrefs from objdump' do
17
+ Dir.mktmpdir('redb-arm') do |dir|
18
+ stub_const('PWN::Plugins::REDB::ROOT', File.join(dir, 'redb'))
19
+ bin = File.join(dir, 'x')
20
+ File.write(bin, 'fake')
21
+ dump = <<~ASM
22
+ 0000000000000000 <caller>:
23
+ 0: d10043ff sub sp, sp, #0x10
24
+ 4: 94000000 bl 8 <strcpy@plt>
25
+ ASM
26
+ allow(Open3).to receive(:capture2).with('nm', '-C', bin).and_return(['', 1])
27
+ allow(Open3).to receive(:capture2).with('objdump', '-d', bin).and_return([dump, 1])
28
+ allow(Open3).to receive(:capture2).with('strings', '-a', bin).and_return(['', 1])
29
+ described_class.open(bin: bin)
30
+ expect(described_class.xrefs_to(bin: bin, sym: 'strcpy').map { |row| row['dst'] || row[:dst] }).to include('strcpy')
31
+ end
32
+ end
33
+
16
34
  it 'answers who calls strcpy from cache on a second open' do
17
35
  Dir.mktmpdir('redb') do |dir|
18
36
  stub_const('PWN::Plugins::REDB::ROOT', File.join(dir, 'redb'))
19
37
  src = File.join(dir, 'x.c')
20
38
  bin = File.join(dir, 'x')
21
39
  File.write(src, "#include <string.h>\nvoid caller(void){ char b[8]; strcpy(b, \"x\"); }\nint main(void){ caller(); return 0; }\n")
22
- system('cc', '-O0', '-g', '-fno-builtin', '-fno-inline', '-o', bin, src)
40
+ expect(system('cc', '-O0', '-g', '-fno-builtin', '-fno-inline', '-o', bin, src)).to be(true)
23
41
  described_class.open(bin: bin)
24
42
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
43
  described_class.open(bin: bin)
@@ -113,6 +113,7 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
113
113
  sent = Meshtastic::ToRadio.decode(args[:to_radio]).packet
114
114
  end
115
115
  allow(described_class).to receive(:mesh_maybe_dispatch_to_pwn_ai)
116
+ allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
116
117
  described_class.send(:mesh_send_text, env: mesh_env, obj: radio, text: 'Local message', channel_name: 'LongFast')
117
118
  expect(sent.decoded.payload).to eq('Local message')
118
119
  packet = { decoded: { emoji: 1, reply_id: sent.id } }
@@ -40,6 +40,47 @@ describe PWN::Plugins::REPL, 'mesh payload chunking' do
40
40
  expect(reconstructed).to eq(body)
41
41
  end
42
42
 
43
+ it 'does not split a word across chunk boundaries' do
44
+ words = (1..80).map { |i| "token#{i}" }
45
+ body = words.join(' ')
46
+ chunks = described_class.send(:mesh_text_chunks, text: body)
47
+ bodies = chunks.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }
48
+ expect(bodies.join).to eq(body)
49
+ expect(chunks.size).to be > 1
50
+ bodies[0..-2].each do |piece|
51
+ expect(piece).to match(/[[:space:]]\z/)
52
+ end
53
+ expect(bodies.join.split).to eq(words)
54
+ end
55
+
56
+ it 'still splits a single word that cannot fit in one payload' do
57
+ body = 'A' * (max + 10)
58
+ chunks = described_class.send(:mesh_text_chunks, text: body)
59
+ expect(chunks.size).to be > 1
60
+ expect(chunks.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }.join).to eq(body)
61
+ end
62
+
63
+ it 'does not treat RATE_LIMIT_EXCEEDED as a free TX slot' do
64
+ obj = {
65
+ proto_data: [{ packet: { decoded: { portnum: :ROUTING_APP, request_id: 7, payload: { error_reason: :RATE_LIMIT_EXCEEDED } } } }],
66
+ rx_mutex: Mutex.new
67
+ }
68
+ t = Time.now
69
+ expect(described_class.send(:mesh_wait_tx_slot, obj: obj, since: 0, timeout: 0.25, request_id: 7)).to eq(:rate_limited)
70
+ expect(Time.now - t).to be < 0.2
71
+ end
72
+
73
+ it 'retries a rate-limited chunk instead of sending the next one' do
74
+ body = 'A' * (max + 10)
75
+ sent = []
76
+ allow(Meshtastic::Serial).to receive(:send_text) { |h| sent << h[:text] }
77
+ allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:rate_limited, :ok, :ok)
78
+ described_class.send(:mesh_send_text, env: env, obj: { proto_data: [] }, text: body, to: '!ffffffff')
79
+ expect(sent.size).to be > 2
80
+ expect(sent[0]).to eq(sent[1])
81
+ expect(sent[2]).not_to eq(sent[0])
82
+ end
83
+
43
84
  it 'keeps each chunk small enough that the encoded ToRadio fits a 256-byte LoRa frame' do
44
85
  body = 'A' * max
45
86
  chunks = described_class.send(:mesh_text_chunks, text: body)
@@ -76,17 +117,17 @@ describe PWN::Plugins::REPL, 'mesh payload chunking' do
76
117
  body = 'A' * (max + 10)
77
118
  obj = { proto_data: [], rx_mutex: Mutex.new }
78
119
  allow(Meshtastic::Serial).to receive(:send_text)
79
- allow(described_class).to receive(:mesh_wait_tx_slot)
120
+ allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
80
121
  described_class.send(:mesh_send_text, env: env, obj: obj, text: body, to: '!ffffffff')
81
122
  n = described_class.send(:mesh_text_chunks, text: body).size
82
- expect(described_class).to have_received(:mesh_wait_tx_slot).exactly(n - 1).times
123
+ expect(described_class).to have_received(:mesh_wait_tx_slot).exactly(n).times
83
124
  end
84
125
 
85
- it 'does not wait for a TX slot after a single payload' do
126
+ it 'waits for a TX slot after a single radio payload' do
86
127
  allow(Meshtastic::Serial).to receive(:send_text)
87
- allow(described_class).to receive(:mesh_wait_tx_slot)
128
+ allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
88
129
  described_class.send(:mesh_send_text, env: env, obj: { proto_data: [] }, text: 'hi', to: '!ffffffff')
89
- expect(described_class).not_to have_received(:mesh_wait_tx_slot)
130
+ expect(described_class).to have_received(:mesh_wait_tx_slot).once
90
131
  end
91
132
 
92
133
  it 'does not treat QueueStatus free as the chunk being on the air' do
@@ -40,6 +40,35 @@ describe PWN::Plugins::REPL, 'mesh compose history' do
40
40
  described_class.send(:mesh_drain_events)
41
41
  end
42
42
 
43
+ it 'does not turn spinner cursor restores into a notice overlay' do
44
+ events = Queue.new
45
+ stub_const('PWN::MeshEvents', events)
46
+ described_class.send(:mesh_capture_output, run: proc {
47
+ 3.times { $stdout.write("\e[?25h") }
48
+ $stderr.write("\e[?25h")
49
+ })
50
+ expect(events).to be_empty
51
+ end
52
+
53
+ it 'does not notice TTYSpinner cursor restore during capture' do
54
+ events = Queue.new
55
+ stub_const('PWN::MeshEvents', events)
56
+ described_class.send(:mesh_capture_output, run: proc {
57
+ PWN::Plugins::TTYSpinner.send(:show_cursor, spin: nil)
58
+ })
59
+ expect(events).to be_empty
60
+ end
61
+
62
+ it 'keeps real warnings after stripping cursor sequences' do
63
+ events = Queue.new
64
+ stub_const('PWN::MeshEvents', events)
65
+ described_class.send(:mesh_capture_output, run: proc {
66
+ $stderr.write("\e[?25hWARNING: decoder fixture\e[?25h\n")
67
+ })
68
+ expect(events.pop(true)[:notice]).to eq('WARNING: decoder fixture')
69
+ expect(events).to be_empty
70
+ end
71
+
43
72
  it 'captures dependency stdout and stderr and restores both streams on failure' do
44
73
  events = Queue.new
45
74
  stub_const('PWN::MeshEvents', events)
data/spec/spec_helper.rb CHANGED
@@ -31,6 +31,7 @@ RSpec.configure do |config|
31
31
  end
32
32
  config.filter_run_excluding :rtl_sdr_hardware unless ENV['PWN_TEST_RTL_SDR_HARDWARE'] == '1'
33
33
  config.filter_run_excluding :combo_nation_mcp unless ENV['PWN_TEST_COMBO_NATION_MCP'] == '1'
34
+ config.filter_run_excluding :x86_64_binary unless RbConfig::CONFIG['host_cpu'].to_s.match?(/amd64|x86_64/)
34
35
 
35
36
  next if ENV['PWN_SPEC_VERBOSE']
36
37
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.742
4
+ version: 0.5.743
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.