rbbcc 0.11.4 → 0.11.6

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: e4f674b42de394618b5c86e8b57a5d0edbf3132310eb11547f789ce098baa0f2
4
- data.tar.gz: a0d5a695da3347a3ad3752c650436f8f96386db7231c724b5f2af428383d60e9
3
+ metadata.gz: 4bd5c0ae3d72c8e4e931d4ecb48872212c1389dd56897a5c16ca3626a1a7a725
4
+ data.tar.gz: c117a487cbc4c1b5fed3df4eea8813d4b6c16ea3c6dcd306e41b504fb2776bc3
5
5
  SHA512:
6
- metadata.gz: '069947eb933d84024ba9b951fb8ca08768947bf13ce88b40b3d3dba1f99adc66dcb7e1a5c45deb7cddc4d28e476eddb877454e4e61b15a8ff1f85b2299bc0657'
7
- data.tar.gz: 0bba44088c66da0893a64e34e238e94c951c676838604ba43ad019f37bdcbae9b4eb586173e11e8f1919fd691334349155c9325ff98e634282afcb7c712c2ca4
6
+ metadata.gz: 1c11515d0a7b919ffaa6c0001cc74b96f0bbc217e07fa46c2fe6b37398499a6cb81fac23425e45f32b8802b6b90a6c107f845ef3b610ea7838d8b29178129adc
7
+ data.tar.gz: 253c500eebc928aa13327496b4cdb96f844a91df06aa5c4bf85561a1958f2c181f792e24262b1ae9d5ac0562796ffb475ae178b2a88a8451f17e50f6c1f7f727
data/.gitignore CHANGED
@@ -7,6 +7,8 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /.agent
11
+ /.claude
10
12
 
11
13
  .ruby-version
12
14
 
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gem "bundler", "~> 2.0"
7
7
  gem "rake", "~> 13.0"
8
8
  gem "pry", "~> 0.12"
9
9
  gem "minitest", "~> 5"
10
+ gem "http-2", "~> 1.1"
10
11
 
11
12
  #group :omnibus_package do
12
13
  # gem "appbundler"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbbcc (0.11.4)
4
+ rbbcc (0.11.6)
5
5
  fiddle
6
6
 
7
7
  GEM
@@ -9,6 +9,7 @@ GEM
9
9
  specs:
10
10
  coderay (1.1.3)
11
11
  fiddle (1.1.8)
12
+ http-2 (1.1.3)
12
13
  io-console (0.8.2)
13
14
  method_source (1.1.0)
14
15
  minitest (5.27.0)
@@ -28,6 +29,7 @@ PLATFORMS
28
29
 
29
30
  DEPENDENCIES
30
31
  bundler (~> 2.0)
32
+ http-2 (~> 1.1)
31
33
  minitest (~> 5)
32
34
  pry (~> 0.12)
