
Diffing Zyxel GS1900 Firmware to Confirm Patch 3 Actually Closes the Root RCE
On this page
What this Zyxel GS1900 firmware diff needs to settle
This walkthrough shows how to diff two Zyxel GS1900 firmware images to confirm whether the follow-up patch actually closes the root command execution path, and how to verify the result on a lab switch instead of trusting the advisory. The narrow question is not “is the Zyxel GS1900 vulnerable.” Three outlets reported attacks, and the flaw behind those attacks was described as separate from the one everyone thought had been fixed. The question I actually care about is smaller and harder: after the follow-up patch, is the path from an HTTP request to a root shell gone, or did the vendor filter the exact string from the incident write-up while the sink stayed reachable through a sibling handler?
You cannot answer that from an advisory. Advisories describe symptoms and affected versions. They almost never say “the system() call in cfg_handler is now unreachable from any authenticated request.” A firmware diff says that, or it says the opposite, and it says it in a form you can re-run.
So this is a walkthrough of the diff I would run against two GS1900 images — the pre-patch build and the one the vendor shipped afterward — and what each of the two plausible outcomes actually means. Where I have not run something on real GS1900 hardware, I say so. The commands are real and runnable; transcripts are labelled as illustrative where I could not produce them from the actual images.
What the Zyxel GS1900 reporting actually establishes — and what it does not
The root RCE claim under test
The reports, all dated 2026-11-05, line up on the same story: attacks against Zyxel GS1900 switches that had been attributed to an already-patched flaw were in fact enabled by a separate defect, and that defect yields root command execution. The vendor then shipped a further patch for affected models. The Hacker News headline frames it as the patch failing to block a zero-day exploit that allows root command execution; Infosecurity frames it as the patch missing the flaw behind the attacks; SecurityWeek frames it as the attacks being blamed on a patched flaw when a zero-day was the real cause.
That is a coherent and specific story. It is also, in the material I was given, three headlines and a summary. I have not read the full articles, and I am not going to reconstruct their details from memory.
What the story implies technically is this: there are at least two defects, not one. Defect A got patched. Attacks continued. Defect B is what actually let an attacker run commands as root, and it survived the first fix. That is the claim a diff can settle — because if defect B reached the same sink through a different caller, the first fix would have changed a caller and left the sink intact.
What the public summaries leave unresolved
The gap between “a patch was issued” and “you can verify the patch” is where defenders get hurt. Missing from the coverage I have:
- A CVE identifier mapped to the second defect. None appeared in the material I was given. It may exist; I did not see it, and I am not going to guess one.
- The fixed build string per model. Without that, “did Patch 3 land” is unanswerable, because the filename you downloaded is not the build you are running.
- Root cause detail: which binary, which CGI endpoint, which parameter, which sink.
- Any statement of whether the second patch removes the shell invocation or filters input before it.
The first three come from the vendor’s own advisory, which is the only source that can tell you which model maps to which fixed build. The fourth comes from a diff, and only from a diff.
Confirmed claims versus open questions
| Claim | Where it comes from | How I am treating it |
|---|---|---|
| GS1900 switches were attacked | SecurityWeek, The Hacker News, Infosecurity (2026-11-05) | Reported by three outlets; not independently verified by me |
| The attacks had been attributed to an already-patched flaw | Same three | Reported |
| The real defect is separate and yields root command execution | Same three | Reported; consistent with the class of bug |
| A further patch was issued for affected models | Same three | Reported |
| “10 models” are affected | tech-insider.org only | Unconfirmed — single lower-tier source |
| The fix is called “Patch 3” | tech-insider.org only | Unconfirmed — an ordinal, not a build string |
| CVE identifier for the second defect | Not in the material I was given | Unknown |
| Fixed build string per model | Not in the material I was given | Unknown — vendor advisory only |
| Which binary/handler/sink is involved | Not in the material I was given | Unknown — this is what the diff answers |
The “10 models” and “Patch 3” numbers are the kind of detail that gets repeated into permanence because it reads precise. Precision is not accuracy. Treat the model count as an upper bound until the vendor advisory confirms it, and treat “Patch 3” as a journalist’s label rather than a version you can compare against show version.
Why “the patch didn’t patch” keeps happening on embedded switches
I have taken apart enough consumer and SMB network gear to have a default hypothesis, and it is not “clever attacker bypass.” The default hypothesis is that the patch fixed the thing that was reported, and the thing that was reported was one route into a shared sink.
Symptom-level fixes leave the sink open
The report an incident responder files says something like: request X with parameter Y reaches a shell. The fastest fix that makes the test case pass is to reject that parameter, or that endpoint, or that specific metacharacter. It closes the reported instance. It does not close the sink, because the sink was never the parameter — the sink is the place where a string built from request data gets handed to /bin/sh.
Filtering at the entry point feels like defense in depth. On these devices it is usually just the first of several entry points, and the developer who wrote the fix knew about exactly one of them.
Shared admin plane creates sibling paths
Embedded switch firmware is rarely a clean layered application. The web UI is typically a small C or Go HTTP daemon with a CGI directory of handlers, plus shell scripts in /etc that do the actual config work, plus a CLI daemon that writes to the same config store. Several handlers independently reach the same helper that shells out. Fix set_dns.cgi and set_ntp.cgi still calls the same do_system() wrapper with a string it built from the POST body.
This is why a “bypass” so often looks trivial in retrospect: it is not a bypass at all. It is the same bug, at a different door, that was never closed.
No sink regression test in the release
Here is the part I hold vendors responsible for. Almost no embedded firmware release ships a regression test that asserts a sink is unreachable. “Patched” in practice means “recompiled with a changed string and QA clicked through the web UI once.”
A test that would have caught the second defect is not exotic: take every handler that can write into the config, feed each one a canary payload, and assert that no child process is spawned with that payload in its argv. That test is maybe eighty lines. It exists for web applications. It does not exist for switch firmware, and the result is a patch cycle you have to verify yourself.
Building a lab where this diff is safe to run
Scope rules
Only diff firmware you obtained legitimately from the vendor, and only run dynamic tests against hardware you own or are explicitly authorised to test. Downloading a firmware image from a third-party mirror is not authorisation, and probing a switch on someone else’s network is a criminal act in most jurisdictions regardless of intent.
Static analysis of an image on your own laptop is low risk. Anything that sends a request is not, so keep dynamic checks on a bench unit that is not connected to production, or on an isolated VLAN with no route to the management plane of anything you care about.
Tooling
What I actually use:
binwalkfor container triageunsquashfs(orsasquatchfor vendor-modified squashfs) for the rootfsdiffoscopefor the file-level diff, because it normalises a lot of noise for freeradare2for quick function-level comparisons,Ghidrawhen I need decompilation to reason about control flow- a USB-TTL serial console or an isolated VLAN plus a process listing for anything dynamic
Install on Debian-ish hosts:
sudo apt install binwalk squashfs-tools diffoscope radare2
## Ghidra separately; keep it out of the analysis VM's network namespace if you like
Handling both firmware images without losing the baseline
Never overwrite the baseline. Structure it like this:
mkdir -p lab/gs1900/{baseline,patched,extract}
cd lab/gs1900
cp ~/firmware/GS1900-<old-build>.bin baseline/
cp ~/firmware/GS1900-<new-build>.bin patched/
chmod a-w baseline/*.bin
sha256sum baseline/*.bin patched/*.bin | tee SHA256SUMSName the directories after the build string you read off the device or the release notes, not after the download date. Six weeks later, “patched” and “new” and “final” will mean nothing to you.
Extracting the GS1900 firmware
binwalk triage of the firmware container
cd lab/gs1900
binwalk baseline/*.binThe output shape you are looking for is a bootloader header, a compressed kernel, and a filesystem:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, ... name: "Linux Kernel"
64 0x40 LZMA compressed data
1048576 0x100000 Squashfs filesystem, little endian, version 4.0,
size: 12058624 bytes, compression: xz
Those offsets are from my notes on a different MIPS switch image, not from a GS1900. Treat them as a shape, not a result — your numbers will differ, and a GS1900 image may use a different container layout or a vendor-modified squashfs that stock unsquashfs refuses. The line that matters is the filesystem offset; everything above it is boot plumbing.
If unsquashfs fails on the extracted blob, that is a signal, not a dead end: try sasquatch, or check whether the filesystem is cramfs, or look at whether there is a second-stage container inside. Zyxel has used more than one packaging scheme across product lines, so do not assume the GS1900 matches whatever blog post you found.
Unpacking the rootfs and mapping the tree
cd lab/gs1900
unsquashfs -d extract/baseline-rootfs -o 1048576 baseline/*.bin
unsquashfs -d extract/patched-rootfs -o 1048576 patched/*.bin
du -sh extract/*-rootfsThen map the tree instead of guessing at paths:
cd lab/gs1900/extract/baseline-rootfs
ls -la bin sbin usr/bin usr/sbin 2>/dev/null | head -60
ls -la etc/init.d | head -40
## find the web surface rather than assuming /www
find . -maxdepth 4 -type d ( -name 'www' -o -name 'web' -o -name 'htdocs' ) 2>/dev/null
find . -type f ( -name 'httpd' -o -name 'lighttpd' -o -name '*web*' ) -perm -u+x 2>/dev/nullTwo habits pay off here. First, file everything that looks like a daemon, because on these images the “HTTP server” is often a vendor binary with an innocuous name. Second, read /etc/init.d/* and rcS before you read any binary: the init scripts tell you which daemon serves the UI, which user it runs as, and whether there is a CGI helper process.
Locating the management surface and CGI handlers
cd lab/gs1900/extract/baseline-rootfs
## CGI handlers and anything that looks like a config setter
find . -type f -path '*cgi*' -o -type f -name '*.cgi' | head -50
## Which handlers shell out? Strings are the fastest first cut.
grep -rlE 'system(|popen(' --include='*' . 2>/dev/null | head
for f in $(find . -type f -perm -u+x -size +20k | head -40); do
if strings -a "$f" | grep -qE '^(/bin/(sh|ash)|system|popen)'; then echo "$f"; fi
donegrep over binaries mostly finds the string system in help text and error messages, so the strings pass is the discriminating one: a binary that contains /bin/sh as a literal is a binary that probably invokes a shell. That shortlist plus the CGI directory is where the rest of this walkthrough lives.
Diffing the pre-patch GS1900 firmware against Patch 3
File-level triage with diffoscope
cd lab/gs1900
diffoscope --no-progress --text diff-filelevel.txt extract/baseline-rootfs extract/patched-rootfs
grep -E '^[+-]{3} ' diff-filelevel.txt | head -80The first real signal is the changed-file list. On a firmware update, the list is normally wide and shallow: a version banner, a build timestamp, a couple of rebuilt shared objects, and one or two binaries with genuine logic changes. The genuinely changed binaries are what you read first.
Filtering build noise out of the firmware diff
Every firmware diff looks enormous because of embedded build metadata. Cut it down:
cd lab/gs1900
## compare file inventories first
( cd extract/baseline-rootfs && find . -type f -printf '%p %s
' | sort ) > base.list
( cd extract/patched-rootfs && find . -type f -printf '%p %s
' | sort ) > new.list
diff base.list new.list | head -60
## then hash-match the identical ones and only diff the rest
comm -12 <(cut -f1 base.list) <(cut -f1 new.list) > common.paths
while read -r p; do
a="extract/baseline-rootfs/$p"; b="extract/patched-rootfs/$p"
cmp -s "$a" "$b" || echo "CHANGED $p"
done < common.pathscmp is blunt and fast and it does not care about timestamps. What survives is the actual list of changed bytes. Expect that list to still include rebuilt-but-identical binaries, which you can eliminate by comparing stripped sections or by checking whether the diff is confined to .comment, .note.gnu.build-id, or a version string.
Symbol and string deltas
cd lab/gs1900
for p in usr/sbin/httpd usr/bin/cfgmgr; do # use your actual paths
a="extract/baseline-rootfs/$p"; b="extract/patched-rootfs/$p"
strings -a -n 6 "$a" | sort -u > "/tmp/$(basename $p).old"
strings -a -n 6 "$b" | sort -u > "/tmp/$(basename $p).new"
echo "== $p"; diff "/tmp/$(basename $p).old" "/tmp/$(basename $p).new"
doneWhat I look for in the delta, in priority order:
- New error strings. A new “invalid parameter” or “not permitted” message next to a config setter is the sound of a filter being added.
- Removed format strings. A vanished
"%s"-shaped format that used to build a shell command is the sound of a concatenation being removed. - New helper names in the symbol table (if the binary is not stripped) like
validate_,sanitize_,is_safe_. - Changed paths: a handler that used to call
do_system()and now calls something else.
New strings and new symbols are suggestive. They are not proof. Proof is the call graph.
The firmware diff outcomes that matter
Everything in that delta collapses into one of three findings:
| Finding in the diff | What it probably means | Confidence |
|---|---|---|
The sink itself changed — system()/popen() call sites removed, replaced with execve + argv, or the shell string construction deleted | A real fix at the dangerous layer | High, if all call sites changed |
| Only a caller changed — a new validation branch added in one handler ahead of the same unchanged sink | A probable bypass; siblings likely still reach the sink | High, if the sink is byte-identical |
| Sink unchanged, no new validation anywhere | The patch addressed something else entirely | Medium — verify against the advisory |
In my experience with this class of device, the second row is the common outcome and the first row is rare. That is an opinion, not a finding about Zyxel specifically — I have not diffed these two GS1900 builds.
Tracing how a request reaches root command execution
The HTTP request-to-root data flow
The path is boringly consistent across vendors:
- An HTTP request hits the UI daemon, authenticated as admin.
- The daemon dispatches to a CGI handler or a route in the same process.
- The handler reads a parameter out of the POST body or query string.
- The handler writes it into the config store — often by invoking a helper with
-k key -v value. - The helper, or the handler, builds a string and hands it to a shell.
Step 5 is the only step that matters. Steps 1–4 are how you get there.
The sinks that matter
| Sink | Typical use | Why it is dangerous |
|---|---|---|
system(const char *) | run a config script, ping, traceroute | passes the whole string to /bin/sh -c |
popen(const char *, "r") | read command output for the UI | same, plus the output may be reflected |
execl("/bin/sh", "sh", "-c", buf, NULL) | convenience wrapper | identical exposure |
execve("/bin/sh", argv, env) with a request-derived argv element | shell script with argument | safe only if argv is not re-parsed by the script |
vendor do_system() wrapper | abstraction over system() | hides the shell from grep, not from the attacker |
The wrapper in the last row is why string-grepping system( is not sufficient on these images. Find the wrapper once, then find every caller of it.
The dangerous shell-command pattern
Here is the shape I expect to see in a pre-patch handler, written as the pattern rather than as decompiled Zyxel code:
/* handler reads "hostname" from the POST body */
char cmd[256];
snprintf(cmd, sizeof(cmd), "cfg set system.hostname '%s'", user_value);
do_system(cmd); /* -> /bin/sh -c */
And the “fix” that keeps showing up:
if (strchr(user_value, ';') || strchr(user_value, '|') ||
strchr(user_value, '&') || strchr(user_value, '$')) {
return -1; /* rejected */
}
A metacharacter blacklist. It misses backticks in some shells, it misses newlines (which terminate a command just as well as ; in sh -c), it misses the quote character that breaks out of the single-quoted context above, and it does nothing about a value that later gets echoed into a config file that a boot script sources. Filtering is a guess about where the parser boundary is; using an argument vector removes the question.
The same failure exists one layer down in shell scripts:
## /etc/scripts/set_hostname.sh
hostname "$1"
echo "HOSTNAME='$1'" >> /etc/sysconfig/network
If any caller invokes that script as set_hostname.sh <user input> via system(), the quoting is already lost before the script sees it.
Confirming the sink is actually closed with patch verification
Static check: trace every call site to the sink
Three conditions, all required:
- In the changed binary, no call site reaches
system()/popen()with a buffer that has request data in it. Verify in a decompiler, not by symbol name — these binaries are usually stripped, somainis an address, and you trace backward from the sink. - Every sibling handler that writes the same config key reaches the same validation, or does not reach the shell at all. Enumerate the siblings from the CGI directory, not from the incident report.
- The shell scripts invoked with arguments do not re-parse them. If a script uses
evalor builds a command from$1, argument-level handling upstream is false comfort.
If condition 1 fails, stop. There is nothing to test dynamically; the fix is not in the image.
Dynamic check: watch for shell children on a lab unit
On a bench unit, on an isolated VLAN, with a serial console attached:
# baseline process count for the web daemon
PID=$(pidof httpd || pgrep -f httpd | head -1)
echo "daemon pid: $PID"
pgrep -P "$PID" | wc -l # children before
## canary request: authorised lab unit only, non-destructive payload
curl -sk -b cookies.txt -X POST --data "hostname=lab-canary-2718" "https://192.0.2.10/cgi-bin/set_hostname.cgi" -o /dev/null -w '%{http_code}
'
## what to look for: a short-lived shell child, or the canary in a child's argv
pgrep -P "$PID" | wc -l
ps -ef | grep -F 'lab-canary-2718' | grep -v grepThe evidence you want is negative: no extra child of the web daemon, no sh -c in ps, and no canary string in any process argv. The evidence that the bug is live is the opposite — a sh that lives for a few milliseconds and is gone before you can look, which is why you capture it from the serial console with ps running in a loop rather than trusting a single snapshot:
while :; do ps -ef | grep -E 'sh -c|canary' | grep -v grep; sleep 0.2; done
Note that “the request returned 200 and the hostname changed” tells you almost nothing. A handler can accept the value, store it verbatim, and render it back into the UI without ever reaching a shell — and equally, it can reject the ; payload while still being exploitable through a different handler you did not test.
Verification results table
Filled in for the method; the GS1900-specific cells are open because I do not have the hardware or the images in front of me.
| Endpoint | Pre-patch (expected) | After follow-up patch | Evidence source | Status |
|---|---|---|---|---|
| Config-write handler named in the report | shell child spawned with request data | unknown | vendor advisory + diff | Untested by me |
| Sibling handler writing the same key | likely same sink, same exposure | unknown | call-graph diff | Untested by me — this is the one I would check first |
| CLI daemon equivalent path | unknown | unknown | binary diff | Untested by me |
| Boot script re-parsing a stored value | unknown | unknown | grep -n 'eval|\\$1' /etc/scripts/* | Untested by me |
The honest reading of that table is that the public reporting does not let you fill it in, and neither does this post. The diff does, and it takes an afternoon.
What a durable fix looks like versus a filter
Remove the shell from the path
/* no shell, no parsing, no metacharacters to blacklist */
char *argv[] = { "/usr/sbin/cfg", "set", "system.hostname", user_value, NULL };
pid_t p = fork();
if (p == 0) { execv(argv[0], argv); _exit(127); }
waitpid(p, NULL, 0);
Overwrite the buffer between fork and exec if it held anything sensitive; the argument vector is still visible in ps to a local attacker, which on a switch with a single admin role mostly does not matter, but it is a real change in exposure compared with a config-file write.
Centralize authorization and input validation at the config-write layer
The per-handler check is the structural problem. If seven CGI handlers write to the config store, there should be one function that writes to the config store, and that function should be the only path to the shell — or better, there should be no shell in it at all, just a validated key/value assignment plus a service reload triggered by a fixed, non-parameterised command.
Moving the check down also fixes the sibling problem without needing to find every sibling. That is the argument for it: you cannot forget a handler you never enumerated.
Add a regression test that fails if any handler reaches the shell with request data
On the lab unit, in CI, from a Node harness that runs against a freshly flashed build:
// Authorised lab unit only. Fails if a request-derived string reaches a shell.
const { execSync } = require("node:child_process");
const BASE = process.env.LAB_SWITCH; // e.g. https://192.0.2.10
const CANARY = "hjcanary" + Date.now();
async function assertNoShellChild(path, body) {
const before = childCount();
await fetch(BASE + path, { method: "POST", body });
const during = childCount();
const inPs = execSync("ps -ef").toString().includes(CANARY);
if (during > before || inPs) {
throw new Error("sink reachable via " + path);
}
}
function childCount() {
const pid = execSync("pidof httpd || pgrep -f httpd | head -1").toString().trim();
return Number(execSync("pgrep -P " + pid + " | wc -l").toString().trim());
}It is not a perfect test — a shell child can spawn and exit between samples. It is enormously better than no test, and it would have caught the class of defect that the reporting describes: a second handler reaching a sink nobody re-checked.
Defensive playbook for anyone running GS1900 hardware
Check management-plane exposure first
Before patch levels, check reachability. An internet-facing switch management UI is the finding, at any patch level.
# from an authorised vantage point only
nmap -Pn -p 80,443,8080,8443 --open <mgmt-ip-range>
## and confirm from outside your perimeter, not from inside itThen go through the inventory question that actually decides your risk: how many GS1900 units do you have, who can route to their management VLAN, and is that VLAN reachable from the guest network or the VPN pool. On most networks I have looked at, the answer to the last one is “yes, because it was convenient in 2019.”
Verifying that Patch 3 actually landed
Do not trust the filename. Read the running build:
- In the web UI, the firmware/version page — capture the build string, not just the model.
- On the console, whatever the CLI exposes as version or firmware info.
- Compare against the fixed build in the vendor advisory, per model.
Two switches in the same rack can be on different builds and the same model number. The only thing that proves the patch is on the device is the string the device reports.
Post-compromise hunting
If the second defect really did give root command execution, and the first patch did not close it, then the window between the first patch and the second is a window in which an attacker with UI access could have had root. Patching does not evict anyone. Look for:
- Local accounts you did not create, especially in any config export
- Config drift you did not authorise — VLANs, mirror ports, management ACLs, SNMP communities, syslog targets
- New listeners or services enabled on the management plane
- Logging disabled, or the log target changed
- Unexplained reboots or config resets
- The surrounding network: a switch is a great place to sit for SPAN/mirror traffic, and a compromised one is a pivot, not just an endpoint
A config export compared against a known-good baseline from before the incident is the highest-value artifact here. If you do not have a pre-incident baseline, you have learned something about what to keep.
Patch, verify, then hunt: order of operations
- Patch. Reduce the window even if you cannot verify yet.
- Verify the build string from the device, not the download.
- Verify the sink behaviour on a sample unit if you have a lab image and a bench device.
- Hunt. Because step 1 does not remove anything that is already resident.
Doing 4 before 1 is a choice; doing 1 and stopping is the common one, and it is the wrong one when root command execution was on the table.
What I confirmed versus what I did not test
I did not confirm, by testing, anything about Zyxel GS1900 firmware. I do not have the images or the hardware, and I did not run the diff above against them.
What I am going on:
- The event itself — active attacks, a prior patch, a separate defect yielding root command execution, a further patch — as reported by SecurityWeek, The Hacker News, and Infosecurity on 2026-11-05. I read the headlines and summary, not the full articles.
- The “10 models” figure and the “Patch 3” label from a single lower-tier source. Unconfirmed, and the label especially should not be used to verify an install.
- CVE mapping, per-model fixed builds, and root cause: unknown to me. Vendor advisory for the first two, a diff for the third.
What the method above gives you that this post cannot: your own evidence. If you run the file-level diff and the sink is byte-identical while only a caller gained a validation branch, you have found a probable bypass in the build you are about to deploy, and you have found it before an attacker does. That is worth an afternoon and a bench switch.
Further Reading
- Zyxel security advisory index — find the GS1900 entry for affected models and fixed build strings: zyxel.com/global/en/support/security-advisories
- SecurityWeek, “Zyxel Switch Hacks Blamed on Patched Flaw Actually Exploited a Zero-Day” (2026-11-05): Google News aggregator link
- The Hacker News, “Zyxel GS1900 Patch Fails to Block Zero-Day Exploit Allowing Root Command Execution” (2026-11-05): Google News aggregator link
- Infosecurity Magazine, “Zyxel Switch Patch Missed Flaw Behind Wave of Attacks” (2026-11-05): Google News aggregator link
- OWASP, OS Command Injection defence cheat sheet — the argument-vector approach used above: cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
More posts

The CVE-2024-21182 WebLogic T3 Exploit: From KEV Listing to Patch Verification for Java Teams
A deep technical walkthrough of CVE-2024-21182 in Oracle WebLogic, tracing its KEV listing and active exploitation history while showing Java teams how to validate patching and harden T3 exposure.

Detecting Zyxel GS1900 Compromise: Management Plane, Config, and Traffic Checks
A technical walkthrough of detecting Zyxel GS1900 zero-day compromises through management-plane anomalies, configuration tampering, and network traffic indicators, after attackers hijacked nearly 1,000 switches in the wild.

Benchmarking ERNIE 5.5 Flash Base: Separating Per-Token FLOPs From KV-Cache Bandwidth at 1M Context
A deep technical walkthrough of benchmarking ERNIE 5.5 Flash Base's 280B-parameter/14B-active MoE at 1M-token context, separating per-token FLOPs from KV-cache bandwidth costs.
Loading comments…