Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">

<!-- Styles -->
<link rel="stylesheet" href="/static/css/main.css?v=21">
<link rel="stylesheet" href="/static/css/main.css?v=25">
</head>
<body>
<!-- ================= SEO CONTENT (INDEXED BY GOOGLE) ================= -->
Expand Down Expand Up @@ -83,16 +83,6 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
<div id="app">
<svg></svg>
</div>
<nav class="rooms-index" aria-label="Linux kernel rooms" style="position:fixed;left:12px;bottom:8px;z-index:4;display:flex;flex-wrap:wrap;gap:10px;font:11px 'Share Tech Mono',monospace;letter-spacing:0.4px;">
<a href="/linux-crypto-subsystem" style="color:#555;text-decoration:none;">Crypto</a>
<a href="/linux-security-subsystem" style="color:#555;text-decoration:none;">Security</a>
<a href="/linux-processes-subsystem" style="color:#555;text-decoration:none;">Processes</a>
<a href="/linux-memory-subsystem" style="color:#555;text-decoration:none;">Memory</a>
<a href="/linux-network-subsystem" style="color:#555;text-decoration:none;">Network</a>
<a href="/linux-filesystem-subsystem" style="color:#555;text-decoration:none;">Files</a>
<a href="/linux-devices-subsystem" style="color:#555;text-decoration:none;">Devices</a>
<a href="/kernel-dna" style="color:#555;text-decoration:none;">Kernel DNA</a>
</nav>

<!-- ================= SCRIPTS ================= -->
<script src="/static/js/safe-utils.js?v=2"></script>
Expand All @@ -109,7 +99,7 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
<script src="/static/js/memory-card.js?v=5"></script>
<script src="/static/js/waits-card.js?v=3"></script>
<script src="/static/js/sockets-card.js?v=1"></script>
<script src="/static/js/flow-card.js?v=15"></script>
<script src="/static/js/flow-card.js?v=20"></script>
<script src="/static/js/flow-history-card.js?v=1"></script>
<script src="/static/js/history-card.js?v=2"></script>
<script src="/static/js/ipc-ui.js?v=8"></script>
Expand All @@ -122,9 +112,7 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
<script src="/static/js/nginx_files.js?v=6"></script>
<script src="/static/js/right-semicircle-menu.js?v=55"></script>
<script src="/static/js/kernel-dna.js?v=50"></script>
<script src="/static/js/ip-entry-card.js?v=1"></script>
<script src="/static/js/network-stack.js?v=67"></script>
<script src="/static/js/network-socket-body.js?v=3"></script>
<script src="/static/js/network-stack.js?v=62"></script>
<script src="/static/js/devices-belt.js?v=25"></script>
<script src="/static/js/aes-ref.js?v=3"></script>
<script src="/static/js/crypto-belt.js?v=61"></script>
Expand All @@ -140,7 +128,7 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
console.error('❌ RightSemicircleMenuManager class NOT loaded!');
}
</script>
<script src="/static/js/main.js?v=245"></script>
<script src="/static/js/kernel-tape.js?v=13"></script>
<script src="/static/js/main.js?v=249"></script>
<script src="/static/js/kernel-tape.js?v=15"></script>
</body>
</html>
1 change: 1 addition & 0 deletions kernel_ai/api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
("/active-connections", "active_connections", h.active_connections, None),
("/traceroute", "traceroute_info", h.traceroute_info, None),
("/flow-history", "flow_history", h.flow_history, None),
("/socket-activity", "socket_activity", h.socket_activity, None),
("/ip-entry", "ip_entry", h.ip_entry, None),
("/network-stack-realtime", "network_stack_realtime", h.network_stack_realtime, None),
("/devices-realtime", "devices_realtime", h.devices_realtime, None),
Expand Down
2 changes: 2 additions & 0 deletions kernel_ai/http/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
runqueue,
sentry_test,
siem_alerts,
socket_activity,
syscall_detail,
syscalls_realtime,
wakeups,
Expand Down Expand Up @@ -107,6 +108,7 @@
"security_realtime",
"sentry_test",
"siem_alerts",
"socket_activity",
"syscall_detail",
"syscalls_realtime",
"traceroute_info",
Expand Down
12 changes: 12 additions & 0 deletions kernel_ai/http/api_handlers/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ def _payload():
return api_json(_payload)


