Skip to content

Commit c37b17a

Browse files
committed
Fix nmap bypass: comm-based tracking for pre-monitor openclaw workers
Root cause: openclaw-gateway workers created before the monitor starts are never added to tracked_pids because the fork hook only propagates tracking for already-tracked parents. Short-lived worker processes (e.g. the gateway's executor) are invisible in /proc at bootstrap time, so their children (bash, nmap, etc.) run without BPF enforcement. Fix: - bpf_hooks.c: add tracked_comms map and hash_comm_buf() helper; extend sched_process_fork to fall back to comm-name lookup when the parent is not in tracked_pids — auto-backfills the process into tracked_pids so it and all descendants are tracked immediately - monitor.py: add populate_tracked_comms() that registers djb2 hashes of openclaw worker comm strings at startup; sources both target_paths basenames and a /proc scan that catches openclaw-gateway (comm "openclaw-gatewa") and similar workers already running before load - dashboard/app.py: suppress PATH-LIN-015 self-noise from monitor's own npm subprocess; raise parse_log_lines cap to 5000; include rotated log backup in BLOCK_LOG_PATHS
1 parent dc4b13b commit c37b17a

File tree

3 files changed

+225
-53
lines changed

3 files changed

+225
-53
lines changed

deploy/dashboard/app.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def _save_dismissed(dismissed: set) -> None:
160160

161161
BLOCK_LOG_PATHS = [
162162
"/var/log/clawedr.log",
163+
"/var/log/clawedr.log.1", # rotated backup — ensures alerts aren't lost after rotation
163164
os.path.expanduser("~/Library/Logs/clawedr.log"),
164165
"/tmp/clawedr_log_tailer.log",
165166
]
@@ -231,7 +232,7 @@ def _parse_ancestry(details: str) -> list[dict] | None:
231232
return chain if chain else None
232233

233234

234-
def _parse_log_lines(max_lines: int = 1000) -> list[dict]:
235+
def _parse_log_lines(max_lines: int = 5000) -> list[dict]:
235236
"""Parse recent BLOCKED entries from the log files."""
236237
import datetime
237238
alerts: list[dict] = []
@@ -249,9 +250,17 @@ def _parse_log_lines(max_lines: int = 1000) -> list[dict]:
249250
ancestry = _parse_ancestry(raw_details)
250251
# Strip ancestry from the displayed details string
251252
clean_details = _ANCESTRY_RE.sub("", raw_details).strip()
253+
rule_id = m.group(3)
254+
# Suppress monitor self-noise: PATH-LIN-015 blocks for libuv-worker
255+
# uid=0 processes are spawned by the monitor's own npm subprocess,
256+
# not by the agent. These flood the log and are not agent actions.
257+
if (rule_id == "PATH-LIN-015"
258+
and "comm=libuv-worker" in clean_details
259+
and "uid=0" in clean_details):
260+
continue
252261
entry: dict = {
253262
"timestamp": m.group(1),
254-
"rule_id": m.group(3),
263+
"rule_id": rule_id,
255264
"details": clean_details,
256265
"blocked": kind != "ALERT",
257266
}

deploy/linux/bpf_hooks.c

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ BPF_ARRAY(egress_mode, u8, 1);
6565
/* Shell interpreters: only track when parent is tracked (agent-spawned shells)
6666
*/
6767
BPF_HASH(parent_tracked_bins, u64, u8, 16);
68+
/* Comm-name hashes of openclaw worker processes (e.g. "openclaw-gatewa").
69+
* When a process with a matching comm forks, it and its child are auto-tracked
70+
* even if the process predates the monitor (e.g. long-lived gateway workers). */
71+
BPF_HASH(tracked_comms, u64, u8, 16);
6872
/* OpenClaw gateway PIDs: never SIGKILL these (openat/statx log-only) */
6973
BPF_HASH(protected_pids, u32, u8, 16);
7074

@@ -124,6 +128,19 @@ static __always_inline u64 simple_hash(const char *s, int len) {
124128
return h;
125129
}
126130

131+
/* Hash comm string from a local stack buffer (bpf_get_current_comm output). */
132+
static __always_inline u64 hash_comm_buf(const char *buf) {
133+
u64 h = 5381;
134+
#pragma unroll
135+
for (int i = 0; i < 16; i++) {
136+
char c = buf[i];
137+
if (c == 0)
138+
break;
139+
h = ((h << 5) + h) + (u64)c;
140+
}
141+
return h;
142+
}
143+
127144
/* Hash path from kernel buffer (e.g. bpf_d_path output); must match djb2. */
128145
static __always_inline u64 hash_path_buf(const char *buf, int max_len) {
129146
u64 h = 5381;
@@ -732,10 +749,25 @@ TRACEPOINT_PROBE(sched, sched_process_fork) {
732749
* thread PID as parent_pid. When a worker thread (libuv, etc.) forks
733750
* a child, parent_pid is the thread PID which may differ from the TGID. */
734751
u8 *tracked = tracked_pids.lookup(&parent_pid);
752+
u32 tgid = bpf_get_current_pid_tgid() >> 32;
735753
if (!tracked) {
736-
u32 tgid = bpf_get_current_pid_tgid() >> 32;
737754
tracked = tracked_pids.lookup(&tgid);
738755
}
756+
if (!tracked) {
757+
/* Comm-based fallback: auto-track openclaw worker processes that predate
758+
* the monitor. When a process whose comm matches a known openclaw worker
759+
* (e.g. "openclaw-gatewa") forks, backfill it into tracked_pids so that
760+
* its child—and all future descendants—are correctly tracked. */
761+
char cur_comm[16] = {};
762+
bpf_get_current_comm(&cur_comm, sizeof(cur_comm));
763+
u64 comm_h = hash_comm_buf(cur_comm);
764+
u8 *comm_tracked = tracked_comms.lookup(&comm_h);
765+
if (comm_tracked) {
766+
u8 one = 1;
767+
tracked_pids.update(&tgid, &one);
768+
tracked = comm_tracked;
769+
}
770+
}
739771
if (tracked) {
740772
u8 one = 1;
741773
tracked_pids.update(&child_pid, &one);
@@ -998,10 +1030,17 @@ TRACEPOINT_PROBE(syscalls, sys_enter_write) {
9981030
/* ── exit: clean up tracked set + heuristic state ── */
9991031
TRACEPOINT_PROBE(sched, sched_process_exit) {
10001032
u64 pid_tgid = bpf_get_current_pid_tgid();
1001-
u32 pid = pid_tgid >> 32;
1002-
1003-
tracked_pids.delete(&pid);
1004-
heu_fork_state.delete(&pid);
1033+
u32 tgid = pid_tgid >> 32; /* userspace PID = thread group leader */
1034+
u32 tid = (u32)pid_tgid; /* kernel thread ID */
1035+
1036+
/* Only remove from tracked_pids when the thread group leader (main thread)
1037+
* exits. Worker threads (libuv-worker, pthread pool, etc.) share the same
1038+
* TGID as their parent process. Deleting on every thread exit would wipe
1039+
* the parent process from tracking, causing intermittent enforcement gaps. */
1040+
if (tgid == tid) {
1041+
tracked_pids.delete(&tgid);
1042+
heu_fork_state.delete(&tgid);
1043+
}
10051044
/* heu_state and heu_connect_state keys include pid; they'll age out or we
10061045
* could delete by prefix — skip for now to avoid iteration */
10071046

0 commit comments

Comments
 (0)