Repository: bpftrace/bpftrace
Pull request #4118: Support accessing up to 255 USDT probe args
Author: login: jschwinger233
Created: 2025-05-13T12:12:01Z | Merged: 2025-05-14T19:08:59Z

<!--
Please provide a description of your change below this comment.

Then please complete the checklist.

Useful contribution guidelines and tips are in docs/developers.md.

Warning: please make sure that you have implemented and tested your
         change against the latest version of bpftrace (unless opening a
         PR for a release branch).
-->

This PR supports arg6-arg11 in USDT. Bpftrace userspace doesn't limit the maximum arg number, even arg9999 is a semantically valid builtin. It raises error when libbpf (via bcc API) fails to find the usdt.

Fixes: https://github.com/bpftrace/bpftrace/issues/3605

##### Checklist

- [ ] Language changes are updated in `man/adoc/bpftrace.adoc`
- [x] User-visible and non-trivial changes updated in `CHANGELOG.md`
- [x] The new behaviour is covered by tests



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c7206f50725..df4f9facd88f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,8 @@ and this project adheres to
   - [#4037](https://github.com/bpftrace/bpftrace/pull/4037)
 - Add warning when unset or empty positional parameters are used
   - [#4095](https://github.com/bpftrace/bpftrace/pull/4095)
+- Support accessing up to 255 USDT probe args
+  - [#4118](https://github.com/bpftrace/bpftrace/pull/4118)
 #### Changed
 - `-p` CLI flag now applies to all probes (except BEGIN/END)
   - [#3800](https://github.com/bpftrace/bpftrace/pull/3800)
diff --git a/src/ast/ast.h b/src/ast/ast.h
index ac175b6470bd..1b38ebdb34a3 100644
--- a/src/ast/ast.h
+++ b/src/ast/ast.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <charconv>
 #include <cstddef>
 #include <cstdint>
 #include <string>
@@ -340,11 +341,24 @@ class Builtin : public Node {
     return builtin_type;
   }
 
-  // Check if the builtin is 'arg0' - 'arg9'
+  // Check if the builtin is 'arg0' - 'arg255'
   bool is_argx() const
   {
-    return !ident.compare(0, 3, "arg") && ident.size() == 4 &&
-           ident.at(3) >= '0' && ident.at(3) <= '9';
+    if (ident.size() < 4 || ident.size() > 6 || !ident.starts_with("arg"))
+      return false;
+
+    std::string_view num_part = ident.substr(3);
+
+    // no leading zeros
+    if (num_part.size() > 1 && num_part.front() == '0')
+      return false;
+
+    int arg_num = 0;
+    auto [ptr, ec] = std::from_chars(num_part.data(),
+                                     num_part.data() + num_part.size(),
+                                     arg_num);
+    return ec == std::errc() && ptr == num_part.data() + num_part.size() &&
+           arg_num >= 0 && arg_num < 256;
   }
 
   std::string ident;
diff --git a/src/ast/irbuilderbpf.cpp b/src/ast/irbuilderbpf.cpp
index d64662e9a18c..c1b52751d6f8 100644
--- a/src/ast/irbuilderbpf.cpp
+++ b/src/ast/irbuilderbpf.cpp
@@ -11,6 +11,7 @@
 #include "bpftrace.h"
 #include "globalvars.h"
 #include "log.h"
+#include "util/exceptions.h"
 
 namespace libbpf {
 #include "libbpf/bpf.h"
@@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
+        "failed to initialize usdt context for probe " + attach_point->target);
   }
 
   std::string ns = attach_point->usdt.provider;
@@ -1505,10 +1505,10 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
                             usdt_location_index,
                             arg_num,
                             &argument) != 0) {
-    builtin.addError() << "couldn't get argument " << arg_num << " for "
-                       << attach_point->target << ":" << attach_point->ns << ":"
-                       << attach_point->func;
-    exit(-2);
+    throw util::FatalUserException("couldn't get argument " +
+                                   std::to_string(arg_num) + " for " +
+                                   attach_point->target + ":" +
+                                   attach_point->ns + ":" + attach_point->func);
   }
 
   Value *result = CreateUSDTReadArgument(ctx, &argument, builtin, as, loc);
diff --git a/src/ast/passes/semantic_analyser.cpp b/src/ast/passes/semantic_analyser.cpp
index 8a71ee9e43d7..c219aa7e5060 100644
--- a/src/ast/passes/semantic_analyser.cpp
+++ b/src/ast/passes/semantic_analyser.cpp
@@ -950,6 +950,7 @@ void SemanticAnalyser::visit(Builtin &builtin)
       return;
     ProbeType pt = probetype(probe->attach_points[0]->provider);
     AddrSpace addrspace = find_addrspace(pt);
+    int arg_num = atoi(builtin.ident.substr(3).c_str());
     for (auto *attach_point : probe->attach_points) {
       ProbeType type = probetype(attach_point->provider);
       if (type != ProbeType::kprobe && type != ProbeType::uprobe &&
@@ -957,11 +958,11 @@ void SemanticAnalyser::visit(Builtin &builtin)
         builtin.addError() << "The " << builtin.ident
                            << " builtin can only be used with "
                            << "'kprobes', 'uprobes' and 'usdt' probes";
+      // argx in USDT probes doesn't need to check against arch::max_arg()
+      if (type != ProbeType::usdt && arg_num > arch::max_arg())
+        builtin.addError() << arch::name() << " doesn't support "
+                           << builtin.ident;
     }
-    int arg_num = atoi(builtin.ident.substr(3).c_str());
-    if (arg_num > arch::max_arg())
-      builtin.addError() << arch::name() << " doesn't support "
-                         << builtin.ident;
     builtin.builtin_type = CreateUInt64();
     builtin.builtin_type.SetAS(addrspace);
   } else if (!builtin.ident.compare(0, 4, "sarg") &&
diff --git a/src/lexer.l b/src/lexer.l
index 92289646daed..89ffc32064cc 100644
--- a/src/lexer.l
+++ b/src/lexer.l
@@ -44,7 +44,7 @@ hspace   [ \t]
 vspace   [\n\r]
 space    {hspace}|{vspace}
 path     :(\\.|[_\-\./a-zA-Z0-9#$+\*])+
-builtin  arg[0-9]|args|cgroup|comm|cpid|numaid|cpu|ncpus|ctx|curtask|elapsed|func|gid|pid|probe|rand|retval|sarg[0-9]|tid|uid|username|jiffies|nsecs|kstack|ustack
+builtin  arg[0-9]+|args|cgroup|comm|cpid|numaid|cpu|ncpus|ctx|curtask|elapsed|func|gid|pid|probe|rand|retval|sarg[0-9]|tid|uid|username|jiffies|nsecs|kstack|ustack
 
 int_type        bool|(u)?int(8|16|32|64)
 builtin_type    void|(u)?(min|max|sum|count|avg|stats)_t|probe_t|username|lhist_t|hist_t|usym_t|ksym_t|timestamp|macaddr_t|cgroup_path_t|strerror_t|kstack_t|ustack_t
diff --git a/tests/runtime/usdt b/tests/runtime/usdt
index e9a47b245dac..f4bca6e6ebe4 100644
--- a/tests/runtime/usdt
+++ b/tests/runtime/usdt
@@ -132,7 +132,7 @@ EXPECT Attaching 3 probes...
 
 NAME usdt probes - attach to probe on multiple files by wildcard
 PROG usdt:./testprogs/usdt*::* { printf("here\n" ); exit(); }
-EXPECT Attaching 48 probes...
+EXPECT Attaching 49 probes...
 
 NAME usdt probes - attach to probe on multiple providers by wildcard and pid
 RUN {{BPFTRACE}} -e 'usdt:::*probe2 { printf("here\n" ); exit(); }' -p {{BEFORE_PID}}
@@ -287,3 +287,8 @@ RUN {{BPFTRACE}} runtime/scripts/usdt_multi_modules.bt
 EXPECT Attaching 2 probes...
 TIMEOUT 1
 REQUIRES ./testprogs/systemtap_sys_sdt_check
+
+NAME usdt multi arguments
+RUN {{BPFTRACE}} -e 'usdt:./testprogs/usdt_multi_args:usdt_multi_args:probe1 { printf("%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %lld %llx\n", arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); exit(); }' -c ./testprogs/usdt_multi_args
+EXPECT deadbeef 1 ffffffffffffffff cafebabe 8badf00d feedface 123456789abcdef 0 7fffffffffffffff 5555555555555555 42 aaaaaaaaaaaaaaaa
+REQUIRES ./testprogs/systemtap_sys_sdt_check
diff --git a/tests/testprogs/usdt_multi_args.c b/tests/testprogs/usdt_multi_args.c
new file mode 100644
index 000000000000..4f7c6009bf98
--- /dev/null
+++ b/tests/testprogs/usdt_multi_args.c
@@ -0,0 +1,29 @@
+#ifdef HAVE_SYSTEMTAP_SYS_SDT_H
+#include <sys/sdt.h>
+#else
+#define DTRACE_PROBE1(a, b, c) (void)0
+#endif
+#include <stdint.h>
+
+int main()
+{
+  uint32_t a = 0xdeadbeef;
+  uint32_t b = 1;
+  uint64_t c = UINT64_MAX;
+  uint32_t d = 0xcafebabe;
+  uint32_t e = 0x8badf00d;
+  uint32_t f = 0xfeedface;
+  uint64_t g = 0x0123456789abcdefULL;
+  uint64_t h = 0x0ULL;
+  uint64_t i = 0x7fffffffffffffffULL;
+  uint64_t j = 0x5555555555555555ULL;
+  uint32_t k = 42;
+  uint64_t l = 0xaaaaaaaaaaaaaaaaULL;
+
+  while (1) {
+    DTRACE_PROBE12(usdt_multi_args, probe1, a, b, c, d, e, f, g, h, i, j, k, l);
+  }
+
+  return 0;
+}
+


Reviews:
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:14:18Z
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:14:43Z
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:15:01Z
user: login: jschwinger233
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:30:38Z
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:51:31Z
user: login: jschwinger233
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:02:58Z
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:07:39Z
user: login: jordalgo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:00:19Z
user: login: jordalgo
body: Thanks for the contribution!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T19:07:24Z

Review comments:
diff_hunk: @@ -37,6 +37,8 @@ and this project adheres to
   - [#4037](https://github.com/bpftrace/bpftrace/pull/4037)
 - Add warning when unset or empty positional parameters are used
   - [#4095](https://github.com/bpftrace/bpftrace/pull/4095)
+- Support access arg0-arg11 in USDT probes
path: CHANGELOG.md
user: login: jordalgo
body: From the comment/code below, it looks like we can support up to arg255. Also, I might re-word this to say "Support accessing up to 255 USDT probe args"
created_at: 2025-05-14T10:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
path: src/ast/irbuilderbpf.cpp
user: login: jordalgo
body: why the change to a throw here?
created_at: 2025-05-14T10:14:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1505,10 +1505,10 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
                             usdt_location_index,
                             arg_num,
                             &argument) != 0) {
-    builtin.addError() << "couldn't get argument " << arg_num << " for "
-                       << attach_point->target << ":" << attach_point->ns << ":"
-                       << attach_point->func;
-    exit(-2);
+    throw util::FatalUserException("couldn't get argument " +
path: src/ast/irbuilderbpf.cpp
user: login: jordalgo
body: Same question as above
created_at: 2025-05-14T10:15:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
path: src/ast/irbuilderbpf.cpp
user: login: jschwinger233
body: (I wrote reasons in the commit message :grinning: )

Previously exit() terminates the process immediately, leaving no chance
to output error buffer. This patch instead raises the FatalUserException
with useful information to let users known arg12+ is yet supported by
libbpf.

created_at: 2025-05-14T10:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
path: src/ast/irbuilderbpf.cpp
user: login: jordalgo
body: Ah my apologies, I missed the `exit(-1)`. Just curious, what happens if we just return and not throw or exit? I see that we're adding an error to the builtin - hopefully the passmanger should recognize this and terminate the execution anyway with the correct messaging.
created_at: 2025-05-14T12:51:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
path: src/ast/irbuilderbpf.cpp
user: login: jschwinger233
body: > what happens if we just return

I tried before, bpftrace crashed with segfault.
created_at: 2025-05-14T13:02:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1491,9 +1492,8 @@ Value *IRBuilderBPF::CreateUSDTReadArgument(Value *ctx,
   }
 
   if (usdt == nullptr) {
-    builtin.addError() << "failed to initialize usdt context for probe "
-                       << attach_point->target;
-    exit(-1);
+    throw util::FatalUserException(
path: src/ast/irbuilderbpf.cpp
user: login: jordalgo
body: Ah ok, no worries. Throwing is fine then. I'll take a look at it later.
created_at: 2025-05-14T13:07:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,6 +39,8 @@ and this project adheres to
   - [#4037](https://github.com/bpftrace/bpftrace/pull/4037)
 - Add warning when unset or empty positional parameters are used
   - [#4095](https://github.com/bpftrace/bpftrace/pull/4095)
+- Support access up to 255 USDT probe args
path: CHANGELOG.md
user: login: jordalgo
body: ```suggestion
- Support accessing up to 255 USDT probe args
```
created_at: 2025-05-14T15:00:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: jordalgo
created_at: 2025-05-14T12:55:04Z
body: @jschwinger233 So is there a hard limit on the number of USDT arguments for bcc or libbpf? If so maybe we should add a check for that in semantic_analyser. If it's a moving target then it's fine to keep it as you have it.
author_association: CONTRIBUTOR
user: login: jschwinger233
created_at: 2025-05-14T13:08:07Z
body: @jordalgo the usdt lib https://github.com/libbpf/usdt/blob/46c09f9440257639afc9f8cbf9dcb51f01dd9929/usdt.h#L428  supports up to 12 usdt args for now, it could be increased in future. 

I was just following the issue description from https://github.com/bpftrace/bpftrace/issues/3605, I don't have strong opinions. 

> Support more than 6 args for usdt probes. We could probably just allow u32::max() or something.
author_association: CONTRIBUTOR
user: login: jordalgo
created_at: 2025-05-14T14:56:19Z
body: @jschwinger233 That USDT library is for user-space programs so we don't need to worry about how much that one supports. Do you see any errors in your test program (for the runtime test) if you have 20 arguments (for example) ?
author_association: CONTRIBUTOR
user: login: jschwinger233
created_at: 2025-05-14T15:05:04Z
body: @jordalgo 

> Do you see any errors in your test program (for the runtime test) if you have 20 arguments (for example) ?

I don't know how to compile an elf with 20 usdt args :disappointed_relieved: so I didn't test that

the usdt is set by 

```c
    DTRACE_PROBE12(usdt_multi_args, probe1, a, b, c, d, e, f, g, h, i, j, k, l);
```
which is defind in `<sys/sdt.h>`. Right now 12 is the max arg number supported in sdt.h.

---

If we run `result/bin/bpftrace   -e 'usdt:./a.out:usdt_multi_args:probe1 {printf("%llx \n", arg12); exit();}' ` where the `a.out` only has 12 usdt args while arg12 tries to fetch the 13th arg, bpftrace ends up with:

```
terminate called after throwing an instance of 'bpftrace::util::FatalUserException'
  what():  couldn't get argument 12 for ./a.out:usdt_multi_args:probe1
Aborted
```

author_association: CONTRIBUTOR
user: login: jordalgo
created_at: 2025-05-14T17:12:11Z
body: @jschwinger233 No worries. I think we can merge as is. Just fix up the changelog.
author_association: CONTRIBUTOR
user: login: danobi
created_at: 2025-05-14T20:09:26Z
body: Sweet, thank you!
author_association: MEMBER