# -*- coding: utf-8 -*- """功能全量模糊测试:找出会"崩"的功能。 约定:参数不合法应该抛 ValueError(会被转成给用户看的错误提示)。 抛其它异常(TypeError / KeyError / AttributeError / IndexError / OSError…) 就是 bug。 设计要点(第一版太慢,这里改过): * 只 fuzz 功能**自己声明**的参数(key / key2),不再瞎塞 13 个 key。 之前 perf / installed_apps 这类会真去调 PowerShell,被调了几百次。 * 有副作用的功能用 DANGEROUS 列表跳过实测。 * 全屏类每次调用后立刻关掉,避免堆窗口。 """ from __future__ import annotations import sys import tempfile import tkinter as tk from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "windows")) # 会真的动系统,不实测 DANGEROUS = { "empty_recycle", "close_windows", "sign_out", "sleep", "hibernate", "restart_graphics", "minimize_all", "task_view", "restart_explorer", "lock_only", "lock_and_off", "clear_clip", "open_recycle", "open_file", "display_mode", "fade_volume", "monitor_on", "volume_up", "volume_down", "mute_all", "flush_dns", "password_gen", "cinema_mode", "random_song", "play_video", "stop_after", "pomodoro", "reminder", "cheer", "clip_fav_add", "download_file", "qr_code", "speed_test", "public_ip", } # 会开全屏窗口的:能测,但测完要关 FULLSCREEN = { "big_text", "big_text_quiet", "countdown_screen", "roll_call", "whiteboard", "danmaku", "marquee", "lottery", "dice", "coin_flip", "countdown_321", "score_board", "score_change", "matrix_rain", "starfield", "dashboard", "stopwatch", } # 读一次就够、不跟参数走的(避免重复 shell 调用拖慢测试) EXPENSIVE = { "perf", "disk", "battery", "uptime", "installed_apps", "top_services", "event_errors", "temperature", "hardware_info", "startup_items", "scheduled_tasks", "system_detail", "recent_files", "network_speed", "gpu_info", "arp_devices", "net_adapters", "wifi_password", "disk_by_type", "world_clock", "timestamp_now", "uuid_gen", "screen_color", } JUNK = [None, [], {}, "", "abc", 999999, -1, 0, "a" * 3000] PASSED = 0 CRASHES: list[tuple[str, str, str]] = [] SLOW_SKIPPED: list[str] = [] def main() -> int: global PASSED from lanremote.actions import Actions from lanremote.config import Store from lanremote.features import Features tmp = Path(tempfile.mkdtemp()) store = Store(tmp) class FakeUI: root = tk.Tk() root.withdraw() def ui_call(self, fn, timeout=5.0): return fn() def screen_size(self): return (1920, 1080) def notify(self, *a, **k): pass def log(self, *a, **k): pass def show_message(self, *a, **k): pass def close_image(self): pass actions = Actions(FakeUI(), store) features = Features(actions) defs = {d["name"]: d for d in features.defs()} print(f"功能总数: {len(defs)}", flush=True) def cleanup_fullscreen(name): if name in FULLSCREEN: try: features.run("big_text_clear", {}) except Exception: # noqa: BLE001 pass try: FakeUI.root.update() except tk.TclError: pass # ---------------- 一、空参数 ---------------- print() print("=" * 74) print("一、空参数({})") print("=" * 74, flush=True) for name in sorted(defs): if name in DANGEROUS: continue try: features.run(name, {}) PASSED += 1 except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append((name, "{}", f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {name}({{}}) -> {type(exc).__name__}: {exc}", flush=True) cleanup_fullscreen(name) # ---------------- 二、垃圾参数 ---------------- print() print("=" * 74) print("二、垃圾参数(只喂功能自己声明的 key)") print("=" * 74, flush=True) for name in sorted(defs): if name in DANGEROUS: continue spec = defs[name] keys = [k for k in (spec.get("key"), spec.get("key2")) if k] if not keys: continue # 不接参数的功能,上面那轮已经覆盖 if name in EXPENSIVE: SLOW_SKIPPED.append(name) continue for key in keys: for junk in JUNK: try: features.run(name, {key: junk}) PASSED += 1 except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append((name, f"{key}={junk!r}"[:40], f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {name}({key}={junk!r}) -> " f"{type(exc).__name__}: {exc}", flush=True) break cleanup_fullscreen(name) # ---------------- 三、越权路径 ---------------- print() print("=" * 74) print("三、路径越权(必须全部拒绝)") print("=" * 74, flush=True) escapes = [ r"C:\Windows\System32\drivers\etc\hosts", r"..\..\Windows\win.ini", r"C:\Windows", "/etc/passwd", r"\\?\C:\Windows\win.ini", r"C:Windows\win.ini", r"\Windows\win.ini", r"C:\Windows\..\Windows\win.ini", " C:\\Windows\\win.ini ", ] for path in escapes: try: resolved = features.resolve_in_roots(path) allowed = [str(r.resolve()).lower() for r in features.allowed_roots()] inside = any(str(resolved).lower().startswith(a) for a in allowed) if inside: PASSED += 1 else: CRASHES.append(("resolve_in_roots", repr(path), f"逃出允许范围: {resolved}")) print(f" [越权!] {path!r} -> {resolved}", flush=True) except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append(("resolve_in_roots", repr(path), f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {path!r} -> {type(exc).__name__}: {exc}", flush=True) # 注意:search_files 接的是 keyword(拿去当文件名匹配),不是路径, # 它本来就逃不出允许目录,所以不放在路径测试里。 file_features = ["list_dir", "make_dir", "rename_file", "delete_file", "open_file", "copy_file", "move_file", "zip_dir", "unzip", "file_info", "folder_size"] for name in file_features: for path in escapes[:5]: try: features.run(name, {"path": path, "to": path, "name": "zz", "keyword": "zz"}) CRASHES.append((name, repr(path), "越权路径竟然被执行了")) print(f" [越权!] {name}({path!r}) 竟然执行了", flush=True) except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append((name, repr(path), f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {name}({path!r}) -> " f"{type(exc).__name__}: {exc}", flush=True) # ---------------- 四、目录守卫 ---------------- print() print("=" * 74) print("四、目录守卫(不能对允许目录本身动手)") print("=" * 74, flush=True) for name, params in (("rename_file", {"path": "Downloads", "name": "zz"}), ("delete_file", {"path": "Desktop"}), ("move_file", {"path": "Downloads", "to": "Documents"}), ("copy_file", {"path": "Desktop", "to": "Downloads"})): try: features.run(name, params) CRASHES.append((name, str(params), "竟然允许对允许目录本身动手")) print(f" [危险!] {name} 竟然允许了", flush=True) except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append((name, str(params), f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {name} -> {type(exc).__name__}: {exc}", flush=True) # ---------------- 五、数据目录 ---------------- print() print("五、数据目录(配对码 / 设备令牌)不可访问") # 用真实的 store.dir 构成绝对路径来测 —— 相对路径会解析到 Downloads 下, # 那是允许目录,测不出东西。真正的 CWD 相对路径由端到端测试覆盖。 probes = [str(store.dir / "settings.json"), str(store.dir / "devices.json"), str(store.dir)] for probe in probes: try: features.resolve_in_roots(probe) CRASHES.append(("resolve_in_roots", probe, "数据目录竟然可访问")) print(f" [危险!] {probe} 竟然可访问", flush=True) except ValueError: PASSED += 1 except Exception as exc: # noqa: BLE001 CRASHES.append(("resolve_in_roots", probe, f"{type(exc).__name__}: {exc}")) print(f" [崩溃] {probe} -> {type(exc).__name__}: {exc}", flush=True) try: FakeUI.root.destroy() except tk.TclError: pass print() print("=" * 74) print("汇总") print("=" * 74) print(f" 正常: {PASSED}") print(f" 崩溃/越权: {len(CRASHES)}") if SLOW_SKIPPED: print(f" 跳过的慢功能(只测了空参数): {', '.join(SLOW_SKIPPED)}") if CRASHES: print() print(" ---- 需要修 ----") seen = set() for name, arg, err in CRASHES: key = (name, err.split(":")[0]) if key in seen: continue seen.add(key) print(f" {name}({arg})\n {err}") return 1 if CRASHES else 0 if __name__ == "__main__": sys.exit(main())