
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Quick answer. A
kworkerpegging a core is usually a real kernel bug, most often an ACPI interrupt storm, and you can check that in one read:cat /sys/firmware/acpi/interrupts/*and look for a counter in the millions. What rules that reading out is network traffic, because a genuine kernel thread does not own an outbound TCP connection. If the peggedkworkeris moving bytes to a mining pool, it is a userspace process that renamed itself, andyeet run gh:yeet-src/poolnarc -- --auditscans for 60 seconds and printsCRITICALwith the real pid and the pool address beside it before exiting. Nothing installs on the host beyond yeet, and nosudo.
I build kernel-side tools for Linux hosts and watch engineers point them at boxes they did not set up. That is a narrower credential than it sounds: I have never been the person holding your pager, and I am not going to tell you whether to isolate this host or hand it to your security team. What I do see, over and over, is the shape of the moment before that decision. Somebody is on a box with a pegged core and a process name they cannot verify, they know a restart both fixes the symptom and destroys the evidence, and every check they can find tells them to read a field that the thing they are hunting can rewrite for free. That gap, between a name a process reports about itself and a fact the kernel can vouch for, is the whole subject here.
Six places to stand, and the column that separates them is whether the check reads something the suspect process can edit. That distinction decides the whole question, because the malware in this scenario is defined by having edited its own name.
| Route | What you install first | Reads a field the process can edit | Gives a verdict or evidence | Sees the destination | Scope |
|---|---|---|---|---|---|
poolnarc (yeet + eBPF) | curl -fsSL https://yeet.cx | sh, nothing on the host after | reads comm, in the kernel at send time, deliberately | verdict: CRITICAL / MINING / SUSPICIOUS / CLEAN | yes, classified by destination port | one host, one scan window |
ps and the square-brackets check | already there | yes, this is the failure | neither, just a name | no | one host, right now |
ss -tnp | usually preinstalled | shows the socket, names the process from /proc | evidence, no classification | yes, address and port, if the socket is open when you look | one host, current sockets |
tcpdump | usually a package install | no, reads the wire | bytes on the wire, you interpret them | yes | one interface, while you watch |
| Wazuh | agent, custom rules, FIM, Suricata, configured before the incident | partly, its CPU check shells out to top | alerts, with history | yes, via Suricata deep packet inspection | fleet, continuous |
| CrowdStrike Falcon | sensor package, CID, service started, before the incident | no, kernel-side sensor | detections, with retention and response | yes | fleet, continuous |
Two things fall out of that table before any route is worth explaining. The first is that the two checks a person reaches for at 2am are the two that read the process's own account of itself, and one of them is the check every existing answer to this question recommends. The second is that the routes with the strongest answers, the platforms, are the routes that had to be installed before the thing happened, which is exactly the condition that is not met on the box in front of you.
poolnarc is an eBPF cryptomining scanner for Linux: it watches outbound TCP in the kernel and names the process talking to a mining pool, even when that process has renamed itself to look like a kernel thread. It attaches, collects for a window, prints a report, and exits, so you get your prompt back and there is no service to remember to remove afterward.
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/poolnarc -- --audit # 60-second scan, prints a verdict
yeet run gh:yeet-src/poolnarc -- --audit --duration 90 # longer window on a quiet host
--audit is the mode that matters here: it writes scrolling stdout rather than repainting, so it survives a pipe, a redirect, and an SSH session with no TTY. Script flags go after the -- so the runtime routes them to the script rather than consuming them itself, which is the most common first-run mistake. --duration takes seconds and is capped at 3600, and a non-numeric or negative value silently falls back to 60, so check your quoting. yeet run never takes sudo; the yeet daemon holds CAP_BPF and CAP_PERFMON and performs the privileged load.
What it gives you that the other five routes do not is a classification of the destination combined with a judgment about the name, resolved into one line you can act on. What it does not do is stop anything. It observes and reports; it cannot quarantine, kill, or block, so the response after a CRITICAL is yours.
Every widely circulated answer to "is this kworker real" recommends comparing whether the process appears in square brackets, and the check is unsound for a reason stated in ps's own man page. The ps man page says of the args output: "Sometimes the process arguments will be unavailable; when this happens, ps instead reports the executable name in brackets." Brackets are not a kernel attestation that a process is a kernel thread. They are what ps prints when it could not read /proc/pid/cmdline, which is a condition a userspace process can produce on purpose.
That is not a theoretical gap. The SANS Internet Storm Center diary Linux Process Name Masquerading (Xavier Mertens, 2026-06-24) publishes working proof-of-concept code for a process that presents as [kworker/0:1-events], brackets included, by writing into the contiguous argv and environ memory region rather than merely calling prctl. If the attacker bothers, the brackets appear. The check that every listicle recommends is a check the technique it is supposed to catch already defeats.
ps -eo pid,comm,args | grep kworker # the incumbent check
ss -tnp | grep -i kworker # what it cannot tell you
Run both and the second is the one that carries information, because it asks a question about the socket rather than about the name.
ss -tnp is the fastest way to see a contradiction, and it is worth running first because it costs nothing and it sometimes ends the investigation immediately. It lists established sockets with the owning process, so a kworker appearing next to a foreign address is the contradiction that starts this whole line of reasoning.
Its two limits are structural rather than fixable. It shows you sockets that are established at the instant you look, so a miner that opens and closes between your polls is invisible, and it resolves the process name from /proc, which is the same editable field ps reads. It also has no idea which of those foreign addresses are mining pools. You get 65.21.198.20:14444 and no opinion about it, which means the classification step is yours and depends on knowing that 14444 is a Monero pool convention.
Reach for a packet capture when you need what actually crossed the wire, not a summary of it. Stratum is line-delimited JSON-RPC over plain TCP, so a capture on a pool port shows you readable mining.subscribe and mining.authorize frames rather than an opaque blob, which makes it the strongest artifact to attach to a ticket. It is also the route that reads nothing the process controls.
What it will not do is tell you which process. A capture is per-interface, not per-pid, so it confirms that mining traffic exists on the box and leaves the attribution to you. For packet-level decoding inside the yeet corpus, pktscope is the sibling that does this; for plaintext inside TLS, which needs an SSL_write uprobe, that is wssnoop's territory.
Wazuh is the top-ranked answer to the general question and it is genuinely stronger than anything here for the case it is built for, which is continuous coverage with history you can query afterward. Its own Linux cryptominer detection guide sets out a layered approach across the compromise: SSH brute-force monitoring for initial access, file integrity monitoring on /home/*/.ssh/ and cron directories for persistence, VirusTotal hash lookups for the binary, a periodic CPU check that shells out to top -bn1 | grep 'Cpu(s)', and Suricata deep packet inspection for pool connections.
Read that list as a prerequisite list and the boundary becomes obvious. It requires an agent deployed, custom rules added to /var/ossec/etc/rules/local_rules.xml, FIM directories configured, a VirusTotal integration in ossec.conf, and Suricata fed traffic. Every one of those is a thing that had to be true before the incident. If it was, you are not reading this post. Worth noting that its resource-exploitation layer reads CPU by shelling out to top, which is the same userspace vantage point the brackets check occupies.
CrowdStrike is the answer when you need detection plus the ability to do something about it, which is the half poolnarc structurally does not have. Falcon's Linux sensor runs kernel-side rather than reading /proc, and it carries retention, alert routing, and response actions.
Its install path is the boundary. CrowdStrike's own installation guide is three steps: sudo yum install <installer_filename>, then sudo /opt/CrowdStrike/falconctl -s --cid=<CID> to set the customer ID, then start the service. The same shape holds for a CWPP: Microsoft's Defender for Servers planning guide walks through onboarding machines as Azure Arc VMs, enabling the plan, attaching a Log Analytics workspace, and installing the Azure Policy machine configuration extension before file integrity monitoring can be set up. These are the right tools and they are the wrong shape for the next nine minutes, because installing a sensor on a host you suspect is compromised is not a diagnostic step, it is a change to the evidence.
Usually a firmware interrupt storm, and usually not malware. A kworker is a kernel worker thread that runs deferred work, so when one pegs a core the ordinary explanation is that something is queueing work at an absurd rate, most often an interrupt that never gets acknowledged. Red Hat's bug 1192856, titled "ACPI Interrupt storm causes high kworker CPU usage", records kworker CPU "between 70 and 90%" traced to an interrupt storm on ACPI interrupt GPE06, with 4,347,271 calls counted after a reboot before any application was launched.
That is checkable in one read, and it is the first thing to do because it is cheap and it resolves most cases:
cat /sys/firmware/acpi/interrupts/* # per-GPE counters
grep . /sys/firmware/acpi/interrupts/gpe* | sort -t: -k2 -n | tail
A counter in the millions on a freshly booted box, climbing while you watch, is a firmware interrupt storm and not a compromise. Other benign causes cluster the same way: an Arch BBS thread tracks two permanently pegged kworkers to USB runtime power management, with perf showing both stuck in pm_runtime_work cycling through usb_runtime_suspend and hub_suspend. Kernel work, real kernel thread, genuine bug.
The reason to establish that first is that it is what makes the other reading credible. If your instinct on seeing a pegged kworker is "compromise", you will be wrong most of the time, and being wrong most of the time is how a real detection gets ignored. The question that separates the two readings is not how much CPU it is using. It is whether it is moving network bytes.
No, and that is what makes the contradiction diagnostic rather than confusing. A kernel thread has no user address space and no file descriptor table of its own in the sense a process does; it does not connect(), it does not own a socket, and it does not appear in application context on the TCP send path holding one. So if ss -tnp shows a kworker next to an established connection, the two facts cannot both be true. Either the thing is a kernel thread and the socket is misattributed, or the socket attribution is right and the name is a lie.
Deciding which requires knowing where the name was read, and that is the part every existing answer skips. Three fields are in play and they are not equally trustworthy:
| Field | Who writes it | Cost to the attacker | What it proves |
|---|---|---|---|
/proc/pid/comm | the process itself, via prctl(PR_SET_NAME) | one syscall, no privileges named in the man page | nothing about identity, on its own |
/proc/pid/cmdline | the process, by overwriting the argv region | more work, but published proof-of-concept code exists | nothing, once overwritten |
/proc/pid/exe | the kernel, pointing at the on-disk binary | cannot rewrite it, can delete the file | the real executable, or a deleted marker |
| the socket's owning task at send time | the kernel, at tcp_sendmsg | not writable | which task actually moved the bytes |
The PR_SET_NAME man page is explicit about the first row: it sets the name of the calling thread, "The name can be up to 16 bytes long, including the terminating null byte", and it names no privilege requirement. haxrob's write-up on process name stomping confirms what stays put, which is that /proc/pid/cmdline is unaltered unless argv[0] is separately overwritten and /proc/pid/exe "continues pointing to the original executable binary on disk". That is why the defensible version of this argument is narrow: the process name is not worthless, and matching on it is a perfectly good way to identify a process you launched yourself. What it cannot do is establish identity for a process you did not launch, and the fix is not distrust in general, it is reading the name somewhere the liar does not control the timing.
No, and this is the check most answers to the kworker question recommend. The brackets in ps output are not a kernel flag meaning "this is a kernel thread". They are what ps falls back to printing when it cannot read the process's arguments, per the man page wording quoted above. A process that arranges for its arguments to be unreadable, or that writes brackets into its own argv region, gets the brackets.
The SANS proof-of-concept presents as [kworker/0:1-events] precisely to make this point, and the cost to the attacker is two lines. The short version of the attack is a prctl call for comm plus a memory write over the argv and environ region for cmdline, and after that every tool that reads /proc for a name agrees with the attacker. Red Canary's write-up on the Rocke cryptominer shows the shape of the detection people fall back on instead: "Linux systems will execute processes named kworker all the time, but the processes will not use a binary in a /tmp folder." That is a real and useful heuristic, and note what it does. It gives up on the name entirely and correlates against something else, in that case the file path.
The correlation poolnarc uses instead is the network, and the reason to prefer it here is that a cryptominer cannot opt out of it. It needs work units from a pool and has to return shares, so it holds a connection open and moves bytes on it for as long as it is earning. The name is negotiable and the socket is not.
From two independent places, and a process can rewrite both. comm is a 16-byte field on the kernel task struct; cmdline is a region of the process's own memory holding argv. Neither is privileged to change, which is why the useful question is not whether a name can be trusted but where it is read. poolnarc never asks the process for its name: it reads comm from the task executing on the TCP send path, inside the kernel, at the moment bytes move.
That timing is the entire mechanism, and getting it wrong is what makes naive versions of this detector useless. The tempting place to attribute a connection is at the point it becomes established, which in the kernel is inet_sock_set_state. That hook fires in softirq context, servicing an interrupt, so the task currently on the CPU frequently has nothing to do with the socket. Calling bpf_get_current_comm() there returns swapper or a kworker routinely, and a detector that trusted that reading would flag every busy host as full of kernel-thread miners.
So poolnarc treats attribution at established as provisional. It records the connection with its pid-is-real flag unset, emits no open event at all, and waits for tcp_sendmsg or tcp_cleanup_rbuf, which run in application context on the thread that actually owns the socket. The first time one of those fires with a non-kernel comm, the pid and name are overwritten and the attribution locks.
Here is the inversion, and it is the reason this is a detector rather than a port-matching script. A connection whose comm is still kworker after it has passed through tcp_sendmsg in application context is not a misattribution, because a real kernel thread never reaches that path owning a socket. The same wait that removes every softirq false positive is what turns a surviving kworker into a true positive. One mechanism, both jobs, and the second one is free.
The hooks are fentry/tcp_sendmsg, fentry/tcp_cleanup_rbuf and tp_btf/inet_sock_set_state. The reason for fentry over a kprobe here is portability rather than speed: it attaches through BTF and reads arguments as typed values instead of unpacking a pt_regs by architecture calling convention, so one source file works on x86_64 and aarch64 with no register-name branch. The tradeoff is a hard floor. Both fentry and tp_btf arrived in kernel 5.5 and need kernel BTF, so on anything older this does not load at all rather than falling back. Confirm /sys/kernel/btf/vmlinux exists before debugging anything else, because the BPF load fails at run time and not at build time.
Three tiers, and only the top one is close to exclusively mining (extrapolated, grounding 2 — review). Mining pools publish their ports and clients ship those defaults, so an attacker who wants their malware to work against public pools has to use the pool's port, and that is what makes a port-based classification work at all.
| Tier | Example ports | What a hit means | Effect on the verdict |
|---|---|---|---|
| High confidence | Monero 14444, Ethereum-family 12020, NiceHash 3357 and 9200 | published public-pool defaults, close to exclusively mining (extrapolated, grounding 2 — review) | can reach MINING, and CRITICAL with a spoofed name |
| Stratum-class | the repdigits 3333, 4444, 5555, 7777, plus 2020 and 8008 | commonly mining, plausible for a generic JSON-RPC service | reported as Stratum-class, never confirmed on its own |
| Crypto peer-to-peer | Bitcoin 8333, Ethereum 30303 | a node, not a miner | counted and reported, never escalates |
The repdigit convention is the softest part of this (extrapolated, grounding 3 — review): the ports are documented pool defaults, but the framing that the long tail uses them by convention is inference around those defaults rather than a published fact. The database is tiered rather than flat for exactly this reason. A destination port is evidence about the pool, not proof about the process, so port evidence alone never reaches the top verdict. What reaches CRITICAL is a mining port plus a comm matching a kernel-thread prefix, because that pair has no benign reading.
One gap in this scheme matters more than the rest, and it is the reason a clean result is not an all-clear. A private pool on a non-standard port never classifies; a pool on 443 looks like HTTPS. That is the largest hole in the approach, and closing it means allowlist-based egress filtering rather than expecting detection to cover it. Peer-to-peer mining defeats it the same way, and it is not hypothetical. Akamai's researchers documented a campaign in May 2026 against exposed Ollama endpoints that renames its main process to kworker-main to pass as a kernel worker in ps output, then starts a local mining proxy on 127.0.0.1:41947 and routes shares through a decentralized pool over libp2p, so there is no public Stratum endpoint for a port block to match on. Read that campaign as the boundary of this approach: the mimicry half still trips, because kworker-main is a kernel-thread name on a process that owns a socket, while the port half sees a connection to localhost and has nothing to classify.
No. CLEAN is a statement about the scan window, not about the host. It means nothing matching the two rules happened during those 60 seconds, which is a narrower claim than it feels like when you are looking for permission to close the incident.
The specific way it goes wrong is timing. Attribution happens when a connection is established and the first event waits for bytes to move, so a socket that was already open before the probe attached surfaces on its next 64 KiB and not before. A miner idling between work units can sit out a short window entirely. On a quiet host, lengthen it: --duration 300 costs you five minutes and buys a materially better claim than a 60-second run, and --duration accepts up to 3600.
Four other things a clean verdict cannot rule out, each for a structural reason rather than a bug:
nginx-worker on a pool port trips no mimicry rule. It still appears as mining activity and still reaches the MINING verdict, but it never escalates to CRITICAL.tcp_sendmsg and tcp_cleanup_rbuf, so UDP-based protocols and raw sockets are invisible.So a clean result is worth what a negative test is worth: it moves your prior and it does not close the question. The pairing that is genuinely load-bearing is a clean verdict alongside a benign explanation you actually confirmed, such as an ACPI counter in the millions, because then you have both an absence of evidence and a positive account of the symptom.
No, which is why a pool connection alone is reported as a weaker finding than a spoofed name. Plenty of people run miners deliberately, and a full node on Bitcoin's 8333 or Ethereum's 30303 is not mining at all. A process on a known pool port with no name spoofing lands at MINING rather than CRITICAL, and the intended reading is "confirm this is not a miner you run deliberately, then investigate".
The reason the two are separated is that the absence of a lie removes the only signal that has no innocent explanation. Someone in your organization may genuinely be running a miner, a benchmark, or a testnet node, and a host you inherited may have had that as its actual job. CRITICAL exists for the case where a process both talks to a pool and claims to be a kernel thread, because nothing legitimate has a reason to do the second thing. Between those sits SUSPICIOUS: a process whose name matches a system-daemon prefix, such as systemd or sshd or cron, on a Stratum-class port. Those ports see genuine use by JSON-RPC services unrelated to mining, and a real sshd owning a socket is ordinary, so only the destination makes it odd. Review it, check what the destination actually is, and if it is yours that is a false positive by design rather than a defect.
One reporting detail that surprises people reading a report for the first time: process names are truncated at 15 characters before poolnarc ever sees them, because comm is a 16-byte kernel field including the terminating null. Prefix matching is unaffected, which is what the prefix lists exist for, but the name printed in the report is the truncated one.
Nothing. poolnarc reads from the kernel on the host, formats locally, and writes to your terminal: there is no collector, no upload, and no signature database consulted over the network. That matters twice over on a host you suspect is compromised, because a scan that phones home is both a change to the evidence and a second thing to explain in the incident review. The separate question is what leaves the box when you paste the output somewhere, and --anonymize is for that: it aliases process names to proc-01 and addresses to host-01, consistently within a single run, so the relationships in the report survive while the identifying strings do not.
yeet run gh:yeet-src/poolnarc -- --audit --anonymize # pipe-safe, text
yeet run gh:yeet-src/poolnarc --tty -- --anonymize # dashboard, safe to screenshot
The dashboard needs a real terminal, so do not pipe or redirect it, and --tty is a flag belonging to yeet run itself, which is why it sits before the --. The consistency within a run is the part that makes the anonymized output useful rather than merely redacted: if proc-01 appears against host-01 three times, that is the same process and the same destination three times, and the pattern you are trying to communicate is intact.
One limit worth knowing before you rely on the aliasing: it is consistent within a run and not across runs, so host-01 in this morning's scan and host-01 in this afternoon's are not promised to be the same destination. For a report someone will compare against a later one, keep the unaliased version on the box and share the aliased one.
When the question is continuous rather than immediate, and specifically when you need any of four things poolnarc does not have: a rule engine, alert routing, retention you can query after the fact, or the ability to act. It cannot quarantine, kill, or block. It is one behavioral check, scoped to one host, that answers in about a minute.
Route it this way:
poolnarc tells you a process is lying and then stops; the kill, the isolate, and the snapshot are yours or your platform's.poolnarc, and this is the only column it wins. It is what you run while the platform is still deciding.ss -tnp. If you have the address, you do not need classification, you need confirmation that a socket exists.tcpdump, or pktscope for packet-level decoding in the same runtime.container-traffic, which also starts from the container view, since poolnarc reports host-namespace pids and cannot name the container an alert belongs to.These are not competitors on the same axis. A runtime-security platform is what you want running continuously; a 60-second scan is what you want when it was not. If your problem turns out not to be a miner at all and the pegged process is doing real work, finding which process is actually slowing the machine is the next question, and it splits on blocked-versus-busy rather than on identity.
Capture the scan output before you touch the process, because a restart fixes the symptom and destroys the pid, the socket and the name in one move. A CRITICAL verdict carries the pid, the truncated name the process chose, the pool address and port, bytes moved in each direction, and the connection count, which is enough to escalate and enough to justify leaving the box running. It is not enough to tell you how the thing got there, and the report makes no attempt to.
Three questions it structurally cannot answer, worth knowing before you promise your team an answer:
poolnarc cannot confirm a connection is really Stratum rather than something else on that port, and it cannot recover the wallet address.For the response itself, poolnarc is the wrong tool by design, and the boundary is a hard one rather than a soft one: it observes. If the question after the verdict is how to stop a process from doing something rather than how to know that it did, that is enforcement, and sandboxing a process so the kernel refuses what it asks for is the shape of that answer.
Start with cat /sys/firmware/acpi/interrupts/*, because a firmware interrupt storm is the likeliest explanation for a pegged kworker and it costs one read to rule in. Run ss -tnp next, because if a kworker shows up next to a foreign address you have your contradiction in five seconds. Skip the ps square-brackets check entirely: the man page says brackets mean ps could not read the arguments, and the published proof-of-concept for this exact masquerade prints brackets on purpose. Run yeet run gh:yeet-src/poolnarc -- --audit when you need the destination classified and the name read somewhere the process does not control, and lengthen --duration on a quiet box. Reach for tcpdump or pktscope when you need bytes as an artifact rather than a verdict. Deploy Wazuh, CrowdStrike, or a CWPP for the coverage you wish you had had, and accept that today they answer a question you cannot ask retroactively. And treat CLEAN as a statement about your scan window: the failure mode worth avoiding here is not missing a miner, it is closing an incident on 60 seconds of silence.
Because a supervisor restarted it and aggregation is keyed on pid plus name, so a restart shows up as a new row rather than merging silently into the old one. In an audit both rows survive the window, which is useful: two short-lived pids for one name is itself evidence that something is being restarted rather than running steadily. In the live dashboard a row disappears sixty seconds after its last activity.
It sets the name of the calling thread, which surfaces as /proc/pid/comm and the Name field in /proc/pid/status. The man page limit is 16 bytes including the terminating null, so 15 usable characters, and no privilege requirement is listed. It does not change /proc/pid/cmdline, which needs a separate overwrite of the argv region, and it cannot change /proc/pid/exe, which the kernel maintains as a link to the on-disk binary.
Look for a process holding a long-lived outbound TCP connection to a mining pool port, since a miner has to keep a Stratum session open to receive work and return shares. yeet run gh:yeet-src/poolnarc -- --audit does this in one pass: it attaches for 60 seconds, classifies outbound destinations against a tiered pool-port database, prints a verdict, and exits without leaving a daemon running. It needs no signature database and no sudo.
On its own, no. Comparing /proc/pid/comm against /proc/pid/cmdline produces false positives, because plenty of legitimate software renames its threads, and it produces false negatives against an attacker who overwrites both. The stronger comparison is /proc/pid/exe, which the kernel controls and which reveals the real binary or a deleted marker, and the strongest is correlating the name against behavior the process cannot avoid, such as the network connection it depends on.
Overwrite Process Arguments, a sub-technique of Masquerading, covering adversaries who modify a Linux process's in-memory arguments so it appears legitimate. MITRE's detection guidance correlates unexpected null byte sequences, discrepancies between /proc/pid/cmdline and process ancestry, and suspicious memory writes shortly after process start. Note that prctl(PR_SET_NAME) name changes are catalogued separately: Elastic's prebuilt rule for that behavior maps to T1036.005.
Published pool defaults cluster in recognizable places: 14444 for Monero reference pools, 2020 and 12020 for Ethereum-family pools, 3357 and 9200 for NiceHash, and a long tail on repdigit ports such as 3333, 4444, 5555 and 7777. Port evidence is about the pool rather than the process, so a hit on a merely Stratum-shaped port should be treated as a lead. Bitcoin's 8333 and Ethereum's 30303 are peer-to-peer node ports and not mining at all.
Yes, when the probes attach in the host kernel rather than inside the image, because every container on the host shares that kernel. poolnarc attaches to tcp_sendmsg and tcp_cleanup_rbuf there, so traffic from inside containers is visible with no sidecar and nothing added to the image. What comes back is the host-namespace pid rather than a container name, so mapping an alert to a specific container is a lookup you perform afterward.
Loading a BPF program needs CAP_BPF and CAP_PERFMON, but that does not have to mean running your scan as root. With yeet, the daemon holds those capabilities and performs the privileged load, so yeet run is unprivileged and never takes sudo. The kernel floor matters more in practice: fentry and tp_btf both require kernel 5.5 or newer with BTF enabled, so check that /sys/kernel/btf/vmlinux exists.
Cost tracks connection count rather than traffic volume, which is the property that makes it safe on a busy box. Per-connection state lives in a kernel hash map, and userspace hears about a connection three times: once when attribution settles, once per 64 KiB transferred, and once at close. A connection moving a gigabyte therefore produces a bounded trickle of events rather than one per packet, and an idle connection produces none.
Cryptojacking malware does not write logs, and the process name your log agent reports is the name the malware chose, so both the presence and the identity are under the attacker's control. Reading from the socket instead removes that control: the pid and the destination come from kernel state rather than from the process's own account of itself. The one field taken at face value is the name, which is exactly why a name that should be impossible is treated as a signal.
Longer than the miner's idle interval, which you do not know, so bias upward on a quiet host. A 60-second window catches a miner that is actively moving bytes, and misses one that connects on a long cycle and happens to be between work units. A five-minute scan (--duration 300) is a materially stronger negative result for the same effort, and the cap is 3600 seconds. Any clean result remains a statement about the window rather than about the host.
Yes, and it is worth knowing how before trusting a clean scan. A private pool on a non-standard port, or mining proxied through an endpoint on 443, looks like ordinary traffic to a classifier keyed on published pool ports. Peer-to-peer mining does the same by design: one documented campaign runs a local proxy on 127.0.0.1:41947 and routes traffic through a decentralized pool over libp2p. Egress allowlisting is the control that covers this, not detection.
args format that "Sometimes the process arguments will be unavailable; when this happens, ps instead reports the executable name in brackets", which establishes that square brackets in ps output are a fallback for unreadable /proc/pid/cmdline rather than a kernel attestation of kernel-thread status; this is the primary grounding for why the widely recommended brackets check cannot distinguish a real kernel thread from a process that arranged for its arguments to be unreadable./proc/pid/comm via prctl(PR_SET_NAME) and /proc/pid/cmdline by overwriting the contiguous argv and environ memory region; notes "argv[0] is a fixed-size buffer! You can't just point it somewhere else"; ships working proof-of-concept code for a process presenting as [kworker/0:1-events], brackets included, while remaining visible to eBPF-based tooling.comm value; the attribute is accessible via /proc/self/task/tid/comm; no privilege requirement is listed for the operation.PR_SET_NAME changes comm and the Name field of /proc/pid/status with a 15-character TASK_COMM_LEN limit, while /proc/pid/cmdline "remains unaltered unless argv[0] is separately overwritten" and /proc/pid/exe "continues pointing to the original executable binary on disk"; warns that "A naive detection approach is to check discrepancies between comm and cmdline, although this will result in false positives"./sys/firmware/acpi/interrupts/gpe06; establishes the benign firmware explanation for a pegged kworker and the one-read check that confirms it.kworkerds from /var/tmp and states the fallback heuristic directly: "Linux systems will execute processes named kworker all the time, but the processes will not use a binary in a /tmp folder"; illustrates that credible detections abandon the name as identity and correlate against something the attacker does not control.kworker-main to pass as a kernel worker in ps, runs XMRig against Monero with a 50% CPU cap to stay under resource alerting, starts a local mining proxy on 127.0.0.1:41947 and routes shares through a decentralized pool over libp2p rather than a public Stratum endpoint, and persists via a root crontab entry that relaunches it every 15 minutes. The libp2p routing is why blocking known Stratum ports does not cover this case./home/*/.ssh/ and cron directories, custom rules in /var/ossec/etc/rules/local_rules.xml, a VirusTotal integration in ossec.conf, Suricata for pool-connection detection, and a periodic CPU check that shells out to top -bn1 | grep 'Cpu(s)'.sudo yum install <installer_filename>, then sudo /opt/CrowdStrike/falconctl -s --cid=<CID> to set the customer ID, then starting the service with no reboot required, and notes that CrowdStrike "supports both Kernel mode and user mode" with the host's mode visible on the Host management page; establishes that the sensor must be deployed before the incident it detects.BPF_TRACE_FENTRY and BPF_TRACE_RAW_TP (the tp_btf prefix) both tagged v5.5, linked to their introducing kernel commits; describes tracing programs as "a newer alternative to kprobes and tracepoints" using BPF trampolines, "a new mechanism which provides practically zero overhead", and notes fentry programs are always attached at a function's entry point whereas kprobes may attach anywhere.MINING, connections established before the scan surface only on their next 64 KiB, non-TCP traffic is invisible, no payload is read so Stratum cannot be confirmed and no wallet recovered, alerts carry host-namespace pids with no container resolution, and comm truncation caps reported names at 15 characters.inet_sock_set_state is provisional and what locks it at tcp_sendmsg.poolnarc tells you a process is lying, and this is what it looks like when the kernel refuses instead of reporting.pktscope — packet-level decoding when you need the bytes on the wire as an artifact rather than a classification.container-traffic — per-endpoint bandwidth ranking that starts from the container view, which is the attribution poolnarc cannot give you.Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.