33
35
  rake (~> 13.0)
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rbbcc'
3
+ include RbBCC
4
+
5
+ PIN_PATH = "/sys/fs/bpf/pinned_ringbuf"
6
+
7
+ if File.exist?(PIN_PATH)
8
+ puts "Removing old pinned map at #{PIN_PATH}..."
9
+ File.unlink(PIN_PATH)
10
+ end
11
+
12
+ bpf_text = <<~CLANG
13
+ #include <uapi/linux/ptrace.h>
14
+
15
+ struct data_t {
16
+ u32 pid;
17
+ char comm[16];
18
+ };
19
+
20
+ BPF_RINGBUF_OUTPUT(events, 4);
21
+
22
+ int trace_uname(struct pt_regs *ctx) {
23
+ struct data_t *data = events.ringbuf_reserve(sizeof(struct data_t));
24
+ if (!data) return 0;
25
+
26
+ data->pid = bpf_get_current_pid_tgid() >> 32;
27
+ bpf_get_current_comm(&data->comm, sizeof(data->comm));
28
+
29
+ events.ringbuf_submit(data, 0);
30
+ return 0;
31
+ }
32
+ CLANG
33
+
34
+ puts "Process A (Ruby): Loading BPF and attaching tracepoint..."
35
+ b = BCC.new(text: bpf_text)
36
+ b.attach_tracepoint(tp: "syscalls:sys_enter_newuname", fn_name: "trace_uname")
37
+
38
+ puts "Process A (Ruby): Pinning ringbuf map to #{PIN_PATH}..."
39
+ b["events"].pin!(PIN_PATH)
40
+
41
+ puts "\n[Process A Active] Kept alive to feed events. Press Ctrl+C to stop."
42
+ begin
43
+ loop do
44
+ sleep 1
45
+ end
46
+ ensure
47
+ if File.exist?(PIN_PATH)
48
+ puts "\nUnpinning map..."
49
+ File.unlink(PIN_PATH)
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rbbcc'
3
+ include RbBCC
4
+
5
+ PIN_PATH = "/sys/fs/bpf/pinned_ringbuf"
6
+
7
+ type = "struct data_t {
8
+ u32 pid;
9
+ char comm[16];
10
+ };"
11
+
12
+ puts "Process B (Ruby): Initializing consumer client..."
13
+ buf = RingBuf.from_pin(PIN_PATH, type, 4)
14
+
15
+ # ring_buffer listner
16
+ buf.open_ring_buffer do |cpu, data, size|
17
+ event = buf.event(data)
18
+ puts "[Process B Captured] PID: #{event.pid.to_s.ljust(6)} | COMMAND: #{event.comm}"
19
+ end
20
+
21
+ puts "\n[Process B Active] Successfully hooked to pinned map (FD: #{buf.map_fd}). Listening for events...\n\n"
22
+
23
+ begin
24
+ loop do
25
+ buf.ring_buffer_poll()
26
+ end
27
+ rescue Interrupt
28
+ puts "\nExiting Process B."
29
+ end
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rbbcc"
5
+ include RbBCC
6
+
7
+ begin
8
+ require "http/2"
9
+ rescue LoadError
10
+ # Fallback for local vendor source
11
+ local_http2_lib = File.expand_path("../.agent/sources/http-2/lib", __dir__)
12
+ $LOAD_PATH.unshift(local_http2_lib) unless $LOAD_PATH.include?(local_http2_lib)
13
+ require "http/2"
14
+ end
15
+
16
+ FRAME_TYPE_NAMES = {
17
+ 0x0 => "DATA",
18
+ 0x1 => "HEADERS",
19
+ 0x2 => "PRIORITY",
20
+ 0x3 => "RST_STREAM",
21
+ 0x4 => "SETTINGS",
22
+ 0x5 => "PUSH_PROMISE",
23
+ 0x6 => "PING",
24
+ 0x7 => "GOAWAY",
25
+ 0x8 => "WINDOW_UPDATE",
26
+ 0x9 => "CONTINUATION"
27
+ }.freeze
28
+
29
+ HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".b
30
+ HTTP1_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS TRACE CONNECT].freeze
31
+
32
+ BPF_TEXT = <<~BPF
33
+ #include <uapi/linux/ptrace.h>
34
+
35
+ #define MAX_PAYLOAD_SIZE 1024
36
+
37
+ struct event_t {
38
+ u32 pid;
39
+ char event_name[8];
40
+ unsigned char payload[MAX_PAYLOAD_SIZE];
41
+ u32 data_len;
42
+ s32 read_ret;
43
+ };
44
+
45
+ BPF_RINGBUF_OUTPUT(events, 256);
46
+
47
+ int trace_ssl_write(struct pt_regs *ctx) {
48
+ u32 tgid = bpf_get_current_pid_tgid() >> 32;
49
+
50
+ const char *user_buf = (const char *)PT_REGS_PARM2(ctx);
51
+ u64 num = (u64)PT_REGS_PARM3(ctx);
52
+ if (!user_buf || num <= 0) return 0;
53
+
54
+ struct event_t *event = events.ringbuf_reserve(sizeof(struct event_t));
55
+ if (!event) return 0;
56
+
57
+ event->pid = tgid;
58
+ __builtin_memcpy(event->event_name, "ssl_w", 6);
59
+
60
+ u32 len = num > MAX_PAYLOAD_SIZE ? MAX_PAYLOAD_SIZE : (u32)num;
61
+ event->data_len = len;
62
+
63
+ event->read_ret = bpf_probe_read_user(event->payload, len, user_buf);
64
+ if (event->read_ret < 0) {
65
+ event->data_len = 0;
66
+ }
67
+
68
+ events.ringbuf_submit(event, 0);
69
+ return 0;
70
+ }
71
+ BPF
72
+
73
+ def hex_ascii_dump(payload, width: 16)
74
+ lines = []
75
+ payload.bytes.each_slice(width).with_index do |slice, idx|
76
+ offset = idx * width
77
+ hex = slice.map { |b| "%02x" % b }.join(" ")
78
+ ascii = slice.map { |b| b >= 32 && b <= 126 ? b.chr : "." }.join
79
+ lines << format("%04x %-#{width * 3 - 1}s %s", offset, hex, ascii)
80
+ end
81
+ lines.join("\n")
82
+ end
83
+
84
+ def payload_from_event(event)
85
+ raw = event.payload
86
+ bytes = case raw
87
+ when String
88
+ raw.b
89
+ when Array
90
+ raw.map { |v| v & 0xff }.pack("C*")
91
+ else
92
+ if raw.respond_to?(:to_a)
93
+ raw.to_a.map { |v| v & 0xff }.pack("C*")
94
+ else
95
+ raw.to_s.b
96
+ end
97
+ end
98
+
99
+ len = [event.data_len.to_i, bytes.bytesize].min
100
+ bytes.byteslice(0, len) || "".b
101
+ end
102
+
103
+ def parse_http2_frames(payload)
104
+ frames = []
105
+ i = 0
106
+ total = payload.bytesize
107
+
108
+ while i + 9 <= total
109
+ length_bytes = payload.byteslice(i, 3).bytes
110
+ length = (length_bytes[0] << 16) | (length_bytes[1] << 8) | length_bytes[2]
111
+ frame_type = payload.getbyte(i + 3)
112
+ flags = payload.getbyte(i + 4)
113
+ stream_id = payload.byteslice(i + 5, 4).unpack1("N") & 0x7fff_ffff
114
+ i += 9
115
+ break if i + length > total
116
+
117
+ frame_payload = payload.byteslice(i, length)
118
+ i += length
119
+ frames << [frame_type, flags, stream_id, frame_payload]
120
+ end
121
+
122
+ frames
123
+ end
124
+
125
+ def extract_headers_fragment(frame_type, flags, frame_payload)
126
+ return frame_payload if frame_type == 0x9 # CONTINUATION
127
+ return nil unless frame_type == 0x1 # HEADERS only
128
+
129
+ start_idx = 0
130
+ end_idx = frame_payload.bytesize
131
+
132
+ if (flags & 0x08) != 0 # PADDED
133
+ return nil if end_idx.zero?
134
+
135
+ pad_len = frame_payload.getbyte(0)
136
+ start_idx += 1
137
+ end_idx = [start_idx, end_idx - pad_len].max
138
+ end
139
+
140
+ if (flags & 0x20) != 0 # PRIORITY
141
+ return nil if start_idx + 5 > end_idx
142
+
143
+ start_idx += 5
144
+ end
145
+
146
+ frame_payload.byteslice(start_idx, end_idx - start_idx) || "".b
147
+ end
148
+
149
+ def decode_pseudo_headers_with_hpack(header_block, decompressor)
150
+ pairs = decompressor.decode(header_block.b)
151
+ pseudo = {}
152
+ pairs.each do |k, v|
153
+ pseudo[k] = v
154
+ end
155
+ pseudo
156
+ rescue StandardError => e
157
+ { ":error" => e.message }
158
+ end
159
+
160
+ def maybe_http1_summary(payload)
161
+ text = payload.force_encoding(Encoding::UTF_8)
162
+ first_line = text.split("\r\n", 2).first
163
+ return nil if first_line.nil? || first_line.empty?
164
+
165
+ if HTTP1_METHODS.any? { |m| first_line.start_with?("#{m} ") }
166
+ return ["request", first_line]
167
+ end
168
+
169
+ if first_line.start_with?("HTTP/1.1 ") || first_line.start_with?("HTTP/1.0 ")
170
+ return ["response", first_line]
171
+ end
172
+
173
+ nil
174
+ rescue ArgumentError, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
175
+ nil
176
+ end
177
+
178
+ def main
179
+ puts "SSL HTTP tracer (Ruby + ring buffer) を初期化中..."
180
+
181
+ b = BCC.new(text: BPF_TEXT)
182
+
183
+ libssl_path = ENV.fetch("LIBSSL_PATH", "/usr/lib/aarch64-linux-gnu/libssl.so.3")
184
+ b.attach_uprobe(name: libssl_path, sym: "SSL_write", fn_name: "trace_ssl_write")
185
+ puts "Attached uprobe: SSL_write (#{libssl_path})"
186
+
187
+ decompressor = HTTP2::Header::Decompressor.new
188
+ continuation_state = {}
189
+
190
+ b["events"].open_ring_buffer do |_ctx, data, _size|
191
+ event = b["events"].event(data)
192
+ event_name = event.event_name.to_s
193
+ next unless event_name == "ssl_w"
194
+
195
+ payload = payload_from_event(event)
196
+ puts "[PID: #{event.pid}] len=#{event.data_len} read_ret=#{event.read_ret}"
197
+ next if event.data_len.to_i <= 0
198
+
199
+ summary = maybe_http1_summary(payload)
200
+ if summary
201
+ kind, line = summary
202
+ puts "[HTTP/1.x #{kind}] #{line}"
203
+ puts hex_ascii_dump(payload)
204
+ next
205
+ end
206
+
207
+ rest = payload
208
+ if rest.start_with?(HTTP2_PREFACE)
209
+ puts "[H2] client preface detected"
210
+ rest = rest.byteslice(HTTP2_PREFACE.bytesize..) || "".b
211
+ end
212
+
213
+ frames = parse_http2_frames(rest)
214
+ if frames.empty?
215
+ puts hex_ascii_dump(payload)
216
+ next
217
+ end
218
+
219
+ frames.each do |frame_type, flags, stream_id, frame_payload|
220
+ frame_name = FRAME_TYPE_NAMES.fetch(frame_type, "UNKNOWN(#{frame_type})")
221
+ puts "[H2] type=#{frame_name} flags=0x#{format('%02x', flags)} stream=#{stream_id} len=#{frame_payload.bytesize}"
222
+
223
+ case frame_type
224
+ when 0x1 # HEADERS
225
+ fragment = extract_headers_fragment(frame_type, flags, frame_payload)
226
+ if fragment.nil?
227
+ puts "[HPACK] parse_error=invalid HEADERS payload"
228
+ next
229
+ end
230
+
231
+ key = [event.pid.to_i, stream_id]
232
+ if (flags & 0x04) != 0 # END_HEADERS
233
+ pseudo = decode_pseudo_headers_with_hpack(fragment, decompressor)
234
+ if pseudo.key?(":error")
235
+ puts "[HPACK] decode_error=#{pseudo[":error"]}"
236
+ else
237
+ puts "[HPACK] :method=#{pseudo.fetch(":method", "?")} :path=#{pseudo.fetch(":path", "?")}"
238
+ end
239
+ else
240
+ continuation_state[key] = fragment.dup
241
+ puts "[HPACK] collecting CONTINUATION fragments"
242
+ end
243
+ when 0x9 # CONTINUATION
244
+ key = [event.pid.to_i, stream_id]
245
+ fragment = extract_headers_fragment(frame_type, flags, frame_payload)
246
+ unless continuation_state.key?(key)
247
+ puts "[HPACK] note=orphan CONTINUATION frame"
248
+ next
249
+ end
250
+
251
+ continuation_state[key] << fragment
252
+ next if (flags & 0x04).zero?
253
+
254
+ header_block = continuation_state.delete(key)
255
+ pseudo = decode_pseudo_headers_with_hpack(header_block, decompressor)
256
+ if pseudo.key?(":error")
257
+ puts "[HPACK] decode_error=#{pseudo[":error"]}"
258
+ else
259
+ puts "[HPACK] :method=#{pseudo.fetch(":method", "?")} :path=#{pseudo.fetch(":path", "?")}"
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ puts "監視開始。Ctrl+C で終了"
266
+ loop do
267
+ b.ring_buffer_poll(100)
268
+ rescue Interrupt
269
+ puts "\n終了します。"
270
+ exit(0)
271
+ end
272
+ end
273
+
274
+ main if $PROGRAM_NAME == __FILE__
data/lib/rbbcc/bcc.rb CHANGED
@@ -607,7 +607,10 @@ module RbBCC
607
607
  end
