#755·jc

ss drops and corrupts process names with spaces, colons or parens

Author: ppietikainenCreated Sep 14, 2026Updated Sep 15, 2026
Labelsbugready-to-ship

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:

python
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 literal

What 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:

bash
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 19001

Three causes, as far as I can tell:

  1. _parse_opts does val.replace('(', '[') before ast.literal_eval, which rewrites parentheses inside the quoted name — hence svc[1.
  2. key, val = item.split(':') splits on every colon, so a name containing one raises before the value is looked at.
  3. 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 and dict(zip(header_list, entry_list)) drops the overflow. The opts column then holds a truncated users:(("my and 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.