def socket_activity():
def _payload():
local = request.args.get("local", "").strip()
remote = request.args.get("remote", "").strip()
proto = request.args.get("proto", "TCP").strip()
if not local or not remote:
raise ValueError("Missing 'local' or 'remote' query parameter")
return _telemetry.get_socket_activity(local=local, remote=remote, proto=proto)

return api_json(_payload, exception_statuses=[(ValueError, 400)])


def syscall_detail(name):
"""What one syscall is inside this kernel, plus who is parked in it.

Expand Down
10 changes: 10 additions & 0 deletions kernel_ai/http/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ def linux_devices_subsystem_html():
return redirect("/linux-devices-subsystem", code=301)


def io_elevator_prototype_page():
"""Local design prototype: vintage floor-selector as block I/O elevator."""
return render_template("io-elevator-prototype.html")


def syscall_selector_prototype_page():
"""Local design prototype: selector arms as sys_call_table dispatch."""
return render_template("syscall-selector-prototype.html")


def health_check():
return jsonify(
{
Expand Down
72 changes: 71 additions & 1 deletion kernel_ai/services/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,12 @@ def _float_field(text, name):


def _parse_ss_flow_row(header, details, proto):
"""Biography fields from one ss -inoe row. No sock cookie, no cgroup path."""
"""Biography fields from one ss -inoe row. No sock cookie, no cgroup path.

Also the shape of a full segment on this path — mss, pmtu, the negotiated
window scale. That is what lets a reader draw the frame to true scale
instead of a nominal one.
"""
parts = header.split()
if proto == "TCP":
if len(parts) < 5:
Expand Down Expand Up @@ -435,6 +440,16 @@ def _parse_ss_flow_row(header, details, proto):
if name in _CC_NAMES:
cc = name
break
# ss prints the negotiated options as bare words before the cc name. They
# decide how many bytes of every segment are option header rather than data.
opt_ts = bool(re.search(r"(?<![\w:])ts(?![\w:])", details))
opt_sack = bool(re.search(r"(?<![\w:])sack(?![\w:])", details))
wscale_snd = None
wscale_rcv = None
wscale_match = re.search(r"\bwscale:(\d+),(\d+)", details)
if wscale_match:
wscale_snd = int(wscale_match.group(1))
wscale_rcv = int(wscale_match.group(2))
cgroup = None
cg = re.search(r"cgroup:(\S+)", header)
if cg:
Expand Down Expand Up @@ -465,6 +480,16 @@ def _parse_ss_flow_row(header, details, proto):
"rtt_var_ms": rtt_var,
"min_rtt_ms": _float_field(details, "minrtt"),
"cwnd": _int_field(details, "cwnd"),
"mss": _int_field(details, "mss"),
"advmss": _int_field(details, "advmss"),
"pmtu": _int_field(details, "pmtu"),
"snd_wnd": _int_field(details, "snd_wnd"),
"rcv_space": _int_field(details, "rcv_space"),
"rto_ms": _int_field(details, "rto"),
"wscale_snd": wscale_snd,
"wscale_rcv": wscale_rcv,
"opt_ts": opt_ts,
"opt_sack": opt_sack,
"cc": cc,
"timer": _parse_ss_timer(blob),
"source": "ss -tinoe" if proto == "TCP" else "ss -uinoe",
Expand Down Expand Up @@ -504,6 +529,51 @@ def _ss_flow_dump(proto):
return rows


_SS_USER_RE = re.compile(r'users:\(\("([^"]+)",pid=(\d+),fd=(\d+)\)')


def get_socket_owner(local=None, remote=None, proto="TCP"):
"""Which process holds this 4-tuple, when the kernel will admit it.

