diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-18 21:02:41 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-18 21:02:41 -0400 |
| commit | c2c6a9eb419ebfc0ea5cac25e199362da25f1013 (patch) | |
| tree | 4834f69e2e372836d8814d2ff788ea5ee956cfbe /fw/test/fwtest.rc | |
| parent | 978b9cde1b7f448db5816a0d15fe9f72a94e3341 (diff) | |
netfs: name what is served instead of listing what is hidden
/net/tcp/trans, /net/udp/trans and /net/icmp/trans install kernel
address translations. devip gates them with iseve() (devip.c:406), and
through this server that is fw's identity, not the caller's -- fw does
every open with its own credentials and never looks at the client's. On
a machine where fw runs as eve, which is the ordinary case, there was no
gate at all. Demonstrated in a sandbox with an empty rule set:
=== baseline: real /net, no fw ===
echo: write error: local ip not found
=== inside the sandbox ===
connect: refused (as expected)
append via fw: local ip not found
create via fw: bad process or channel control request
Both errors come from transwrite itself, so the open succeeded and fw
imposed nothing; and the second proves the OTRUNC path is reachable,
which runs transwrite(p, nil, 0, 0) and flushes the whole table before
the write is even parsed.
/net/log was half closed: the write was refused so a program could not
turn tracing on, but reading it was the leak, and anything an
administrator turns on elsewhere is then readable from inside the
sandbox. ipifc data was refused rather than hidden, against the
principle stated ten lines above it for ether and ipmux, and its snoop
file is the same wire and was not mentioned at all.
The pattern is the problem. A list of things to deny has now been wrong
twice, in the same way the ctl filter was, and the answer is the one
that worked there: nothing is served unless it is named. Protocol
directories come from a list of names rather than from "has a clone
file", because devether has one of those too and #l bound into /net
would have become a protocol; a protocol missing from the list is one
nobody can reach, which is the safe way to be out of date. Within one,
only clone, stats and the conversation files, and for ipifc not clone,
not data, not snoop. In the root, only cs and dns writable and arp,
bootp, iproute, ipselftab and ndb readable.
Splitting the path also disposes of a name like "tcp/../.." arriving as
a single walk element from a client speaking 9P straight to the server:
more than three components, or an empty one, is not a path this server
handed out, so it is not one it will honour.
Sixteen new checks. Against the previous netfs.c six of them fail --
trans served, log served, ipifc data and snoop served, and both listing
checks -- while cs, arp, ndb, iproute, ipifc status, clone and connect
filtering all still pass, which is the half that matters. They ask by
stat rather than by read: reading log or a data file blocks until
traffic arrives, so reading would hang on exactly the build that still
serves them, and a test that hangs on a regression is worse than none.
46 pass, twice in a row with no cleanup between.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw/test/fwtest.rc')
| -rwxr-xr-x | fw/test/fwtest.rc | 68 |
1 files changed, 62 insertions, 6 deletions
diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc index 0b923e9..a7ba4d2 100755 --- a/fw/test/fwtest.rc +++ b/fw/test/fwtest.rc @@ -104,6 +104,19 @@ fn have { echo no } +# Is it gone from the served tree? +# +# A stat, not a read. Reading /net/log, or an interface's data or snoop +# file, blocks until traffic arrives, so a check that read them would +# hang rather than fail on exactly the build that still serves them - +# and a test that hangs on a regression is worse than no test. +fn gone { + if(test -e $1) + echo there + if not + echo gone +} + # Stop the firewalls a packet check started. # # Take the interfaces away and fw follows: the relay's read fails and @@ -139,7 +152,8 @@ cat > $tmp/empty.ndb <<'!' ! echo '== namespace mode: what is refused is there to refuse' -for(p in /net/udp/clone /net/gre/clone /net/ndb /net/log /net/ipifc/0/data){ +for(p in /net/udp/clone /net/gre/clone /net/ndb /net/log /net/ipifc/0/data \ + /net/ipifc/0/snoop /net/tcp/trans /net/ether0/clone /net/ipmux/clone){ r=`{have $p} check 'the real /net has '^$p yes $"r } @@ -164,13 +178,55 @@ echo '== namespace mode' r=`{wr /net/ndb 'x'} check '/net/ndb is not writable' refused $"r - r=`{wr /net/log 'tcp'} - check '/net/log is not writable' refused $"r - - r=`{rd /net/ipifc/0/data} - check 'an interface data file is not readable' refused $"r + # trans installs a kernel address translation and devip gates it + # with iseve() -- which is fw's identity through here, not the + # caller's, so on a machine where fw runs as eve there was no gate + # at all. Opening it with truncation also flushed the table. + r=`{gone /net/tcp/trans} + check 'a protocol trans file is not served' gone $"r + r=`{wr /net/tcp/trans '10.9.9.9 80 10.9.9.8 1234 10.9.9.7 5678'} + check 'and cannot be written' refused $"r + + # reading log is the leak, not writing it: turn it on elsewhere and + # it traces every connection on the machine + r=`{gone /net/log} + check '/net/log is not served at all' gone $"r + + r=`{gone /net/ipifc/0/data} + check 'an interface data file is a wire, and is not served' gone $"r + r=`{gone /net/ipifc/0/snoop} + check 'nor is its snoop file, which is the same wire' gone $"r r=`{rd /net/ipifc/0/status} check 'but its status still is' ok $"r + + r=`{gone /net/ether0/clone} + check 'a card is not served, clone file or no' gone $"r + r=`{gone /net/ipmux/clone} + check 'nor is ipmux' gone $"r + + # The mount driver splits a path before it sends it, so this only + # asks that the obvious way out is shut; a compound name arriving + # as one walk element needs a client speaking 9P straight to the + # server, and splitpath is what refuses that. + r=`{gone '/net/tcp/../../adm/keys'} + check 'nothing outside /net resolves through it' gone $"r + + # what a program still needs + r=`{wr /net/cs 'tcp!10.9.9.9!80'} + check 'cs still translates' ok $"r + r=`{rd /net/arp} + check 'arp is still readable' ok $"r + r=`{rd /net/ndb} + check 'ndb is still readable' ok $"r + r=`{rd /net/iproute} + check 'iproute is still readable' ok $"r + + r=`{ls -p /net | grep -c '^(log|ipmux|ether0|ether1)$'} + check 'none of them are even listed' 0 $"r + r=`{ls -p /net/tcp | grep -c '^trans$'} + check 'and trans is not listed either' 0 $"r + r=`{ls -p /net/tcp | grep -c '^clone$'} + check 'while clone still is' 1 $"r } @{ |