608
608
 
609
609
  def _open_ring_buffer(map_fd, fn, ctx)
610
- buf = Clib.bpf_new_ringbuf(map_fd, fn, ctx)
610
+ # Avoid GC'ing function pointer
611
+ @_ring_buffer_fns ||= []
612
+ @_ring_buffer_fns << fn
613
+ buf = Clib.bpf_new_ringbuf(map_fd, @_ring_buffer_fns[-1], ctx)
611
614
  if !buf
612
615
  raise "Could not open ring buffer"
613
616
  end
data/lib/rbbcc/consts.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  module RbBCC
2
- module BPF
2
+ class BCC
3
+ end
4
+ BPF = BCC # avoid confuse from python port
5
+
6
+ class BPF
3
7
  # From bpf_prog_type in uapi/linux/bpf.h
4
8
  SOCKET_FILTER = 1
5
9
  KPROBE = 2
data/lib/rbbcc/table.rb CHANGED
@@ -40,6 +40,8 @@ module RbBCC
40
40
  fields << [field_type, field_name].join(" ")
41
41
  end
42
42
  end
43
+ return nil if fields.empty?
44
+
43
45
  klass = Fiddle::Importer.struct(fields)
44
46
  char_ps = fields.select {|f| f =~ /^char\[(\d+)\] ([_a-zA-Z0-9]+)/ }
