Debug with gdb#
Take a distroless container — no shell, no gdb, no libc headers, nothing to exec into — and land on a breakpoint with source shown and locals readable. Ten minutes, most of it image pulls.
Note
Commands here are written podbench <verb> — the only spelling there is. If you
have not installed the launcher, run each as uvx podbench <verb>. See
Setup.
Warning
A breakpoint on a probed pod is on a timer. A process stopped in a debugger
does not answer its probes, and the kubelet cannot tell that from a hang. Two
deadlines follow, and the quiet one is the one that will catch you out. Both are
(failureThreshold - 1) x max(periodSeconds, timeoutSeconds) + timeoutSeconds
after the pause begins, plus up to one more period depending on where in the
cycle it began:
deadline |
what happens |
how visible |
|---|---|---|
|
the pod goes not-ready and stops taking Service traffic — the address stays in the EndpointSlice with |
quiet — nothing restarts, and it recovers a probe period after you continue, so afterwards nothing points at the debugger |
|
the container is killed and restarted, and the seat — which shares its namespaces — is killed with it |
loud — an event, a bumped restart count, and a burnt seat name: an ephemeral container cannot be restarted, so coming back needs |
Measured against tests/e2e/apps/python-service.yaml (readiness every 5 s,
liveness every 10 s, both failureThreshold: 3, both timeoutSeconds: 1, so
11–16 s and 21–31 s): a gdb attach held 18 s took the pod out of the Service
after ~12 s and put it back 5 s after detach, with no restart. Held 45 s, it
also produced Container app failed liveness probe, will be restarted at ~25 s
and exitCode: 137 on both the workload and the seat.
podbench attach prints these numbers for the pod you name, computed from its
own spec — including the opposite answer, “no probes, no deadline”, when the
target has none.
Probes cannot be turned off on a running pod. A pod update may change only
containers[*].image, initContainers[*].image, activeDeadlineSeconds,
tolerations and terminationGracePeriodSeconds; unlike resources — the
asymmetry that makes --resize possible — probes have no resize-style
subresource. So live attach on a probed pod is a short-visit tool: break,
look, continue. Logpoints and conditional breakpoints never stop the process.
For an unlimited pause use podbench dev, which strips
all three probes by construction.
Everything here is driven by podbench dbg, run from a terminal in the seat.
podbench dbg is not gdb -p: it fixes seven commands in one order, and the
order is a correctness property rather than a preference. Setting the sysroot
after attaching gives you a backtrace that looks entirely believable and is
wrong.
1. A distroless target#
An initContainer compiles the program into an emptyDir; the distroless
container runs it. No local build, no registry.
$ kubectl create namespace podbench-gdb
$ kubectl -n podbench-gdb apply -f - <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata:
name: victim-src
data:
victim.c: |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <math.h>
struct work { int id; double value; char label[32]; };
static double transform(struct work *w) {
double r = sqrt((double)w->id) * w->value;
return r;
}
static int compute(int n, struct work *w) {
w->id = n;
w->value = (double)n * 1.5;
snprintf(w->label, sizeof(w->label), "item-%d", n);
double t = transform(w);
return (int)t;
}
static void outer_loop(void) {
struct work w; int i = 0;
for (;;) {
memset(&w, 0, sizeof(w));
int v = compute(i, &w);
printf("tick %d -> %d (%s)\n", i, v, w.label); fflush(stdout);
i++; sleep(2);
}
}
int main(int argc, char **argv) {
printf("victim starting, pid=%d\n", (int)getpid()); fflush(stdout);
outer_loop(); return 0;
}
---
apiVersion: v1
kind: Pod
metadata:
name: victim
labels: {app: victim}
spec:
restartPolicy: Never
volumes:
- {name: app, emptyDir: {}}
- {name: src, configMap: {name: victim-src}}
initContainers:
- name: build
image: debian:bookworm-slim
command: ["/bin/sh", "-c"]
args:
- |
set -ex
apt-get update -qq
apt-get install -y -qq --no-install-recommends gcc libc6-dev
mkdir -p /app/src
cp /src/victim.c /app/src/victim.c
cd /app/src && gcc -g -O0 -o /app/victim victim.c -lm
volumeMounts:
- {name: app, mountPath: /app}
- {name: src, mountPath: /src}
containers:
- name: victim
image: gcr.io/distroless/cc-debian12
command: ["/app/victim"]
volumeMounts: [{name: app, mountPath: /app}]
YAML
$ kubectl -n podbench-gdb wait --for=condition=Ready pod/victim --timeout=300s
Confirm there is genuinely no shell in there:
$ kubectl -n podbench-gdb exec victim -c victim -- /bin/sh -c 'echo hi'
error: Internal error occurred: error executing command in container:
failed to exec in container: ... exec: "/bin/sh": stat /bin/sh: no such file or directory
Note the source is left at /app/src/victim.c inside the target’s rootfs.
That is not incidental — see Where source text actually comes from below.
2. Attach the seat#
$ podbench attach pod/victim -n podbench-gdb
Check the report says [x] live attach. If it does not, skip to
Without SYS_PTRACE below — you still get source-level
debugging, just not of the already-running process.
Then ssh in with the alias it printed.
3. Find the process#
$ ssh podbench-podbench-gdb-victim-1
root@victim:~# podbench pids
container victim: the processes in its PID namespace
PID UID TARGET ST THR PTRACE CONTAINER COMM CMDLINE
1 0 yes S 3 ok 87d20e23a1b4 victim /app/victim
38 0 - S 1 ok 7206c89bf0e1 podbench podbench agent
podbench pids is not ps. Under a shared PID namespace every process in the
pod is visible — including other podbench sessions’ — so attribution keys
off the target’s container runtime ID, which the launcher injected as
PODBENCH_TARGET_CID. The rules that look obvious are all wrong: “the target is
PID 1” breaks under shareProcessNamespace: true (PID 1 is /pause), and
matching mount namespaces breaks there too.
If the TARGET column is a guess rather than a fact, podbench pids says so.
ST, THR and PTRACE are the columns to read when you disagree with the pid
dbg or debug-config chose, because they are what it chose on. ST is the
kernel’s process state: a Z there is a zombie, which no seat can attach to
however good its credentials — it is the entry you should never pass as
podbench dbg <pid>. PTRACE says whether this seat may read that process at all; a DENIED
beside a live process is usually a uid the seat does not share (see
Attach to a pod for the rung that fixes it). THR is the
thread count, which is what separates a workload from the short-lived helper it
forks.
4. Attach gdb#
root@victim:~# podbench dbg 1
With no argument at all, podbench dbg discovers the pid from the target
container ID. Before starting gdb it runs the capability probe, so if attach is
going to be denied you are told which mechanism denies it rather than being
handed an EPERM.
What it feeds gdb, in this order — see it without starting gdb using
podbench dbg --dry-run 1:
set pagination off
handle SIGURG nostop noprint pass
set sysroot /proc/1/root
directory /proc/1/root
add-auto-load-safe-path /proc/1/root
set debuginfod enabled on
file /proc/1/root/app/victim
attach 1
Every line earns its place:
Command |
Why |
|---|---|
|
This pins gdb’s own default rather than repairing anything: the image’s own gdb 13.1, with no startup commands and no attach, already reports |
|
gdb resolves the target’s loader and shared libraries, not the debug image’s. gdb 13’s default sysroot is |
|
sysroot does not cover source lookup. This is what turns |
|
setting a sysroot makes gdb decline to auto-load the target’s |
|
symbols for stripped binaries and system libraries. Symbols only — see below. A library’s are fetched after the attach, with the target stopped, so the image bounds each fetch at |
|
this is what recovers the user frames. A trailing |
|
Rust targets only. A Rust binary asks for |
When that file line names /tmp instead#
gdb canonicalises the exec file’s name before BFD opens it, and the kernel
resolves /proc/<pid>/root to /. The sysroot prefix is therefore erased for
this one command, and gdb reads whatever this seat keeps at the target’s own
path. Where the two files differ you get
BFD: /python/…/python3.11: .gnu.version_r invalid entry
warning: Can't read symbols from /python/…/python3.11: bad value
Error reading attached process's symbol file.
— a complaint that names the target’s path while describing the seat’s file —
and where they differ only slightly, no complaint at all and symbols from the
wrong build. It is not rare: podbench’s image and any uv-managed workload both
install an interpreter at /python/cpython-<version>-<triple>/, so a Python
target collides by construction (issue #90).
So podbench dbg checks whether this container has a file at the target’s exe
path, and if it does, copies the target’s binary to /tmp/podbench-exe/<pid>/
and points file there. It says so in one line, and --dry-run prints the
command it will actually run. Nothing else in the sequence changes — shared
libraries are resolved by a different path in gdb and stay sysrooted.
podbench dbg <pid> --print-exec-file prints just that path. The image’s
gdb-podbench wrapper asks for more than the path — podbench dbg <pid> --print-startup-commands prints every line above except the attach, and the
wrapper passes each as -iex before a third-party gdb --pid <n> attaches, so
gdb -p, cpptools and debugpy all get the sysroot, the staged exec file, the
auto-load path and the SIGURG handling that podbench dbg gets.
The staged copy is gdb’s fix, and lldb has no equivalent#
lldb has the same bug and cannot be given the same cure, which was measured rather than assumed. gdb canonicalises the path you hand it, so a path nothing shadows is enough. lldb ignores the path you hand it: once it has attached it re-resolves the executable from the process and overrides the target with the name as resolved in the seat’s own mount namespace —
warning: Executable binary changed from "/tmp/podbench-exe/1/victim" to "/app/victim".
Executable binary set to "/app/victim".
— so the staged copy is loaded and then dropped. settings set target.exec-search-paths does not help either; moving the seat’s own file aside
does, which is the same mechanism as gdb’s under a remedy nobody has in a pod.
It is at least loud, unlike gdb’s second failure mode, but the warning lands
in the debug console and the session carries the wrong symbols regardless.
So debug-config emits no lldb entry at all where this seat has a file at
the target’s exe path, and says which file refused it. The gdb entry beside it
is unaffected — that one gets the staged copy — and so is podbench dbg.
Measured 2026-08-19 with a standalone lldb 21.1.8 on a test bed, against a
real mount namespace built with podman - not inside a podbench seat. CodeLLDB’s own bundled lldb, running in a remote extension
host, has not been observed doing this; the refusal assumes it behaves as the
lldb it ships. Untried, and the shape that would actually be correct:
platform select remote-linux against an lldb-server started inside the
target’s namespace, which never resolves anything in the seat’s.
5. Breakpoint, source, step#
victim declares no probes, so this pause is unlimited and you can take as long
over it as you like — podbench attach said as much under supports. On a pod
that does carry probes, read the timer warning at the top of this page before
you break anywhere.
(gdb) break compute
Breakpoint 1 at 0x575fbad351f4: file victim.c, line 19.
(gdb) continue
Continuing.
Breakpoint 1, compute (n=23, w=0x7ffc633f7600) at victim.c:19
19 w->id = n;
(gdb) bt
#0 compute (n=23, w=0x7ffc633f7600) at victim.c:19
#1 0x0000575fbad35297 in outer_loop () at victim.c:31
#2 0x0000575fbad3531b in main (argc=1, argv=0x7ffc633f7778) at victim.c:42
(gdb) next
20 w->value = (double)n * 1.5;
(gdb) print *w
$1 = {id = 23, value = 0, label = '\000' <repeats 31 times>}
(gdb) info source
Current source file is victim.c
Compilation directory is /app/src
Located in /proc/1/root/app/src/victim.c
That last block is the check that matters. Located in /proc/1/root/app/src/victim.c is a single, clean sysroot prefix — the field a
DAP client hands to the editor.
Detach and leave the workload running:
(gdb) detach
(gdb) quit
Where source text actually comes from#
Be clear-eyed about this: debuginfod delivers symbols, not sources, on Debian and Ubuntu targets. Both halves of that were measured.
Symbols work, and they are cheap: a fully symbolised, source-line-annotated backtrace across coreutils and glibc for a stripped binary cost 4.7 MB of
~/.cache/debuginfod_client, and it works through the sysroot including for distroless libraries.Sources fail, twice over, and both causes are fatal on their own:
Download failed: Invalid argument. Continuing without source file ./src/sleep.c. 142 src/sleep.c: Inappropriate ioctl for device.
Debian’s
-dbgsympackages carryDW_AT_comp_dir : .from reproducible-builds normalisation, and the debuginfod protocol requires an absolute path, so gdb’s./src/sleep.cis rejected client-side withEINVAL. And the server has no sources anyway:/buildid/<id>/debuginforeturns 200,/buildid/<id>/source/src/sleep.creturns 404.
Fedora/RHEL debuginfod is known to serve sources. Debian and Ubuntu targets will not. So source text has to come from one of these, and you have to pick one deliberately:
Source in the target image — what the demo above does. Ship
/app/srcin the workload image (or on a volume) anddirectory /proc/<pid>/rootfinds it with no path mapping at all. Simplest, and the only option that needs nothing on the client.A checkout in the debug container. Clone the source into the seat and point
podbench dbgat it:root@victim:~# git clone https://github.com/you/app /workspace/src root@victim:~# podbench dbg --source-dir /workspace/src 1
--source-diris repeatable and is wired with gdb’sdirectory. gdb searches the most recently added directory first, so your checkout wins over the target’s rootfs.Client-side mapping to a checkout on your laptop, via
sourceFileMapinlaunch.json— see below. This is the only one that keeps a full clone off the pod, and it is also the least proven.
None of these is a finished design. Source provisioning for Observe mode is an open problem, not a solved one.
VS Code attach templates#
These go in .vscode/launch.json in the remote window — the debug adapter
runs inside the debug container, next to gdb, so every path below is a path in
that container.
Let podbench write it#
Do not hand-copy the templates below unless you have to. In the seat:
root@victim:~# podbench debug-config
debug-config: native target, observe mode, x86_64
debug-config: emitting gdb: native target, observe mode
debug-config: emitting lldb: native target; CodeLLDB brings its own lldb to the seat
debug-config written to /root/.vscode/launch.json
Run and Debug -> "podbench: attach to victim [pid 1 victim] (gdb)"
Run and Debug -> "podbench: attach to victim [pid 1 victim] (lldb)"
The pid and the kernel’s name for the process are in every entry name because
up to five candidates are offered at once, and an entrypoint script’s children
are routinely three processes all called python. Anything the ranking left
out is named in a debug-config: line, and podbench pids lists the rest.
It fills in the pid, the sysroot-prefixed program, the setup ordering, the
architecture, miDebuggerPath and the mode’s path mappings from what it can
already see, which is the whole point: every one of those fails silently when
wrong.
Every flavour that applies is emitted, named for its debugger, because
launch.json holds a list and VS Code’s dropdown is a better chooser than a
guess. A flavour that cannot be emitted gets a sentence naming the mechanism
instead — --flavour gdb|lldb|delve|debugpy asks for one by name and makes it
say why if it cannot. --print-config emits instead of writing, --output puts
it beside the folder you actually opened, and re-running replaces its own
entries and leaves any hand-written configuration alone.
For a Python target the answer is debugpy rather than gdb, and it depends on
the architecture as well as the language — the
CLI reference has the three axes and what each one
changes. A stock Python image has no debugpy for the injection to bootstrap
with, and debug-config then prints the uv pip install that puts one inside
the target; --provision runs it, at the cost of ~15 MB of the pod’s ephemeral
storage, egress from the pod, and nothing that survives a restart.
VS Code reads .vscode/launch.json from the folder that is open, not from
$HOME. A config written to ~ when you opened / never appears in the Run
and Debug list, and nothing reports an error.
C/C++ with ms-vscode.cpptools (gdb)#
{
"version": "0.2.0",
"configurations": [
{
"name": "podbench: attach to the workload",
"type": "cppdbg",
"request": "attach",
"processId": "1",
"program": "/proc/1/root/app/victim",
"cwd": "/root",
"MIMode": "gdb",
"miDebuggerPath": "/usr/local/bin/gdb-podbench",
"targetArchitecture": "arm64",
"setupCommands": [
{ "text": "set sysroot /proc/1/root" },
{ "text": "directory /proc/1/root" },
{ "text": "add-auto-load-safe-path /proc/1/root" },
{ "text": "set debuginfod enabled on" }
],
"sourceFileMap": {
"/app/src": "/proc/1/root/app/src"
}
}
]
}
Four things are load-bearing.
program must be the sysroot-prefixed path, or gdb reads the debug image’s
idea of the binary. setupCommands run before the attach, which is the ordering
the CLI sequence depends on — do not move the sysroot line into a post-attach
hook.
miDebuggerPath must be /usr/local/bin/gdb-podbench, the image’s wrapper,
and never /usr/bin/gdb. cpptools launches gdb as a child, so gdb inherits
cpptools’ own working directory — its extension directory, which VS Code
replaces wholesale on extension update. gdb links libpython;
-enable-pretty-printing (which cpptools always sends) initialises CPython;
CPython calls getcwd(); and gdb dies during startup:
gdb: warning: error finding working directory: No such file or directory
Fatal signal:
A fatal error internal to GDB has been detected, further
debugging is not possible. GDB will now terminate.
No signal name, no backtrace — it crashes before it can format either — and
VS Code surfaces only ERROR: Unable to start debugging. GDB exited unexpectedly, which points at the attach rather than at startup. podbench dbg
never hits this because it never enables pretty-printing, so the CLI works
perfectly on a seat where the VS Code debugger cannot start at all. Reproduce it
in any seat with:
mkdir -p /tmp/gone && cd /tmp/gone && rmdir /tmp/gone
printf -- "-enable-pretty-printing\n-gdb-exit\n" | gdb --interpreter=mi
cwd must be set, and must be a directory this seat can enter. On a
developer’s machine ${workspaceFolder} always exists so nobody sets it; in a
seat it can resolve to nothing, and the result is that same unformattable crash.
debug-config fills it in from the seat’s own $HOME: /root above, because
that example is a root seat, but /tmp/podbench-home on a seat pinned to the
target’s uid — /root is mode 0700 there and belongs to an account the image
has no record of, so naming it emitted a cwd the debugger could not chdir into.
Where the seat has no $HOME at all it is /tmp, which is 1777 in every image.
Measured inside a seat with test -d and test -x; no VS Code client has
driven the adapter.
Note
targetArchitecture is worth setting on arm64 — without it cpptools logs
Debuggee TargetArchitecture not detected, assuming x86_64 — but it is not
what causes the crash above. It is a plausible-looking red herring that sits
directly next to the real symptom in the log.
Warning
The VS Code C++ extension consumes info source’s fullname, which is exactly
the field the wrong source-mapping approach corrupts, so if paths come out
doubled (/proc/1/root/proc/1/root/…) that is the failure to look for. Map the
compilation directory, never /.
sourceFileMap keys are the compilation directories recorded in the DWARF
(info source prints them as Compilation directory); values are where those
directories can be read from in the debug container.
Rust with vadimcn.vscode-lldb (CodeLLDB)#
{
"version": "0.2.0",
"configurations": [
{
"name": "podbench: attach to the Rust workload",
"type": "lldb",
"request": "attach",
"pid": "1",
"program": "/proc/1/root/app/myapp",
"sourceMap": {
"/app/src": "/proc/1/root/app/src",
"/rustc/<commit-hash>": "/workspace/rust-src/library"
},
"initCommands": [
"settings set target.exec-search-paths /proc/1/root/usr/lib /proc/1/root/lib"
]
}
]
}
CodeLLDB spells the key sourceMap, not sourceFileMap. lldb has no exact
analogue of gdb’s set sysroot for /proc/<pid>/root, so the executable path
must be sysroot-prefixed explicitly and the library search paths set by hand.
The /rustc/<commit-hash> key is what rustc bakes into standard-library debug
info; rustup component add rust-src provides the right-hand side.
That program holds only while this container has no file of its own at
/app/myapp. Where it does, lldb discards the path after attaching and reads
the seat’s copy — “The staged copy is gdb’s fix, and lldb has no equivalent”,
under section 4 above, has the measurement and the warning it prints.
debug-config withdraws the entry in that case rather than writing one, so a
template copied by hand is the only way to get it back, and it will be wrong.
The wrong-sysroot failure, verbatim#
This is the most dangerous failure in this whole workflow, because it does not
look like a failure. set sysroot / against a target whose glibc differs from
the debug image’s (here Ubuntu 24.04’s 2.39 against the image’s 2.36):
### WRONG: set sysroot /
0x000077f6377dcb7a in wcsxfrm_l () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x00005c459789e35f in outer_loop () at victim.c:29 <- wrong line, too
#5 0x00005c45978a0d80 in __frame_dummy_init_array_entry ()
### RIGHT: set sysroot /proc/1/root
0x000077f6377dcb7a in clock_nanosleep () from /proc/1/root/lib/x86_64-linux-gnu/libc.so.6
#3 0x00005c459789e35c in outer_loop () at victim.c:35
clock_nanosleep reported as wcsxfrm_l, interleaved frames, and even the
user-code line number wrong. Against a Debian-12 distroless target the bug is
invisible, because both glibcs share a build ID and the backtrace comes out
correct — a matched debug image hides this rather than fixing it.
podbench dbg is what makes it impossible.
Three anti-patterns#
CAP_SYS_ADMIN. It makes gdb’s defaulttarget:sysroot work with zero configuration, which is exactly why someone rediscovers it every year. It also breakslibthread_db(Expected absolute pathname for libpthread in the inferior, but got target:/lib/…), is container-escape-adjacent and is rejected by any restricted Pod Security Standard. podbench never asks for it.set substitute-path / /proc/<pid>/root/. It functions, but gdb re-applies the substitution on display and emits/proc/1/root/proc/1/root/proc/1/root/…— which is the string a DAP client hands to your editor. Usedirectory.set sysrootafterattach. Libraries get fixed up on the fly, so it looks like it worked; the main executable does not, and the frames above libc come back as?? ().
Without SYS_PTRACE#
Losing the capability costs exactly one thing: attach to an already running process. Full source-level debugging survives, because gdb starting the program itself needs no capability and is exempt from Yama:
root@victim:/workspace# podbench dbg --launch ./myprog --some-flag
--launch consumes the rest of the command line, so put any other
podbench dbg flags before it. Add --run to start the program immediately.
podbench dbg will tell you this itself when attach is denied — it names the
mechanism and points at the alternative:
podbench dbg: cannot attach to pid 1: yama-scope
Yama's ptrace_scope forbids attaching to a non-descendant...
verdict: read-only inspection of the target; no live attach
the target's rootfs, maps and environ are still readable, so `podbench pids`
and read-only inspection work.
ptrace-free alternative: `podbench dbg --launch ./yourprog [args]`. gdb forks the
inferior itself, which needs no capability and is not subject to Yama below
ptrace_scope=2.
to keep attaching to this process, the target can opt in with one line:
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY).
That last line is worth knowing: the natural workflow myprog & ; gdb -p $!
makes gdb a sibling, which Yama denies at ptrace_scope=1. A sibling that
called prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY) is attachable with zero
capabilities. It is a one-line, capability-free, node-change-free opt-in for a
program you control.
On a pod where the ptrace-gated reads went with the attach — the launch-only verdict — the consolation line says that instead, because offering a sysroot that will not open costs a second afternoon:
verdict: launch-only: `podbench dbg --launch` works; no read-only inspection
the reads that take PTRACE_MODE_READ went with it (cmdline, status and fd
only; root, maps and environ denied), so a sysroot, `environ` or `maps` is not
the fallback here.
The matrix is in the line because it is the honest form of the answer: cmdline,
status and fd are still readable — they always are — so podbench pids works
and “the target is closed” would be the same overclaim as issue #51, pointing
the other way.
--launch survives all of that: gdb forks the inferior and traces its own
descendant, which needs no capability and no Yama exemption. It is still
measured rather than assumed, and the tick beside debug launched processes
is the measurement — podbench capreport attaches to a child it forked itself
before it claims the rung. Three things take it away with everything else, none
of which cares whose descendant the inferior is: a seccomp filter that rejects
ptrace(2) outright, ptrace_scope 2 or 3 — scope 2 is the one Yama setting
with no descendant exemption, and it demands CAP_SYS_PTRACE of
PTRACE_TRACEME too — and an LSM policy that denies ptrace — differing
AppArmor profiles, or differing SELinux MCS categories, which capreport
reports as lsm-mismatch. podbench capreport names whichever of the three it
finds.
The offer is conditional for that reason: where the scratch attach was measured
and failed, podbench dbg says so instead of pointing at --launch, because a
second identical denial is what costs the afternoon.
Gotchas#
ASLR cannot be disabled.
RuntimeDefaultseccomp permitsptrace(2)but blockspersonality(ADDR_NO_RANDOMIZE), so gdb warnsError disabling address space randomizationand addresses vary run to run. Set breakpoints by symbol.Attach works on one node and not the next. Yama’s
ptrace_scopeis a node-level knob and differs by kernel flavour, not by architecture — two arm64 nodes in the same cluster disagreed in testing. podbench probes per node and prints the node name for this reason.A target already being traced refuses with the same
EPERMas a policy denial, because a tracee has exactly one tracer.podbench capreportreportsalready-tracedwhen it can tell.Multithreaded targets are unproven.
libthread_dbloaded andinfo threadslisted the single LWP in testing; a genuinely multithreaded target was never tried. Nor was a target in a user namespace.elfutils/binutilsare in the image —readelf --debug-dump=infoandeu-readelfare how a build-ID miss gets diagnosed.
Clean up#
$ kubectl delete namespace podbench-gdb