fsp-server: implement GET_PRO (0x47), which Swiss asks for first
Swiss sent CC_GET_PRO 'swiss/patches' and the server replied "unsupported".
Reply shape, from fsplib's fsp_getpro:
* pos MUST equal FSP_PRO_BYTES (1), or fsplib fails with ENOMSG
* the flags byte is at buf[len] -- the first byte of the EXTRA region,
after the echoed name, NOT inside the data field
* a missing directory must be CC_ERR so fsplib reports ENOENT, because
Swiss probes "swiss/patches", then "swiss", then "" and relies on that
fallback chain
Those bits are not cosmetic. deviceHandler-FSP.c does:
if ((pro & (MKDIR|ADD|DEL|GET)) == (MKDIR|ADD|DEL) || (pro & OWNER)) {
features |= FEAT_PATCHES; emulable |= EMU_ETHERNET;
} else { ... clears both ... }
so a plain read-only answer makes Swiss DISABLE patch support on this device.
Default is therefore the honest GET|LIST (0x50) with patches disabled, and
--claim-owner reports OWNER to turn them back on -- flagged in the help as
unsafe for now, because this server has no UP_LOAD or MAKE_DIR, so Swiss
believing it can write here would make a patch write fail rather than degrade.
Try without it first.
T9 checks pos == 1 and that the flags land at buf[len] with GET|LIST and
without OWNER; T10 checks a missing directory gives CC_ERR so the fallback
chain works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,15 @@ import time
|
||||
CC_VERSION, CC_INFO = 0x10, 0x11
|
||||
CC_ERR, CC_GET_DIR, CC_GET_FILE = 0x40, 0x41, 0x42
|
||||
CC_BYE, CC_STAT = 0x4A, 0x4D
|
||||
CC_GET_PRO = 0x47
|
||||
# Directory protection bits (fsplib.h). Swiss uses these to decide whether
|
||||
# the device supports patches:
|
||||
# if ((pro & (MKDIR|ADD|DEL|GET)) == (MKDIR|ADD|DEL) || (pro & OWNER))
|
||||
# features |= FEAT_PATCHES; emulable |= EMU_ETHERNET;
|
||||
# so a plain read-only answer makes Swiss DISABLE patch support here.
|
||||
PRO_OWNER, PRO_DEL, PRO_ADD, PRO_MKDIR = 0x01, 0x02, 0x04, 0x08
|
||||
PRO_GET, PRO_README, PRO_LIST, PRO_RENAME = 0x10, 0x20, 0x40, 0x80
|
||||
PRO_BYTES = 1
|
||||
RDTYPE_END, RDTYPE_FILE, RDTYPE_DIR, RDTYPE_SKIP = 0x00, 0x01, 0x02, 0x2A
|
||||
HSIZE, FSP_SPACE = 12, 1024
|
||||
SERVER_KEY = 0x1234
|
||||
@@ -128,6 +137,13 @@ def main():
|
||||
ap.add_argument("--port", type=int, default=21)
|
||||
ap.add_argument("--quiet-reads", action="store_true",
|
||||
help="only log the rate, not every read")
|
||||
ap.add_argument("--claim-owner", action="store_true",
|
||||
help="report FSP_DIR_OWNER on GET_PRO, which makes Swiss "
|
||||
"enable FEAT_PATCHES and EMU_ETHERNET on this device. "
|
||||
"⚠️ This server is READ-ONLY -- it has no UP_LOAD or "
|
||||
"MAKE_DIR -- so Swiss believing it can write here "
|
||||
"will make any patch write fail. Try without it "
|
||||
"first; use it only if a boot demands patches.")
|
||||
a = ap.parse_args()
|
||||
root = os.path.realpath(a.root)
|
||||
|
||||
@@ -192,6 +208,25 @@ def main():
|
||||
s.sendto(pack(CC_STAT, SERVER_KEY, seq, pos, body), peer)
|
||||
continue
|
||||
|
||||
if cmd == CC_GET_PRO:
|
||||
# Reply shape (fsplib fsp_getpro): pos MUST equal FSP_PRO_BYTES,
|
||||
# and the flags byte sits at buf[len] -- i.e. the first byte of
|
||||
# the EXTRA region, after the echoed name, not inside data.
|
||||
# A non-existent directory must be CC_ERR so fsplib reports
|
||||
# ENOENT: Swiss probes "swiss/patches", then "swiss", then "".
|
||||
if not os.path.isdir(real):
|
||||
s.sendto(pack(CC_ERR, SERVER_KEY, seq, pos, b"no such dir\0"), peer)
|
||||
print(f"[{peer[0]}] GET_PRO {path!r} -> ENOENT (expected; "
|
||||
f"Swiss falls back)")
|
||||
continue
|
||||
pro = PRO_GET | PRO_LIST | (PRO_OWNER if a.claim_owner else 0)
|
||||
s.sendto(pack(CC_GET_PRO, SERVER_KEY, seq, PRO_BYTES,
|
||||
path.encode("latin-1", "replace"), bytes([pro])), peer)
|
||||
print(f"[{peer[0]}] GET_PRO {path!r} -> {pro:#04x}"
|
||||
+ (" (OWNER: Swiss will enable patches)" if a.claim_owner
|
||||
else " (read-only: Swiss disables patches here)"))
|
||||
continue
|
||||
|
||||
if cmd == CC_GET_DIR:
|
||||
if not os.path.isdir(real):
|
||||
s.sendto(pack(CC_ERR, SERVER_KEY, seq, pos, b"not a dir\0"), peer)
|
||||
|
||||
@@ -124,6 +124,36 @@ try:
|
||||
fails.append(f"T8 traversal escaped the root: type {ty} size {sz}")
|
||||
else:
|
||||
print("T8 ../../etc/passwd refused")
|
||||
# T9 GET_PRO: flags go in the EXTRA region at buf[len], and pos must
|
||||
# equal FSP_PRO_BYTES (1) or fsplib returns ENOMSG.
|
||||
CC_GET_PRO = 0x47
|
||||
r = rt(req(CC_GET_PRO, 20, 0, ""))
|
||||
if r["cmd"] != CC_GET_PRO:
|
||||
fails.append(f"T9 GET_PRO cmd {r['cmd']:#04x}")
|
||||
elif r["pos"] != 1:
|
||||
fails.append(f"T9 GET_PRO pos {r['pos']}, want 1 (FSP_PRO_BYTES) "
|
||||
f"- fsplib returns ENOMSG otherwise")
|
||||
else:
|
||||
s.sendto(req(CC_GET_PRO, 21, 0, ""), A)
|
||||
raw = s.recv(2048)
|
||||
ln = struct.unpack("!H", raw[8:10])[0]
|
||||
pro = raw[HSIZE + ln] # buf[len] -- the first extra byte
|
||||
if not (pro & 0x10) or not (pro & 0x40):
|
||||
fails.append(f"T9 GET_PRO flags {pro:#04x} lack GET|LIST")
|
||||
elif pro & 0x01:
|
||||
fails.append(f"T9 GET_PRO claims OWNER without --claim-owner")
|
||||
else:
|
||||
print(f"T9 GET_PRO -> pos=1, flags {pro:#04x} (GET|LIST) at buf[len]")
|
||||
|
||||
# T10 a missing directory must be CC_ERR, so fsplib reports ENOENT and
|
||||
# Swiss falls back from "swiss/patches" to "swiss" to "".
|
||||
r = rt(req(CC_GET_PRO, 22, 0, "swiss/patches"))
|
||||
if r["cmd"] != CC_ERR:
|
||||
fails.append(f"T10 GET_PRO on a missing dir returned {r['cmd']:#04x}, "
|
||||
f"want CC_ERR -- Swiss's fallback chain needs ENOENT")
|
||||
else:
|
||||
print("T10 GET_PRO 'swiss/patches' -> CC_ERR (Swiss falls back to root)")
|
||||
|
||||
finally:
|
||||
srv.terminate()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user