45
47
  unless char_ps.empty?
@@ -280,6 +282,11 @@ module RbBCC
280
282
  false # TODO: implement me in the future
281
283
  end
282
284
 
285
+ # Just a wrapper to BCC class method
286
+ def pin!(path)
287
+ BCC.pin!(self.map_fd, path)
288
+ end
289
+
283
290
  private
284
291
  def normalize_key(key)
285
292
  case key
@@ -368,7 +375,7 @@ module RbBCC
368
375
  end
369
376
 
370
377
  def event(data)
371
- @event_class ||= get_event_class
378
+ @event_class ||= (get_event_class || self.leaftype)
372
379
  ev = @event_class.malloc
373
380
  Fiddle::Pointer.new(ev.to_ptr)[0, @event_class.size] = data[0, @event_class.size]
374
381
  return ev
@@ -429,7 +436,28 @@ module RbBCC
429
436
 
430
437
  class RingBuf < TableBase
431
438
  include EventTypeSupported
432
-
439
+
440
+ # Make a dynamic BCC program to load the pinned ringbuf map
441
+ def self.from_pin(path, leaftype, size, name: "events")
442
+ map_fd = Clib.bpf_obj_get(path)
443
+ if map_fd < 0
444
+ raise SystemCallError.new("Could not open pinned map", Fiddle.last_error)
445
+ end
446
+ leaftype_typename = case leaftype
447
+ when /\Astruct\s+(\w+)\s+{/m
448
+ "struct #{Regexp.last_match(1)}"
449
+ else
450
+ leaftype
451
+ end
452
+
453
+ prog = <<~CLANG
454
+ #{leaftype}
455
+ BPF_TABLE_PINNED("ringbuf", u32, #{leaftype_typename}, #{name}, #{size}, "#{path}");
456
+ CLANG
457
+ b = BCC.new(text: prog)
458
+ b[name.to_s]
459
+ end
460
+
433
461
  def initialize(bpf, map_id, map_fd, keytype, leaftype, name: nil)
434
462
  super
435
463
  @_ringbuf = nil
@@ -438,7 +466,7 @@ module RbBCC
438
466
  end
439
467
 
440
468
  def event(data)
441
- @event_class ||= get_event_class
469
+ @event_class ||= (get_event_class || self.leaftype)
442
470
  ev = @event_class.malloc
443
471
  Fiddle::Pointer.new(ev.to_ptr)[0, @event_class.size] = data[0, @event_class.size]
444
472
  return ev
@@ -470,6 +498,10 @@ module RbBCC
470
498
  @bpf._open_ring_buffer(@map_fd, fn, ctx)
471
499
  nil
472
500
  end
501
+
502
+ def ring_buffer_poll(timeout=-1)
503
+ @bpf.ring_buffer_poll(timeout)
504
+ end
473
505
  end
474
506
 
475
507
  class StackTrace < TableBase
data/lib/rbbcc/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RbBCC
2
- VERSION = "0.11.4"
2
+ VERSION = "0.11.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbbcc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.4
4
+ version: 0.11.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -99,8 +99,11 @@ files:
99
99
  - examples/pin_maps_a.rb
100
100
  - examples/pin_maps_b.rb
101
101
  - examples/py-orig/sockblock.py
102
+ - examples/ringbuf_pin_a.rb
103
+ - examples/ringbuf_pin_b.rb
102
104
  - examples/ruby_usdt.rb
103
105
  - examples/sbrk_trace.rb
106
+ - examples/ssl_http_trace.rb
104
107
  - examples/syscalluname.rb
105
108
  - examples/table.rb
106
109
  - examples/tools/bashreadline.rb