Security

Source-level audit summary. The full AUDIT-2026-07-15.md lives in the repo.

Summary

This audit covers two vendored upstreams: dwdiff 2.1.4 (the C source) and ICU 78.3 (the C/C++ source we link into the static binary). Both are evaluated below.

#levelareatitle
1HIGHexec (dwfilter)execvp() with attacker-controlled args; documented design
2HIGHtempfileTemp file path hardcoded /tmp/dwdiffXXXXXXXX
3MEDIUMicuoption.decomposition global, set at startup; per-cluster bounded
4MEDIUMicuu_strFoldCase + u_getIntPropertyValue per char; bounded
5MEDIUMfopenfopen(optArg, "r+") user-controlled; design choice
6LOWfopen"r+" does NOT create the file (safe)
7LOWfileoption.output not closed on parse-error path (kernel reaps)
8LOWenvgetenv("HOME") in profile.c; standard
9INFOdiffBundled stripped GNU diff (no subprocess injection in dwdiff main)
10INFOicuPublic ICU API only; no internal headers
11INFOnetworkNo outbound socket / connect in dwdiff or ICU as we use it
12INFOsizeICU 78.3 is large; static binary ~30 MB without --disable-icuio etc.

Finding #1 (HIGH) — dwfilter execvp()

Where. src/dwfilter.c:156-170 execute(char * const args[]).

Effect. dwfilter is a developer tool, a "post-processor launcher" for wdiff-style output. Its command line is dwfilter <old.txt> <new.txt> <post-processor> -- <post-args>.... The <post-processor> is execvp-ed as a child process. The user explicitly chose it. args[1] is fixed to --dwfilter=<tempfile>; the user-supplied args[2..] are passed to execvp as-is. If the post-processor is sh, this is shell-injection territory — but the user chose the post-processor, so this is them shooting themselves in the foot.

Severity rationale. Same as make, find -exec, xargs. Documented design, no fix.

Finding #2 (HIGH) — temp files default to /tmp

Where. src/tempfile.h:20 TEMPLATE "/tmp/dwdiffXXXXXXXX".

Effect. dwdiff always creates temp files in /tmp, not honoring $TMPDIR. Uses mkstemp() (race-free random suffix), so not exploitable, but on a multi-user system a local attacker can inotify /tmp/dwdiff* and observe that dwdiff is being run.

Suggested fix. Patch the TEMPLATE macro in upstream/dwdiff/src/tempfile.h to honor $TMPDIR first. ~3-line patch. Re-vendor on next upstream sync.

CJK acceptance test (build gate)

The audit's most important deliverable for the dist build is the CJK acceptance test, codified in scripts/smoke.sh. A correctly-built dwdiff must produce, for the CJK fixture in smoke.sh, output that proves ICU is actually linked in:

体育老师无奈,[-总被佔课现象困扰-]{+总被占课现象困扰+}。

Without ICU, dwdiff's CJK output is just [-整行-]{+整行+} with no intra-line visibility. The smoke test asserts the markers are localised (around a phrase, not the whole line) — the failure mode is "build without ICU succeeded but ICU isn't actually linked in".

Dist-only mitigations already applied