Skip to content

Commit d8931cf

Browse files
authored
Add TLS probes to additional OpenSSL funcs to support tracing for Python 3.10 and later (#1338)
Summary: Add TLS probes to additional OpenSSL funcs to support tracing for Python 3.10 and later Python v3.10+ uses the `*_ex` version of the SSL read and write functions. In this PR we expand our eBPF tracing to handle this variant. In particular, we find the number of bytes read or written using the function args. vs. the return value which was used for the non-ex variants of SSL read & write. By expanding our eBPF probes to handle the `*_ex` variants of ssl read & write, we enable TLS tracing for Python 3.10+, but only when Python is not compiled with `--enable-shared` (See #1347 for more details). Additionally, we introduce a new test case using a Python 3.10 docker image. This test case, which is the default image, is compiled with `--enable-shared` and thus requires adding uprobes to `libpython.<version>.so`. The test case has the effect of expanding coverage for both `_ex` variants of ssl read & write and for Python compiled with `--enable-shared`. However, the scope of this PR was not intended to include a release to support tracing Python binaries compiled with `--enable-shared`, rather, that additional feature is planned for a later PR with more diligence behind covering different Python versions. Therefore, we introduce a compile time ifdef PL_BPF_TESTING to enable shared Python tracing for testing (to cover both ex and shared) and to keep the shared Python feature out of our releases for now. Relevant Issues: #1113 #1347 Type of change: /kind feature Test Plan: Added an `openssl_trace_bpf_test` for a Python 3.10 application and existing test coverage Changelog Message: ```release-note Add support for TLS tracing applications using Python 3.10 and later ``` --------- Signed-off-by: Dom Del Nano <ddelnano@pixielabs.ai>
1 parent eaca17d commit d8931cf

File tree

12 files changed

+244
-27
lines changed

12 files changed

+244
-27
lines changed

.bazelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ build:qemu-bpf --sandbox_fake_username
121121
build:qemu-bpf --//bazel/cc_toolchains:libc_version=glibc2_36
122122
build:qemu-bpf --//bazel/test_runners:test_runner=qemu_with_kernel
123123
build:qemu-bpf --run_under="bazel/test_runners/qemu_with_kernel/test_runner.sh"
124+
# TODO(ddelnano): Temporary until https://github.com/pixie-io/pixie/issues/1347
125+
# is addressed. This is to limit the size of the change needed to fix
126+
build:qemu-bpf --copt -DPL_BPF_TESTING
124127
test:qemu-bpf --test_timeout=180,600,1800,3600
125128

126129
# Build for GCC.

src/stirling/bpf_tools/bcc_wrapper.cc

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,17 @@ Status BCCWrapper::AttachUProbe(const UProbeSpec& probe) {
213213
DCHECK((probe.symbol.empty() && probe.address != 0) ||
214214
(!probe.symbol.empty() && probe.address == 0))
215215
<< "Exactly one of 'symbol' and 'address' must be specified.";
216-
PX_RETURN_IF_ERROR(bpf_.attach_uprobe(
216+
auto status = bpf_.attach_uprobe(
217217
probe.binary_path, probe.symbol, std::string(probe.probe_fn), probe.address,
218-
static_cast<bpf_probe_attach_type>(probe.attach_type), probe.pid));
219-
uprobes_.push_back(probe);
220-
++num_attached_uprobes_;
218+
static_cast<bpf_probe_attach_type>(probe.attach_type), probe.pid);
219+
if (!probe.is_optional) {
220+
PX_RETURN_IF_ERROR(status);
221+
}
222+
223+
if (status.ok()) {
224+
uprobes_.push_back(probe);
225+
++num_attached_uprobes_;
226+
}
221227
return Status::OK();
222228
}
223229

src/stirling/bpf_tools/bcc_wrapper.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,13 @@ struct UProbeSpec {
119119

120120
BPFProbeAttachType attach_type = BPFProbeAttachType::kEntry;
121121
std::string probe_fn;
122+
bool is_optional = false;
122123

123124
std::string ToString() const {
124-
return absl::Substitute("[binary=$0 symbol=$1 address=$2 pid=$3 type=$4 probe_fn=$5]",
125-
binary_path.string(), symbol, address, pid,
126-
magic_enum::enum_name(attach_type), probe_fn);
125+
return absl::Substitute(
126+
"[binary=$0 symbol=$1 address=$2 pid=$3 type=$4 probe_fn=$5 optional=$6]",
127+
binary_path.string(), symbol, address, pid, magic_enum::enum_name(attach_type), probe_fn,
128+
is_optional);
127129
}
128130

129131
std::string ToJSON() const {

src/stirling/source_connectors/socket_tracer/bcc_bpf/openssl_trace.c

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ BPF_HASH(openssl_trace_state_debug, uint32_t, struct openssl_trace_state_debug_t
4242

4343
static __inline void process_openssl_data(struct pt_regs* ctx, uint64_t id,
4444
const enum traffic_direction_t direction,
45-
const struct data_args_t* args) {
45+
const struct data_args_t* args, bool is_ex_call) {
4646
// Do not change bytes_count to 'ssize_t' or 'long'.
4747
// Using a 64b data type for bytes_count causes negative values,
4848
// returned as 'int' from open-ssl, to be aliased into positive
@@ -52,6 +52,16 @@ static __inline void process_openssl_data(struct pt_regs* ctx, uint64_t id,
5252
// 2. miscalculate the expected next position (with a very large value)
5353
// Succinctly, DO NOT MODIFY THE DATATYPE for bytes_count.
5454
int bytes_count = PT_REGS_RC(ctx);
55+
56+
// SSL_write_ex and SSL_read_ex will return 1 on success, which means that
57+
// all requested application data bytes have been written to the SSL connection.
58+
// The number of bytes read or written will be contained in the pointer passed
59+
// as the fourth argument to the function.
60+
if (bytes_count == 1 && args->ssl_ex_len != NULL) {
61+
size_t ex_bytes;
62+
BPF_PROBE_READ_VAR(ex_bytes, args->ssl_ex_len);
63+
bytes_count = ex_bytes;
64+
}
5565
process_data(/* vecs */ false, ctx, id, direction, args, bytes_count, /* ssl */ true);
5666
}
5767

@@ -171,7 +181,7 @@ int probe_ret_SSL_write(struct pt_regs* ctx) {
171181

172182
const struct data_args_t* write_args = active_ssl_write_args_map.lookup(&id);
173183
if (write_args != NULL) {
174-
process_openssl_data(ctx, id, kEgress, write_args);
184+
process_openssl_data(ctx, id, kEgress, write_args, false);
175185
}
176186

177187
active_ssl_write_args_map.delete(&id);
@@ -211,16 +221,14 @@ int probe_ret_SSL_read(struct pt_regs* ctx) {
211221

212222
const struct data_args_t* read_args = active_ssl_read_args_map.lookup(&id);
213223
if (read_args != NULL) {
214-
process_openssl_data(ctx, id, kIngress, read_args);
224+
process_openssl_data(ctx, id, kIngress, read_args, false);
215225
}
216226

217227
active_ssl_read_args_map.delete(&id);
218228
return 0;
219229
}
220230

221-
// Function signature being probed:
222-
// int SSL_write(SSL *ssl, const void *buf, int num);
223-
int probe_entry_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
231+
static int probe_entry_SSL_write_syscall_fd_access_agnostic(struct pt_regs* ctx, bool is_ex_call) {
224232
uint64_t id = bpf_get_current_pid_tgid();
225233
uint32_t tgid = id >> 32;
226234

@@ -236,12 +244,31 @@ int probe_entry_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
236244
struct data_args_t write_args = {};
237245
write_args.source_fn = kSSLWrite;
238246
write_args.buf = buf;
247+
if (is_ex_call) {
248+
size_t* ssl_ex_len = (size_t*)PT_REGS_PARM4(ctx);
249+
write_args.ssl_ex_len = ssl_ex_len;
250+
}
251+
239252
active_ssl_write_args_map.update(&id, &write_args);
240253

241254
return 0;
242255
}
243256

244-
int probe_ret_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
257+
// Function signature being probed:
258+
// int SSL_write(SSL *ssl, const void *buf, int num);
259+
int probe_entry_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
260+
return probe_entry_SSL_write_syscall_fd_access_agnostic(ctx, false);
261+
}
262+
263+
// Function signature being probed:
264+
// int SSL_write_ex(SSL *ssl, const void *buf, size_t* written);
265+
//
266+
// On success SSL_write_ex will store the number of bytes written in *written.
267+
int probe_entry_SSL_write_ex_syscall_fd_access(struct pt_regs* ctx) {
268+
return probe_entry_SSL_write_syscall_fd_access_agnostic(ctx, true);
269+
}
270+
271+
static int probe_ret_SSL_write_syscall_fd_access_agnostic(struct pt_regs* ctx, bool is_ex_call) {
245272
uint64_t id = bpf_get_current_pid_tgid();
246273

247274
int fd = get_fd_and_eval_nested_syscall_detection(id);
@@ -253,16 +280,22 @@ int probe_ret_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
253280
// Set socket fd
254281
if (write_args != NULL) {
255282
write_args->fd = fd;
256-
process_openssl_data(ctx, id, kEgress, write_args);
283+
process_openssl_data(ctx, id, kEgress, write_args, is_ex_call);
257284
}
258285

259286
active_ssl_write_args_map.delete(&id);
260287
return 0;
261288
}
262289

263-
// Function signature being probed:
264-
// int SSL_read(SSL *s, void *buf, int num)
265-
int probe_entry_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
290+
int probe_ret_SSL_write_ex_syscall_fd_access(struct pt_regs* ctx) {
291+
return probe_ret_SSL_write_syscall_fd_access_agnostic(ctx, true);
292+
}
293+
294+
int probe_ret_SSL_write_syscall_fd_access(struct pt_regs* ctx) {
295+
return probe_ret_SSL_write_syscall_fd_access_agnostic(ctx, false);
296+
}
297+
298+
static int probe_entry_SSL_read_syscall_fd_access_agnostic(struct pt_regs* ctx, bool is_ex_call) {
266299
uint64_t id = bpf_get_current_pid_tgid();
267300
uint32_t tgid = id >> 32;
268301

@@ -278,12 +311,31 @@ int probe_entry_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
278311
struct data_args_t read_args = {};
279312
read_args.source_fn = kSSLRead;
280313
read_args.buf = buf;
314+
if (is_ex_call) {
315+
size_t* ssl_ex_len = (size_t*)PT_REGS_PARM4(ctx);
316+
read_args.ssl_ex_len = ssl_ex_len;
317+
}
318+
281319
active_ssl_read_args_map.update(&id, &read_args);
282320

283321
return 0;
284322
}
285323

286-
int probe_ret_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
324+
// Function signature being probed:
325+
// int SSL_read(SSL *s, void *buf, int num)
326+
int probe_entry_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
327+
return probe_entry_SSL_read_syscall_fd_access_agnostic(ctx, false);
328+
}
329+
330+
// Function signature being probed:
331+
// int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
332+
//
333+
// On success SSL_read_ex will store the number of bytes actually read in *readbytes.
334+
int probe_entry_SSL_read_ex_syscall_fd_access(struct pt_regs* ctx) {
335+
return probe_entry_SSL_read_syscall_fd_access_agnostic(ctx, true);
336+
}
337+
338+
static int probe_ret_SSL_read_syscall_fd_access_agnostic(struct pt_regs* ctx, bool is_ex_call) {
287339
uint64_t id = bpf_get_current_pid_tgid();
288340

289341
int fd = get_fd_and_eval_nested_syscall_detection(id);
@@ -294,9 +346,17 @@ int probe_ret_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
294346
struct data_args_t* read_args = active_ssl_read_args_map.lookup(&id);
295347
if (read_args != NULL) {
296348
read_args->fd = fd;
297-
process_openssl_data(ctx, id, kIngress, read_args);
349+
process_openssl_data(ctx, id, kIngress, read_args, is_ex_call);
298350
}
299351

300352
active_ssl_read_args_map.delete(&id);
301353
return 0;
302354
}
355+
356+
int probe_ret_SSL_read_ex_syscall_fd_access(struct pt_regs* ctx) {
357+
return probe_ret_SSL_read_syscall_fd_access_agnostic(ctx, true);
358+
}
359+
360+
int probe_ret_SSL_read_syscall_fd_access(struct pt_regs* ctx) {
361+
return probe_ret_SSL_read_syscall_fd_access_agnostic(ctx, false);
362+
}

src/stirling/source_connectors/socket_tracer/bcc_bpf_intf/socket_trace.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ struct data_args_t {
281281

282282
// For sendmmsg()
283283
unsigned int* msg_len;
284+
285+
// For SSL_write_ex and SSL_read_ex
286+
size_t* ssl_ex_len;
284287
};
285288

286289
struct close_args_t {

src/stirling/source_connectors/socket_tracer/openssl_trace_bpf_test.cc

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ using ::px::stirling::testing::SocketTraceBPFTestFixture;
4545
using ::px::stirling::testing::ToRecordVector;
4646

4747
using ::testing::StrEq;
48+
using ::testing::Types;
4849
using ::testing::UnorderedElementsAre;
4950

5051
class NginxOpenSSL_1_1_0_ContainerWrapper
@@ -76,6 +77,11 @@ class Node14_18_1AlpineContainerWrapper
7677
int32_t PID() const { return process_pid(); }
7778
};
7879

80+
class Python310ContainerWrapper : public ::px::stirling::testing::Python310Container {
81+
public:
82+
int32_t PID() const { return process_pid(); }
83+
};
84+
7985
// Includes all information we need to extract from the trace records, which are used to verify
8086
// against the expected results.
8187
struct TraceRecords {
@@ -162,12 +168,12 @@ http::Record GetExpectedHTTPRecord() {
162168
return expected_record;
163169
}
164170

165-
typedef ::testing::Types<NginxOpenSSL_1_1_0_ContainerWrapper, NginxOpenSSL_1_1_1_ContainerWrapper,
166-
Node12_3_1ContainerWrapper, Node14_18_1AlpineContainerWrapper>
167-
OpenSSLServerImplementations;
168-
169-
typedef ::testing::Types<NginxOpenSSL_1_1_1_ContainerWrapper, NginxOpenSSL_3_0_7_ContainerWrapper>
170-
OpenSSLServerNestedSyscallFDImplementations;
171+
using OpenSSLServerImplementations =
172+
Types<NginxOpenSSL_1_1_0_ContainerWrapper, NginxOpenSSL_1_1_1_ContainerWrapper,
173+
Node12_3_1ContainerWrapper, Node14_18_1AlpineContainerWrapper>;
174+
using OpenSSLServerNestedSyscallFDImplementations =
175+
Types<Python310ContainerWrapper, NginxOpenSSL_1_1_1_ContainerWrapper,
176+
NginxOpenSSL_3_0_7_ContainerWrapper>;
171177

172178
template <typename T>
173179
using OpenSSLTraceTest = BaseOpenSSLTraceTest<T, false>;

src/stirling/source_connectors/socket_tracer/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pl_cc_test_library(
6565
"//src/stirling/source_connectors/socket_tracer/testing/containers/amqp:consumer_image.tar",
6666
"//src/stirling/source_connectors/socket_tracer/testing/containers/amqp:producer_image.tar",
6767
"//src/stirling/source_connectors/socket_tracer/testing/containers/pgsql:demo_client_image.tar",
68+
"//src/stirling/source_connectors/socket_tracer/testing/containers/ssl:python_min_310_https_server.tar",
6869
"//src/stirling/source_connectors/socket_tracer/testing/containers/thriftmux:server_image.tar",
6970
"//src/stirling/testing/demo_apps/go_grpc_tls_pl/client:golang_1_17_grpc_tls_client.tar",
7071
"//src/stirling/testing/demo_apps/go_grpc_tls_pl/client:golang_1_18_grpc_tls_client.tar",

src/stirling/source_connectors/socket_tracer/testing/container_images.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ class NginxOpenSSL_3_0_7_Container : public ContainerRunner {
117117
static constexpr std::string_view kReadyMessage = "";
118118
};
119119

120+
class Python310Container : public ContainerRunner {
121+
public:
122+
Python310Container()
123+
: ContainerRunner(::px::testing::BazelRunfilePath(kBazelImageTar), kContainerNamePrefix,
124+
kReadyMessage) {}
125+
126+
private:
127+
static constexpr std::string_view kBazelImageTar =
128+
"src/stirling/source_connectors/socket_tracer/testing/containers/ssl/"
129+
"python_min_310_https_server.tar";
130+
static constexpr std::string_view kContainerNamePrefix = "python_min_310_https_server";
131+
static constexpr std::string_view kReadyMessage = "INFO";
132+
};
133+
120134
class CurlContainer : public ContainerRunner {
121135
public:
122136
CurlContainer()

src/stirling/source_connectors/socket_tracer/testing/containers/ssl/BUILD.bazel

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
#
1515
# SPDX-License-Identifier: Apache-2.0
1616

17-
load("@io_bazel_rules_docker//container:container.bzl", "container_layer")
17+
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer")
18+
load("@io_bazel_rules_docker//python3:image.bzl", "DEFAULT_BASE", "py3_image")
1819
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
1920

2021
package(default_visibility = ["//src/stirling:__subpackages__"])
@@ -87,3 +88,19 @@ container_layer(
8788
directory = "/etc/node",
8889
tars = [":node_client_server"],
8990
)
91+
92+
container_image(
93+
name = "python_base_with_ssl",
94+
base = DEFAULT_BASE,
95+
layers = [
96+
":nginx_html",
97+
":ssl_keys_layer",
98+
],
99+
)
100+
101+
py3_image(
102+
name = "python_min_310_https_server",
103+
srcs = ["https_server.py"],
104+
base = ":python_base_with_ssl",
105+
main = "https_server.py",
106+
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2018- The Pixie Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
import logging
18+
import ssl
19+
import os
20+
from http.server import HTTPServer, BaseHTTPRequestHandler
21+
from sys import exit, version_info
22+
23+
logging.basicConfig(
24+
level=logging.INFO,
25+
format='%(asctime)s %(levelname)s %(message)s',
26+
handlers=[logging.StreamHandler()])
27+
28+
major, minor = version_info.major, version_info.minor
29+
logging.info(f"{version_info}")
30+
if major < 3 or minor < 10:
31+
logging.fatal(f"Python version must be 3.10 or greater for this test assertion. Detected {version_info} instead")
32+
exit(-1)
33+
34+
pid = os.getpid()
35+
logging.info(f"pid={pid}")
36+
37+
file = open("/usr/share/nginx/html/index.html", "r")
38+
PAYLOAD = file.read()
39+
40+
41+
class MyRequestHandler(BaseHTTPRequestHandler):
42+
def do_GET(self):
43+
self.send_response(200)
44+
self.send_header("Content-type", "text/html")
45+
self.send_header("Content-length", len(PAYLOAD))
46+
self.end_headers()
47+
self.wfile.write(bytes(PAYLOAD, 'utf-8'))
48+
49+
50+
httpd = HTTPServer(('localhost', 443), MyRequestHandler)
51+
52+
httpd.socket = ssl.wrap_socket(httpd.socket,
53+
keyfile="/etc/ssl/server.key",
54+
certfile='/etc/ssl/server.crt', server_side=True)
55+
56+
httpd.serve_forever()

0 commit comments

Comments
 (0)