``ss -p`` walks /proc/*/fd looking for the socket inode, so it only names
processes the caller is allowed to look inside. A socket belonging to
another user comes back unowned rather than guessed at, and costs about
four times a plain dump — which is why this is a separate call from the
flow-history path rather than a flag on it.
"""
proto = str(proto or "TCP").upper()
want_local = _endpoint_key(local)
want_remote = _endpoint_key(remote)
if not want_local or not want_remote:
return {}
ss_cmd = resolve_binary("ss")
if not ss_cmd:
return {}
flags = "-tnp" if proto == "TCP" else "-unp"
try:
result = subprocess.run(
[ss_cmd, flags], capture_output=True, text=True, timeout=3, check=False
)
except (subprocess.TimeoutExpired, OSError):
return {}
for line in (result.stdout or "").splitlines():
parts = line.split()
if len(parts) < 5:
continue
if _endpoint_key(parts[3]) != want_local or _endpoint_key(parts[4]) != want_remote:
continue
match = _SS_USER_RE.search(line)
if not match:
return {"visible": False, "reason": "socket belongs to another user"}
return {
"visible": True,
"comm": match.group(1),
"pid": int(match.group(2)),
"fd": int(match.group(3)),
}
return {}


def get_flow_history(local=None, remote=None, proto="TCP"):
"""Biography of one 4-tuple from ss: lifetime bytes, last talk, min RTT.

Expand Down
112 changes: 112 additions & 0 deletions kernel_ai/services/syscalls.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,118 @@ def _sample_visible_processes(syscall_names, map_syscall_to_subsystem_fn, kernel
return [] if platform.system() == "Linux" else fallback_mock_calls_fn()


# One process can park hundreds of threads in a call; past this the list stops
# being readable and the honest total is reported beside it.
_MAX_TASK_CALLS = 48


def _read_proc_io(pid):
"""syscr/syscw of a task: how many read and write calls it has ever made.

These are counters the kernel keeps per task, so a difference between two
samples is a real count of calls made in between — the closest thing to
"what is it calling" that is readable without ptrace.
"""
out = {}
try:
with open(f"/proc/{pid}/io", "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
key, _, value = line.partition(":")
if key in ("syscr", "syscw", "rchar", "wchar"):
out[key] = int(value.strip())
except (OSError, ValueError):
return {}
return out


def _read_proc_ctxt(pid):
"""Voluntary and forced context switches — world-readable, unlike the rest."""
out = {}
try:
with open(f"/proc/{pid}/status", "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
if line.startswith("voluntary_ctxt_switches"):
out["voluntary"] = int(line.split(":")[1].strip())
elif line.startswith("nonvoluntary_ctxt_switches"):
out["forced"] = int(line.split(":")[1].strip())
except (OSError, ValueError, IndexError):
return {}
return out


def read_process_calls(pid, syscall_names, max_calls=_MAX_TASK_CALLS):
"""What one process is calling, at the two levels the kernel will show us.

The name of the call a thread is parked in comes from /proc/<pid>/syscall,
which needs ptrace-level access and is therefore closed for most processes
on a box with ptrace_scope set. The *counts* of read and write calls in
/proc/<pid>/io are not, so a process that will not tell us what it is
parked in will still tell us how many calls it has made. The two are
reported separately: a missing name is not a quiet process.
"""
try:
pid = int(pid)
except (TypeError, ValueError):
return {"readable": False, "reason": "bad pid"}

io = _read_proc_io(pid)
ctxt = _read_proc_ctxt(pid)
base = {"readable": bool(io or ctxt), "io": io, "ctxt": ctxt}
if not base["readable"]:
return {"readable": False, "reason": "process is gone or closed to us"}

task_dir = f"/proc/{pid}/task"
try:
tids = sorted(os.listdir(task_dir), key=int)
except (OSError, ValueError):
return dict(base, calls_readable=False, reason="task list not readable", calls=[])

calls = []
parked = 0
denied = 0
for tid in tids:
try:
with open(f"{task_dir}/{tid}/syscall", "r", encoding="utf-8", errors="replace") as fh:
line = fh.read().strip()
except PermissionError:
denied += 1
continue
except (OSError, ValueError):
continue
if not line or line in ("-1", "running"):
continue
head = line.split()
try:
nr = int(head[0])
except (IndexError, ValueError):
continue
parked += 1
if len(calls) >= max_calls:
continue
calls.append({
"tid": int(tid),
"comm": _read_comm(tid),
"nr": nr,
"name": syscall_names.get(nr, f"syscall_{nr}"),
})
if denied and not calls:
return dict(
base,
calls_readable=False,
reason="ptrace scope hides what it is parked in",
calls=[],
threads=len(tids),
)
return dict(
base,
calls_readable=True,
reason=None,
calls=calls,
parked=parked,
threads=len(tids),
)


def get_softirq_nucleotides(map_interrupt_to_subsystem_fn, limit=8):
"""Per-vector softirq totals from /proc/softirqs."""
out = []
Expand Down
Loading
Loading