The scan stepped over the NUL that ends the received data before breaking,
so on a request whose headers were cut short it returned a pointer one past
the data. handle_post() then tested that byte for the end of the header,
reading whatever an earlier packet had left there instead of the NUL the
appcall wrote, and could go on to run an endpoint against the stale bytes.
Break before advancing so the returned pointer sits on the NUL and the
caller's check sees it.
The digit loop accumulated into a uint16_t without a bound, so a query
such as /vlan.json?vid=65540 read as VLAN 4 and every consumer saw a small,
valid-looking number for an out-of-range one. Clamp the result at 0xffff
once another digit would overflow (6552 * 10 + 9 is the last value that
fits). The consumers already reject or mask 0xffff: vlan_get() refuses
anything from 4095 up, send_l2() masks the index to the table size, and
l2_delete() masks the high byte.
scan_header() found Content-Type and Cookie with is_word(), which compares
bytes exactly and requires a separator after the pattern. Field names are
case-insensitive (RFC 7230 section 3.2), and the whitespace after the colon
is optional, so "content-type: multipart/..." and "Content-Type:multipart/..."
were both treated as absent. The value pointers were then fixed offsets that
assumed exactly one space.
Add header_value(), which matches a lower-case name anchored at the line
start, folds the request bytes to lower case as it compares, and returns
the start of the value past any blanks, so a call site no longer adds the
name length by hand. Cookie scanning reuses the returned pointer, and the
end-of-header test becomes the strstart() the file already has.
The tail of handle_post() interleaved the config and firmware upload
paths behind one config_upload conditional. Give each path its own
function, handle_config_fragment() and handle_firmware_fragment(), and
reduce handle_post() to routing.
Both handlers keep their own copy of the buffer-and-bounds-check
prologue: with SDCC a shared helper costs 2 bytes of direct RAM for
the fragment pointer (a spill slot when the parameter lives in xdata,
a DSEG home when it does not), and direct RAM is fully allocated. The
memory layout is unchanged from the pre-split code.
cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last and cfg_bl are used only
by config_take(), so move them out of file scope and into the function.
The five cursors stay static: SDCC register-caches automatic xdata
locals and spills them through DSEG slots, which costs 6 bytes of direct
RAM this build has no room for; static keeps the access pattern of the
old globals and the memory layout is unchanged. cfg_bl produces no spill
slot at the current register pressure, so it is a plain local. The
mechanism is recorded in issue #386.
handle_post() reused cfg_pos and cfg_end as scratch for unrelated
values; those uses get their own named locals, frag_len and
payload_start.
Both conflicts are the register keyword removal meeting a change of ours,
so each takes main's signature and keeps what the branch was saying:
nic_rx_packet() still returns a bool, and port_ingress_filter_get() stays
declared.
main has since put its own copy of the STP module in BANK2, so the pragma
this branch carried is now duplicated. The one with main's comment stays.
The preamble scan cursor and the buffered-length parameter carry no
state between calls, so they do not need static xdata slots. As plain
locals the compiler places both in registers, trimming 64 bytes of
BANK1 code and two bytes of xdata.
The offsets shared by config_take() stay static: direct data space is
fully allocated on machines like the SWTGW218AS, so plain locals there
add overlay bytes that no longer link, and xdata-class locals spill
three temporaries into direct space while growing the code by roughly
120 bytes. Document pre_acc, whose accumulation across TCP segments is
why it must remain global.
A firmware upload previously ended in a silent connection close, leaving
the client unable to distinguish a verified upload from a failed one.
Send an explicit 200/400 verdict with the CRC result, with
Content-Length so the browser completes the response before the reset,
and only reset the chip once the verdict has been fully ACKed.
The unconditional close after a config upload is gone since the
buffered config path answers with its own response, so drop the
now-unreachable close hack from the streaming path.
A firmware image cannot be buffered whole, so the upload is streamed to
flash, and stream_upload() already resumes across TCP segments. The
multipart preamble did not: handle_post() walked the part headers from the
start of whichever segment it held, so a client that split inside the
octet-stream part header lost its place, never started streaming, and the
request hung. Firefox splits exactly there, right after filename=".
Buffer the multipart body only until the octet-stream part header is
complete, locate the payload, then stream from that point; later segments
stream as before. The header reuses the configuration buffer, which is idle
during a firmware upload, so no extra memory is needed.
cmd_parser.c conflicted twice: main rewrote the pvid command around the
new port-separator and atoi helpers, next to the line where this branch
delegates "stp" to stp_parse(). Both belong, so the STP delegation keeps
main's pvid body.
The linker caught what the merge could not see: main changed atoi_byte()
to return the digit count and leave the value in atoi_results_u8, where
it used to return non-zero on failure and write through a pointer. Note
the sense is inverted, so the three calls in rtl837x_stp.c are adjusted
rather than just re-arranged.
Only httpd.c conflicted: main added the buffered configuration upload
next to the pointers this branch had moved into xdata to free internal
RAM. Both belong, so the new globals sit above declarations that keep
their storage class.
The I2C controller transfers up to sixteen bytes per transaction and
page_impl.c already used that for sfp_send_data(), while sfp_read_reg()
asked for one byte and every caller looped. Reading a module therefore
cost one address phase per byte: 87 transactions when a module is
inserted, 52 for the sfp command, 36 for the vendor block in status.json.
sfp_read_block() replaces sfp_read_reg() and the callers that already
wanted a run of registers ask for it once: the vendor fields as three
16 byte pages, the diagnostics as one transfer, rate and encoding
together. That drops the three paths above to 8, 6 and 3 transactions,
and sfp_send_data() loses its copy of the transfer.
The vendor loops now run over 16..63 rather than 20..59 so the page base
is a multiple of sixteen and the index into the buffer is a single AND.
The four extra bytes at each end are read and discarded. The diagnostics
read asks for 16 bytes rather than the 15 it uses, because 16 is a width
the shipped firmware already exercises and 15 is not.
The device address, the bus selection and the start bit go into the
control register in one write now that the memory address is written
first, so a transfer touches that register once instead of three times.
The register reads take their result from the SFRs directly rather than
through the sfr_data mirror. The result is a bool and the destination is
sfp_buf, so a caller that cares about a failed transfer looks at the
return value instead of a flag.
Every caller gives up on the first failed read rather than carrying a
flag to the end, which is why the module read moved out of handle_sfp
into a function of its own. A module whose read fails is left marked as
absent, so the next poll retries it instead of configuring the SerDes
from bytes that never arrived.
BANK1 -194 bytes, BANK2 +382, common segment +44, xdata +15 for the
buffer, and one byte more of internal RAM free than before the series.
Built for all 25 machine definitions on sdcc 4.5.0; the tightest common
segment is 98 bytes free on SWTG024AS_V2_0, against 54 before this
series.
CONFIG_UPLOAD_BUF at 2560 capped a configuration upload at about 2.2K
while the config sector holds 4K. Size the buffer as CONFIG_LEN plus
room for the multipart framing so the whole sector is usable.
config_take() wrote the payload without checking it against the sector:
safe before only because the buffer could not hold an oversized one.
Reject a payload that does not fit CONFIG_LEN, terminator included,
instead of writing past the erased sector.
send_config() streams a configuration larger than the TCP output buffer
through the cont_addr/cont_len continuation, but set cont_addr to the
offset within the config instead of a flash address, so the tail was read
from code space. The file server sets cont_addr absolute; do the same by
adding CONFIG_START.
The bug was unreachable while CONFIG_UPLOAD_BUF capped uploads below
TCP_OUTBUF_SIZE, but a stored config near the full sector exposes it.
Saving the configuration works in Chrome and fails in Firefox, and the
difference is only how each browser splits the request. The handler
erased the config sector as soon as the request started and then parsed
the multipart body one TCP segment at a time, which requires every
boundary and every part header to fall inside a single segment. Firefox
splits inside a part header, so the parser lost its place and the
erased sector was left holding a truncated body or nothing at all. A
single-burst scripted post lost the whole body the same way.
The configuration is limited to two kilobytes, so the whole request body
now accumulates in xdata and is parsed only after the closing boundary
has arrived. The parts are walked in one pass, the part carrying a
filename is written to a freshly erased sector, and the client receives
a 200 instead of the previous silent close. No segmentation can confuse
this, since the parser only ever sees a complete body.
Locating the closing boundary first also bounds the walk over the parts,
since none can lie beyond it, so the length of the buffer is no longer
the bound and the test for the two trailing dashes is unnecessary.
The walk matches at offsets inside a buffer that is not terminated, so
neither existing helper fits: strcmp() goes on to compare the byte after
the match and is_word_x() demands a separator there. Add strstart() and
strstart_x() for that case, and use strlen_x() for the boundary length.
The firmware upload path still streams, since a megabyte cannot be
buffered, and is untouched.
The variable lives in rtlplayground.c, so it is declared in rtl837x_common.h
with the others there, and it follows the naming of the rest.
The header carried comments on the externs that the definitions in
rtl837x_stp.c repeat, sometimes differently, which is one place too many to
keep in step. What only the header had, the value ranges and what the
designated arrays hold, moved to the definitions; the rest is gone. Function
declarations lost their comments too.
The status printer only prints, so its running commentary went. A define
replaces the bare 33 in stp_in(), and the note on the loop check is down to
what applies at that line.
The three xdata bytes and the reused stp_we_root scratch are gone; the
compiler needs a register for an index either way. Frees four bytes of
xdata and 29 bytes of code.
The window could be armed from the serial console but only ever disarmed
by an HTTP request. save_cmd, which gates arming, is cleared only while
execute_config() replays the startup config, so every interactive command
armed it wherever it was typed, while mgmt_alive, which disarms it, was
written in exactly one place, on HTTP traffic. An operator working
entirely on the serial console therefore lost STP 180 seconds after
enabling it however much they typed, which is what makes the mechanism
impossible to test from a console.
The documentation described the behaviour that was intended rather than
the one that was built, and in both directions: it said a command on the
serial console also confirms, and it said a reboot with STP in the
startup config disables it again three minutes later. Neither held. The
replay path never armed the window at all.
Repairing the asymmetry would have kept a mechanism whose premise is
contested anyway. A watchdog that switches the protection off in response
to silence adds a second failure mode on top of the first: where the
network is misconfigured and STP is the thing holding a storm back,
restoring forwarding removes the last reason management still answers.
Gone with it: the stp failsafe command, the fs and fsT fields of
/stp.json, the input and the tripped banner on the Spanning Tree page,
the two persistence patterns in config.js, the documentation section, and
mgmt_alive itself, which had no other reader.
550 bytes back, 145 of BANK1 and 405 of BANK2, and five of xdata, which
is the four counters and mgmt_alive and nothing else. Built for
SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
stp_dbridge, stp_dpid and stp_dcost were declared and read by the status page,
but nothing ever wrote them, so they stayed zero for the life of the firmware.
The page's validity test then always failed, and the Designated Bridge,
Designated Port ID and Designated Cost columns reported our own values on every
port, including the root port where the answer is the upstream neighbour. The
three arrays reserved 140 bytes of xdata and never used any of it.
They are filled now, right after the loop check, so a frame that came back from
one of our own ports is not mistaken for a neighbour.
The validity test moves from the last byte of the stored MAC to the stored Port
ID. A Port ID is 1-based on the wire and cannot be zero, while a neighbour whose
MAC happens to end in 0x00 would have failed the old test.
The root path cost byte swap happens once now, and the root port branch reuses
the value instead of repeating the shifts.
BANK2 grows 148 bytes, BANK1 loses 9, and xdata does not move.
Review asked for this across the other commits too. Gone are the blocks
that restate what doc/stp.md already says, the ones that explain what an
embedded programmer already knows, and one that had gone stale inside this
very branch: the CLI summary above stp_parse still described "cost <0-255>
(x1000)" while the parser has taken the raw 0 to 200000000 for some time,
and it never learned about p2p or trk at all. A usage list next to the
parser is the kind of thing that rots first, so it is out rather than
updated.
The review flagged one comment saying a variable is in xdata because the
internal RAM overlay is full, on the grounds that it may stop being true.
Four more of the same kind were in these files and are out as well, one of
them pointing at a file that does not exist in this branch at all. The
declarations still say __xdata, which is the part a reader needs.
Also out: the note on why three helpers are __reentrant, which was really
a paragraph about two bytes of DSEG, and the measurement story behind the
tick divider, which belongs with the other timer numbers in doc/stp.md.
One comment was not stale but simply wrong. "max BPDUs per port per second"
sat on stp_failsafe_tripped, having slid down two lines when the two
failsafe variables were inserted above it. It describes stp_txhold and is
back there now.
Short factual labels stay: they sit next to the magic number they explain
and the codebase uses them throughout. The generated code is byte for byte
what it was before this commit, both banks and xdata unchanged.
The timers assumed stp_timers() runs at 64 Hz. It does not: the main loop
idles on the 200 Hz system tick and calls us every fourth pass, i.e.
50 Hz. Measured on hardware - with hello configured to 2 s the BPDUs left
the port 2.560 s apart, exactly the 28 % overshoot the wrong constant
implies, and every other timer (forward delay, max age, tx-hold refill)
was stretched the same way.
Move the constant to the header with the arithmetic spelled out, and use
it in the status page too, which had the 64 hardcoded and therefore aged
the same counters differently than the engine.
Bring the Spanning Tree page in line with a typical managed switch's
per-port panel. Configuration gains the full-range path cost (raw
0..200000000, 0 = auto, replacing the old 1000x-scaled byte), a
point-to-point admin control (auto/on/off), and the priority is now a
0..240 step-16 dropdown. A new status table shows, per port, the Port
State, Role, Designated Bridge / Port ID / Cost (learned from received
BPDUs, kept per port and aged via the BPDU age), Operational Edge and
Operational Point-to-Point.
The designated fields fall back to presenting this switch as the
segment's designated bridge when no fresh BPDU has been heard (so a
quiet port shows our own bridge-id, as the vendor UIs do). /stp.json
carries the packed hex fields plus our own MAC for that fallback.
Space: reclaim BANK2 for the above by moving rtl837x_pins to HOME and
compacting leds_dump into a register-address table (~800B); bandwidth
returns to BANK1. No BANK3 - hardware-verified that PSBANK > 2 crashes
this SoC at boot (a bricked unit and an SPI-programmer recovery earlier
today); a warning to that effect is now in rtl837x_lldp.c.
Hardware-verified: cost 200000000 and p2p off round-trip through the CLI
and JSON, the status table populates correctly with STP enabled (all
ports Forwarding/Designated, oper-edge and oper-p2p True), LACP 3f/3f
and the LAN unaffected.
(cherry picked from commit 2ec62072f061dc9e78bc821ba1c297cb6819e206)
Enabling STP on a bridge whose management rides an in-band VLAN can cut
off that very management - and not only by our own blocking: on this
network the upstream TP-Link Easy Smart switch's "loop prevention"
reacted to our BPDU hellos by blocking ITS port towards us while our
ASIC was all-forwarding, isolating the whole segment until a power
cycle. Recoverable only by going quiet.
Add a commit-confirm watchdog: while STP is enabled, any HTTP request
re-arms a countdown ("stp failsafe <seconds>", default 180, 0 disables);
if management stays silent for the whole window, STP disables itself,
which also stops BPDU TX so a neighbour's loop protection can release
its block. The web UI polls /stp.json every 2 s, so an open browser
naturally keeps the watchdog re-armed. The trip is reported via
/stp.json (fs, fsT) and as a warning on the Spanning Tree page.
Deliberately not conditioned on our own MSTP port states - the incident
above proves the uplink can be dead while every local port forwards.
Also bound the NIC DMA busy-waits (nic_tx_packet, nic_rx_header,
nic_rx_packet): an unbounded spin on SFR_NIC_CTRL freezes the entire
main loop (timers, HTTP, ARP) if the ASIC ever fails to consume a
transfer; give up after ~65k polls and drop the frame instead.
Hardware-verified end to end: with priority 15 against a live RSTP
bridge the uplink died 6 s after "stp on" and the network recovered BY
ITSELF 66 s later (trip at 45 s + neighbour release), fsT=1, LACP and
LAN intact. Telemetry via syslog-to-edge-port host confirmed the full
chain: countdown 44->4, trip, hello TX stopping at the trip.
(cherry picked from commit 1fa9775156fd6d7ebfdda2382f73430b86601230)
Implements the standard 802.1D-2004/802.1w configuration surface:
Bridge: priority (0-15 x4096), hello time, max age, forward delay,
force-version (RSTP v2 / STP-compatible v0 Config BPDUs), tx hold
count (per-port per-second BPDU budget).
Per port: enable, admin edge (forwarding immediately - no listen gap),
auto edge (forwarding after 3 s of BPDU silence; DEFAULT, so
host-facing ports no longer take the full forward delay),
path cost (0=auto/20000), port priority, BPDU guard (port disabled
on BPDU receipt), root guard (never accept a better root on the
port), BPDU filter (no BPDUs in or out).
Engine additions: root max-age expiry (reclaim the tree when the root goes
silent), root path cost accounting (rx cost + root-port cost, advertised in
our BPDUs), loop detection (our own BPDU coming back blocks the port for a
listen period), topology-change counter, approximated per-port roles
(Root/Designated/Alternate) for diagnostics.
CLI: "stp prio|hello|maxage|fwd|txhold|version ..." and
"stp port <n> on|off|edge|cost|prio|guard|filter ..." (stp_parse, delegated
from cmd_parser); all forms accepted by the startup-config validator so the
whole configuration persists. /stp.json now reports config + status; the
Spanning Tree page exposes everything with immediate-apply controls and live
state/role columns (edit-in-flight guard against the 2 s refresh).
8051 memory: the module moves to code BANK2; internal-RAM pressure from
cross-bank calls resolved by xdata loop iterators/scratch, __reentrant on
the small helpers, and moving httpd's header-pointer globals to xdata.
Verified on hardware (SWTGW218AS): defaults land per standard; priority and
hello change live; admin-edge ports (the LACP bond uplinks) keep the LAN at
0% loss THROUGH "stp on"; auto-edge ports forward after 3 s; a port that
heard real BPDUs (a VM bridge behind physical port 6) correctly declined
auto-edge, sat out the full listen period and became Designated; tc counts
promotions; we win the root election at priority 16384 vs 32768.
(cherry picked from commit 09a34dc6acdc81ab9cab0727d2f4a59c68131a3e)
Add a Spanning Tree page: an on/off toggle driving the existing "stp"
command over /cmd, and a live status section fed by a new /stp.json
endpoint - the elected root bridge (priority + MAC), our path cost,
whether we are the root, and the per-port STP state read live from the
ASIC's MSTP register (same 2-bit encoding stp_setup() writes). Ports are
reported by their physical numbers.
Recovered-from: 3132319, 9365c86
The MAC table listing tests bit 0 of byte 2 of the third table data
word for the static flag, but the flag lives in bit 0 of byte 1: an
entry written with byte 1 bit 0 set survives the aging engine
indefinitely where an identical entry without it ages out, and reads
back with exactly that bit set through both the address and the
next-entry read methods. Byte 2 of that word reads zero for learned and
static entries alike, so every entry has always been listed as learned
and a static entry has never been visible as such in the table listing.
The two flags added with the JSON fix sit in data, where internal RAM is
full enough that this branch stopped linking for some toolchains. __bit
puts them in the bit area instead and hands three bytes back to the
stack: SSEG goes from 131 to 134 on SWTGW218AS.
Patch by vDorst on the pull request.
The MAC table listing wrote its separator once per iteration rather than
once per object. An entry the table engine reports as invalid produces no
object, so it contributed a bare comma, and two in a row give ",," which
JSON.parse rejects. The whole table then fails to load, not just the row
that was missing. The separator now goes before each object and the
closing bracket after the loop, which is the shape send_vlanlist already
uses further down the file.
The next index for an invalid entry was computed as h | low + 1, and the
addition binds tighter than the or. That agrees with (h | low) + 1 except
when the low byte reads 0xff and bit 8 of the index is already set, eight
of the 4096 combinations. There the result is the start of the current
block of 256 rather than the start of the next one, so the walk repeats a
block it has already covered. Reading the index once after the branch
rather than once in each arm removes the second copy of that expression
along with the bug.
The VLAN now comes first in each object. It is taken from the same
L2_DATA_OUT_B read that decides whether the entry is valid, which saves
reading that register a second time. The page addresses the fields by
name, so the order they arrive in does not matter to it.
A bound check on the output buffer goes in for consistency with
send_vlanlist. Thirty entries of at most 74 bytes plus the brackets fit in
the 2500 byte buffer with 179 to spare, so nothing changes today, but the
margin was nowhere stated and L2_MAX_TRANSFER is a tunable.
5 bytes of BANK1, nothing in BANK2, xdata or internal RAM. Built for SWTGW218AS
and KP_9000_6XHML_X2 on sdcc 4.5.0.
The member mask of an aggregation group is decoded by hand in two places,
the lag command and the JSON behind the aggregation page, and every branch
that touches trunks adds another copy.
port_lag_members_get() sits next to port_lag_members_set() and both readers
call it. It answers from the hardware, so it covers a group configured with
lag and one a protocol brought up, without either having to say so.
It reads through reg_read() rather than reg_read_m(), so sfr_data is left
alone. Neither caller looked at it afterwards; both read the hash register
next.
The handler took one raw character of the request line and passed it to
send_counters(), which uses it to index machine.phys_to_log_port. That
array has nine entries and the character is whatever the client sent, so
the read ran up to 246 entries past the end and the result went on to
STAT_GET as a port number. is_word() accepts any request whose name is
followed by a question mark, so nothing constrained the byte to a digit.
Bounding it where it is read keeps the check beside the assumption it
protects and needs nothing from the machine description. sdcc leaves
plain char unsigned and the subtraction wraps in eight bits, so a byte
below '0' comes out above 200 and one upper test covers both ends:
exactly '0' to '8' now reach send_counters. The compiled test is
add a,#0xf7 followed by jnc, which I read back out of the assembly rather
than assuming.
Out of range answers 400 by the path the other malformed requests already
take, rather than an empty array. An empty array would have been worse
than useless here, since the statistics page calls BigInt on the first
element before it looks at the length. The page asks only for index zero
to the port count minus one, so nothing that answered before stops
answering, and a non-200 reply makes its handler do nothing at all.
11 bytes of BANK1, nothing in the common segment, BANK2, xdata or
internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
The POST path tests for a NUL before it looks at a byte. The GET path did not,
and is_separator() counts only space, tab, question mark and equals, so a request
line carrying none of those walks past the end of uip_buf and writes its
terminator into whatever xdata it happens to stop on.
Everything that is not a POST reaches that walk. The pointer advances past the
method before anything checks that the method was GET, so a TLS record sent to
port 80 by a browser trying https first is enough on its own, as is a port
scanner or a malformed line. The stop is wherever the first space, tab, question
mark or equals turns up in memory, which is why the symptoms are erratic.
Two bytes of BANK1. BANK2 and xdata do not move.
Review feedback on #306. The value used to ride in /information.json and
the picker fetched /vlanlist separately, so the page needed both requests
to mean anything. /vlanlist now answers {"mgmt":N,"vlan":[...]} and the
picker reads both from the one response. Both consumers in vlan.js were
taught the new shape, and /information.json no longer carries mgmt_vlan.
The truncation guard now reserves 141 bytes instead of 139: the closing
grew to two bytes with the wrapping object, and the comma in front of a
non-first entry was never counted, so the worst case could land one byte
past outbuf even before this change. char_to_html() does not check.
The comments added on this branch are gone as well, style.css and
system.js both, since these files are served byte for byte.
page_impl.rel stays at DSEG 5, OSEG 0, BSEG 3 and the image reports the
same 10183 bytes of XDATA before and after.