ss drops and corrupts process names with spaces, colons or parens
While developing code to parse listening sockets from a variety of Linux hosts (~= 150, all kinds of distros) running a variety of listeners, I found some corner cases in jc. Not all the bugs appeared in the same ss dump, and all came from ordinary processes rather than anything constructed. Claude helpfully distilled them into the following, which is synthetic but easy to reproduce starting from a listening process:
A process name is whatever the kernel put in comm, and spaces, colons and
parentheses are all legal there. The ss parser handles none of them, in three
different ways.
Steps to reproduce
The column alignment matters, so these are real ss -tulnpe rows from a Debian
12 capture with only the quoted name varied:
import jc
HDR = "Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess\n"
ROW = ('tcp LISTEN 0 5 0.0.0.0:19001 0.0.0.0:* '
'users:(("{}",pid=3201,fd=3)) ino:2415307 sk:a004 cgroup:/ <->\n')
for name in ['plain', 'svc(1', 'my proc', 'pro:c', 'my proc:x']:
try:
r = jc.parse('ss', HDR + ROW.format(name))
blk = r[0].get('process') or r[0].get('opts')
print(name, '->', list(blk['process_id'].values())[0]['user'])
except Exception as e:
print(name, '->', type(e).__name__, e)What happens (jc 1.25.7)
plain -> plain
svc(1 -> svc[1 # silently wrong
my proc -> SyntaxError unterminated string literal
pro:c -> ValueError too many values to unpack (expected 2, got 3)
my proc:x -> SyntaxError unterminated string literalWhat I expected
The name as ss wrote it, in all five cases.
Notes
The parenthesis case is the one that worries me. It does not fail, it returns a name that is wrong, so a caller has no way to notice. I only found it because I was diffing process attribution between two collections and a host appeared to have renamed a service.
Reproducing a listener like this is easy if it is useful:
cc -o /tmp/listener listener.c # anything that binds and listens
cp /tmp/listener "/tmp/my proc:x"
"/tmp/my proc:x" 19001 &
ss -tulnpe | grep 19001Three causes, as far as I can tell:
_parse_optsdoesval.replace('(', '[')beforeast.literal_eval, which rewrites parentheses inside the quoted name — hencesvc[1.key, val = item.split(':')splits on every colon, so a name containing one raises before the value is looked at.- Both
re.split(ONE_OR_MORE_SPACE_PATTERN, ...)and the two-or-more-spaces fallback run over the whole row, so a name with a space splits the row anddict(zip(header_list, entry_list))drops the overflow. Theoptscolumn then holds a truncatedusers:(("myand the pid and fd are gone with no error anywhere.
I have a fix for all three that reads the users: records with a regex instead
of evaluating them, and shields the block from the two space-based splits. It
is tested against 36 ss -tulnpe / ss -tulnp captures spanning 13 distros and
iproute2 ss180129 through 6.17.0 — all parse, names come back exactly as
written, and the rest of the suite is unchanged (with TZ=America/Los_Angeles,
per CONTRIBUTING).
There is one thing I did not touch and should mention: when the options region
contains several groups separated by two or more spaces, the row split scatters
them across columns and only the last one reaches _parse_opts, so opts comes
back as a raw string. That affects 12 of my 36 captures and looks like a
separate fix.
Opening a PR with the fixes and corresponding test cases.
Source: kellyjonbrazil/jc