| Age | Commit message (Collapse) | Author |
|
Three findings, one interface. matchrule handed back a Rule* and a
pointer into a static char[128], both read by the caller after it had
released rulelock:
e = matchrule(verb, ..., &rule);
if(rule != nil && rule->log) /* freed? */
syslog(0, "fw", "... %s", e); /* whose? */
A rule set installed between the return and those two lines frees the
Rule under them, which is a narrow window but this is a firewall, and
two procs deciding at once overwrite each other's reason -- in a program
whose entire output is the reason. netfs.c ran multi-proc from the
first blocking open and had its own static err with the same problem.
Neither is a race you can test for; both stop existing if the answer
lives in the caller's frame, so it does. Seven positional arguments
become named fields while the signature is being rewritten anyway.
The third is that revalidate could not ask without being counted. A
rule edit rebuilds the set, so every hit count starts at zero, and then
revalidate re-checks each live flow against the new rules and charged
every one of them to the rule that matched. So a rule that had decided
nothing since the edit reported one decision per live connection, and
stats answered a different question after every edit. count says
whether this is traffic.
Also: the log said "deny tcp connect 127.0.0.2" for a connection to a
port it never named. getfields writes over the separators it splits
on, so f[1] afterwards is only what precedes the first "!". The
address is copied before it is taken apart.
Four checks. Two exercise the log path end to end, denied and
permitted, matching the full address and the rule number in
/sys/log/fw -- which they create if it is missing and remove again if
they made it. One reads the hit count after an edit: with count put
back to 1 in revalidate it reports 1 where 0 is right. 51 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
todo.md had three items in both halves of itself. Control files being
world-writable, delete 0, and a dead relay leaving half a firewall were
all in "Fixed since the review" as items 10, 11 and 12, and all three
were still sitting in "Worth fixing" underneath. Anyone working the
open list would have redone them. Now an item appears in one half or
the other, and the first round's entries are one line each, since the
detail is in the commit and the value of this file is the part that is
still true.
Fragments got a sentence that described the mechanism and not the
consequence. "Later fragments now match on addresses and protocol
only" reads as though they would pass under an address rule; what
actually happens is that every rule set in fwrules(6) is written in
ports, later fragments match no port, and the datagram does not cross.
That is the right trade against reading ports out of them, which let a
crafted fragment through, but it is a hole in what works and belongs in
BUGS rather than in a changelog line.
man/fw also now says what the served /net contains, which changed
underneath it and was never written down; and it still said the card
was "not undone reliably", when nothing undoes it at all.
design.md gains the whitelist decision next to the others, because the
argument for it is the same one the rule parser already makes and the
next person to add a file to the served tree should meet it.
The mtu comment claimed a card as its reason while sitting in code both
modes use. Between two stacks there is no card; 1500 is still right,
for a reason worth one sentence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Maxframe was Ehdrlen + 16K, an automatic in etherwriteip, which runs on
a proc created with a 32K stack. It fits, and the previous fix was
right that 64K did not, but half a stack for a buffer that -- since the
pkt interface is now told "mtu 1500" -- can never hold more than 1514
bytes is a number waiting to be wrong again, and libthread allocates
that stack with malloc, so being wrong means quietly corrupting the
heap rather than faulting.
So the buffer belongs to the caller, with its size, and the guard is
against that size. etherout allocates it once, from the same Maxpkt it
sizes its read buffer with, which is the only place that knows how much
can arrive. ether.c no longer has a length of its own to drift.
The IPv6 message now also goes to syslog. fw daemonizes, so a message
on file descriptor 2 goes wherever the shell that started it was
pointing, which for a firewall started at boot is nowhere.
No test. etherwriteip is reached only in card mode, which the suite
stays out of on purpose because a test that can leave the machine with
no network is a test nobody runs. A unit harness for ether.c -- point
efd at a pipe, call etherwriteip, read the frame back -- would cover
this and the broadcast mapping that todo.md still records as written
but never observed. Worth doing; not done here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two warnings have stood in fw.c since the program was written:
warning: fw.c:732 auto declared and not used: buf
warning: fw.c:1286 set and not used: m
Neither matters on its own -- an unused array in fsread, and an m = nil
that the next line overwrites -- but a build that always prints two
warnings is a build whose output nobody reads, which is how the next
one that does matter goes unnoticed. Both are the sort of thing kencc
tells you for free.
So the suite now builds the source from clean and asks the compiler
whether it had anything to say. Reintroducing the unused array makes
it fail with the warning printed under the check, which is what a
finding nobody had to look for should look like. It also checks that
mk succeeded, since a build that does not run produces no warnings
either. Skipped if the source is not on the machine being tested.
48 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The last round removed the two call sites and said the code was gone.
It was not: putback, notehandler and the five back* globals were still
there with no callers, and the comment that was meant to be moved back
to reclaim() was copied instead, so the file carried the same twelve
lines twice in a row above a function they do not describe. kencc does
not warn about an unused static function, so nothing objected.
Dead code that reads like a safety net is worse than none. Anyone
finding putback() would reasonably conclude the card is restored on the
way out; it is not, and the reason is in the comment that now stands
where the duplicate was, so the next person to look does not have to
re-derive atexit's pid matching to find out why.
Also moves netfs.c's "connect takes addr!port" comment down to checkctl,
which it describes, from above the okverbs table, which it does not.
No test: the deletion is invisible at runtime, which is the whole
complaint about it. The suite still passes 46, and card mode -- the
only thing putback ever touched -- is deliberately outside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
/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>
|
|
The serious one is that the ctl filter was a blacklist. checkctl looked
at connect and announce and passed everything else, but udpctl takes
"headers", and udpcreate gives a conversation a live write queue at clone
time:
c->wq = qbypass(udpkick, c);
So three writes -- clone, "headers", a header-prefixed datagram to data --
sent a packet anywhere, with no connect for a rule to match. rudp and
icmpv6 have the same verb, gre has raw and forward. none.ndb did not
mean "no network at all", though the manual said it did. It is now a
whitelist of control messages that cannot reach the network by
themselves, which is the argument this code already made about ndb
attributes it does not recognise, applied where it was not.
Also blocking: %M was never installed, so fmtrules emitted ipmask=%M% and
every ctl edit on a rule set containing ip= failed, while save wrote a
file reload would reject. Tests had exercised the ctl path and the ip=
path but never together.
parserules built the new list in the globals with no lock, so for the
length of a reload the relay procs walked a list that was empty and then
half built -- exactly what installrules' comment promised could not
happen. etherwriteip put a 64KB frame on a 32KB proc stack, the same bug
design.md records learning and fixing in relay(). putback and
notehandler were dead code: atexit matches on the registering pid and
_exits never runs the handlers, which is why the cleanup "did not fire"
rather than being flaky. Nothing puts the card back, and the docs that
said otherwise are corrected.
The rest: expired flows kept matching and refreshing themselves; the pkt
interface claimed a 4096 MTU from a 1514-byte card; ports were read out
of non-first fragments; /net/ndb and /net/log were writable and
ipifc/*/data readable through the filter; control files were
world-writable, and owning them as a user called "fw" locked out the
administrator instead; delete 0 appended a rule reading <nil>; a dead
relay left one direction unfiltered with nothing to notice; and IPv6
unicast under -e was dropped in silence when it is simply not
implemented.
All three modes regression tested after: a namespace refusing headers and
port 22 while allowing 443, a card passing https and then blocking it
live, and the machine's network restored afterwards.
doc/todo.md says which of these were reproduced and which were read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
One program with three modes, sharing one rule engine and one ndb rule
language. Which mode it is depends on what you point it at, and it says
so at startup rather than choosing silently.
fw -e /net/ether0 rules.ndb a card: every packet in or out
fw rules.ndb <side> <side> two networks: everything crossing
fw rules.ndb one namespace: what programs ask for
The first two filter packets on a wire, using the pkt medium: the stack
gives up its card and gets a synthetic one with fw on the other end, so
nothing reaches it that fw did not pass. Since the stack no longer has
ethernet, fw answers ARP for the address it stands in for.
The third serves a filtered /net and matches connect and announce before
they reach the kernel, so a refusal comes back out of dial(2) with a
reason. That is only a boundary if the program also loses #I, which
/dev/drivers does and cannot be undone; fw.rc does it in the right order.
Rules are ndb, matched top to bottom, first match wins, no match denies.
Connections are tracked, so permitting traffic one way permits the
replies. A rule change drops connections the new rules forbid rather
than letting them finish: a block blocks. Logging is per rule, to
/sys/log/fw.
Tested on the init-test VM in all three modes: a page fetched through a
real card, a TCP handshake across two networks, request filtering with
the escape routes closed, live rule changes killing established
connections, and one rule file working unchanged at both altitudes.
doc/todo.md has what is not done. Item 1 is the one that matters: a fw
that dies takes the card's address with it, so the machine loses its
network and fw cannot restart unaided. That also blocks svc supervision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|