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.
| # | level | area | title |
|---|---|---|---|
| 1 | HIGH | exec (dwfilter) | execvp() with attacker-controlled args; documented design |
| 2 | HIGH | tempfile | Temp file path hardcoded /tmp/dwdiffXXXXXXXX |
| 3 | MEDIUM | icu | option.decomposition global, set at startup; per-cluster bounded |
| 4 | MEDIUM | icu | u_strFoldCase + u_getIntPropertyValue per char; bounded |
| 5 | MEDIUM | fopen | fopen(optArg, "r+") user-controlled; design choice |
| 6 | LOW | fopen | "r+" does NOT create the file (safe) |
| 7 | LOW | file | option.output not closed on parse-error path (kernel reaps) |
| 8 | LOW | env | getenv("HOME") in profile.c; standard |
| 9 | INFO | diff | Bundled stripped GNU diff (no subprocess injection in dwdiff main) |
| 10 | INFO | icu | Public ICU API only; no internal headers |
| 11 | INFO | network | No outbound socket / connect in dwdiff or ICU as we use it |
| 12 | INFO | size | ICU 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
- Statically linked ICU 78.3 — no system
ICU dependency, no
LD_LIBRARY_PATHrequirement.otool -Lon macOS confirms nolibicu*.dylibis referenced. - 5-target CI matrix — each release is rebuilt from scratch on every push + every tag. No cached binary artefacts cross the trust boundary.
- Vendored at
dwdiff 2.1.4+ICU 78.3— the latest stable versions of both, inheriting upstream security fixes. - ICU build with
--disable-icuio --disable-icusnfp --disable-icuscriptbreaks --disable-extras— keeps the static lib under 30 MB. The disabled components are not used by dwdiff's code path.