You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Code review of user-src/ — findings for discussion
Reviewed the full contents of user-src/ on current master, with the code-review-cpsource skill's 20-section C checklist. Five novel
findings plus one finding that's already being addressed by open
PR #20. Summary table first, then each finding, then a self-review
correction where PR #20 caught something I didn't.
PR #20 (open, @MarkAtwood) already addresses one of the findings (netlink
key-length validation).
Keyword searches for CLOEXEC, SOCK_CLOEXEC, fopen, memzero, base64, fork exec, fd leak, parse_public_key returned no
relevant PRs or issues. The other five findings below appear novel.
Same bug class as wolfSSL issue #9753
(where PR #10162 is
the upstream fix). In multithreaded hosts or library-embedding callers
that fork()+exec(), these descriptors leak to children. Some carry
private-key material.
File
Line
Call
config.c
145
f = fopen(path, "r"); — private/preshared key file
setconf.c
124
config_input = fopen(argv[2], "r"); — full config
ipc-uapi-unix.h
44, 83
socket(AF_UNIX, SOCK_STREAM, 0) — UAPI socket
netlink.h
505
socket(AF_NETLINK, SOCK_RAW | flags, bus) — flags==0 at every mnl_socket_open caller
netlink.h:505 is the high-leverage site: every mnlg_socket_open() in ipc-linux.h (kernel_get_*, kernel_set_*, kernel_generate_privkey, kernel_derive_pubkey, kernel_generate_psk) routes through it with flags=0. One edit at the mnl_socket_open(bus) wrapper (or the inner __mnl_socket_open) covers all five kernel-ops sites.
Regression test pattern: snapshot /proc/self/fd before/after each
call, assert fcntl(fd, F_GETFD) & FD_CLOEXEC. Model from wolfSSL issue #9753 reproducer.
ipc-linux.h's parse_privkey, parse_pubkey, parse_psk
previously used mnl_attr_get_payload_len(attr) as both the allocation
size and the copy length without checking against WG_*_KEY_LEN. PR #20 adds exactly the symbolic size check I would have recommended. No
further action needed from this review.
Finding 3 — Minor: '\0' termination at wrong buffer offset
sizeof(privkey_base64) is WG_BASE64_LEN(WG_KEY_LEN_MAX) = 89, but
the read length is WG_BASE64_LEN(WG_PRIVATE_KEY_LEN) - 1 = 43.
Setting privkey_base64[88] = '\0' leaves indices 43..87 as
uninitialised stack memory.
Not a current bug (downstream uses explicit length), but misleading
and hazardous for future strlen/printf additions. Either delete
the line or write at the right offset:
If the caller passes inLen % 4 != 0, the remaining 1-3 bytes are
silently discarded. Return success is keyed only on the produced
output matching outLen. Input "AAAA" + "x" decodes to 3 bytes and
returns true for outLen == 3 — the trailing "x" is accepted.
Non-critical in normal config-file use (the tokenizer strips
whitespace first) but a fuzz-surface gap. Adding an if (inLen != 0 && inLen != 4) return false; tail check — or
rejecting non-multiple-of-4 up front — closes it.
Finding 5 — Minor: parse_public_key uses memset, siblings use memzero_explicit
Public keys aren't secret, so memset is functionally fine, but the
inconsistency invites accidental copy-paste from parse_public_key
into a future sensitive-buffer context where the compiler-optimised memset gets elided. One-line homogenisation:
Finding 6 — Minor: negative wolfCrypt error returned as CLI exit status
genkey.c:48, pubkey.c:56, others: ret can be a wolfCrypt error
(e.g. WC_KEY_SIZE_E = -181). Returned via main, the shell
truncates to the low 8 bits: -181 & 0xff = 75. Scripts testing $?
see an unexpected positive value.
Convention elsewhere in the tree is return 0 for success and return 1 for error. Normalise by returning ret ? 1 : 0; from the
cleanup/out: blocks.
Inherited from upstream wireguard-tools; fix ripples through several
files. Minor.
Self-review correction
PR #20 also fixes a bug this review missed: in kernel_generate_privkey, kernel_derive_pubkey, and kernel_generate_psk, the out: cleanup block called mnlg_socket_close(nlg) but did not set nlg = NULL; afterward.
Because each of these functions also has a goto try_again loop on EINTR, a second-pass failure where the retry's mnlg_socket_open()
itself fails leaves the stale closed pointer in nlg; any later
cleanup that re-checks if (nlg) would double-close.
I missed this because my pattern library only covered single-pass
cleanup, not the retry-loop variant where the cleanup block is
re-entered after goto try_again. Updated the review methodology to
cover it — see cpsource/code-review-cpsource commit fae0bf4:
"c.md: add Pattern 4 — retry loops that don't NULL after close/free",
plus a matching entry in the §3 Resource Management checklist.
make check is scan-build --maxloop 100 --view make wg-fips — a
Clang static-analyzer run, not a functional test suite. No
genkey→pubkey round-trip test, no base64 known-answer tests, no
CLOEXEC assertions. Given that the tree handles private-key material
at a kernel/user-space interface, at least a minimal behavioural
harness would pay for itself.
Proposal:
test-encoding — round-trip wg_{to,from}_{base64,hex} across all
key sizes, plus KATs from a reference base64 implementation.
test-genkey — wg-fips genkey | wg-fips pubkey → assert output is
well-formed base64 of WG_BASE64_LEN(WG_PUBLIC_KEY_LEN) bytes, 100
iterations.
test-cloexec — the /proc/self/fd snapshot pattern referenced in
Finding 1.
If Finding 1 (CLOEXEC sweep) is agreeable, I'm happy to submit it
as a separate PR with the regression test described.
Findings 3–6 are small; they could be bundled into a single
cleanup PR if you'd prefer a single commit, or I can leave them
for the tree's regular cleanup cadence.
Full per-section walk-through (20-section C/C++ checklist output) is
in user-src/README-code-review-user-src.md in my local tree;
happy to push that to a branch if useful.
Code review of
user-src/— findings for discussionReviewed the full contents of
user-src/on current master, with thecode-review-cpsourceskill's 20-section C checklist. Five novelfindings plus one finding that's already being addressed by open
PR #20. Summary table first, then each finding, then a self-review
correction where PR #20 caught something I didn't.
Upstream status before reviewing
Queried
ghon 2026-04-23:@MarkAtwood) already addresses one of the findings (netlink
key-length validation).
CLOEXEC,SOCK_CLOEXEC,fopen,memzero,base64,fork exec,fd leak,parse_public_keyreturned norelevant PRs or issues. The other five findings below appear novel.
Findings
config.c,setconf.c,ipc-uapi-unix.h,netlink.h,ipc-openbsd.hO_CLOEXEC/SOCK_CLOEXECon fd-creating callsipc-linux.hpubkey.c'\0'termination at wrong buffer offsetencoding.cwg_from_base64silently accepts trailing non-aligned bytesconfig.cparse_public_keyusesmemset, siblings usememzero_explicitgenkey.c,pubkey.c, othersFinding 1 — Major: missing
O_CLOEXEC/SOCK_CLOEXECon fd-creating callsSame bug class as wolfSSL issue #9753
(where PR #10162 is
the upstream fix). In multithreaded hosts or library-embedding callers
that
fork()+exec(), these descriptors leak to children. Some carryprivate-key material.
config.cf = fopen(path, "r");— private/preshared key filesetconf.cconfig_input = fopen(argv[2], "r");— full configipc-uapi-unix.hsocket(AF_UNIX, SOCK_STREAM, 0)— UAPI socketnetlink.hsocket(AF_NETLINK, SOCK_RAW | flags, bus)—flags==0at everymnl_socket_opencalleripc-openbsd.hsocket(AF_INET, SOCK_DGRAM, 0)— OpenBSD ioctl socketnetlink.h:505is the high-leverage site: everymnlg_socket_open()inipc-linux.h(kernel_get_*,kernel_set_*,kernel_generate_privkey,kernel_derive_pubkey,kernel_generate_psk) routes through it withflags=0. One edit at themnl_socket_open(bus)wrapper (or the inner__mnl_socket_open) covers all five kernel-ops sites.Fix sketch:
Regression test pattern: snapshot
/proc/self/fdbefore/after eachcall, assert
fcntl(fd, F_GETFD) & FD_CLOEXEC. Model from wolfSSLissue #9753 reproducer.
Finding 2 — superseded by PR #20
ipc-linux.h'sparse_privkey,parse_pubkey,parse_pskpreviously used
mnl_attr_get_payload_len(attr)as both the allocationsize and the copy length without checking against
WG_*_KEY_LEN. PR#20 adds exactly the symbolic size check I would have recommended. No
further action needed from this review.
Finding 3 — Minor:
'\0'termination at wrong buffer offsetpubkey.c:39andpubkey.c:127:sizeof(privkey_base64)isWG_BASE64_LEN(WG_KEY_LEN_MAX) = 89, butthe read length is
WG_BASE64_LEN(WG_PRIVATE_KEY_LEN) - 1 = 43.Setting
privkey_base64[88] = '\0'leaves indices 43..87 asuninitialised stack memory.
Not a current bug (downstream uses explicit length), but misleading
and hazardous for future
strlen/printfadditions. Either deletethe line or write at the right offset:
Finding 4 — Minor:
wg_from_base64ignores trailing non-aligned bytesencoding.c:154, 198:If the caller passes
inLen % 4 != 0, the remaining 1-3 bytes aresilently discarded. Return success is keyed only on the produced
output matching
outLen. Input"AAAA" + "x"decodes to 3 bytes andreturns true for
outLen == 3— the trailing"x"is accepted.Non-critical in normal config-file use (the tokenizer strips
whitespace first) but a fuzz-surface gap. Adding an
if (inLen != 0 && inLen != 4) return false;tail check — orrejecting non-multiple-of-4 up front — closes it.
Finding 5 — Minor:
parse_public_keyusesmemset, siblings usememzero_explicitconfig.c:122vsconfig.c:112, 132:Public keys aren't secret, so
memsetis functionally fine, but theinconsistency invites accidental copy-paste from
parse_public_keyinto a future sensitive-buffer context where the compiler-optimised
memsetgets elided. One-line homogenisation:Finding 6 — Minor: negative wolfCrypt error returned as CLI exit status
genkey.c:48,pubkey.c:56, others:retcan be a wolfCrypt error(e.g.
WC_KEY_SIZE_E = -181). Returned viamain, the shelltruncates to the low 8 bits:
-181 & 0xff = 75. Scripts testing$?see an unexpected positive value.
Convention elsewhere in the tree is
return 0for success andreturn 1for error. Normalise by returningret ? 1 : 0;from thecleanup/out: blocks.
Inherited from upstream
wireguard-tools; fix ripples through severalfiles. Minor.
Self-review correction
PR #20 also fixes a bug this review missed: in
kernel_generate_privkey,kernel_derive_pubkey, andkernel_generate_psk, theout:cleanup block calledmnlg_socket_close(nlg)but did not setnlg = NULL;afterward.Because each of these functions also has a
goto try_againloop onEINTR, a second-pass failure where the retry'smnlg_socket_open()itself fails leaves the stale closed pointer in
nlg; any latercleanup that re-checks
if (nlg)would double-close.I missed this because my pattern library only covered single-pass
cleanup, not the retry-loop variant where the cleanup block is
re-entered after
goto try_again. Updated the review methodology tocover it — see
cpsource/code-review-cpsourcecommitfae0bf4:"c.md: add Pattern 4 — retry loops that don't NULL after close/free",
plus a matching entry in the §3 Resource Management checklist.
Credit to @MarkAtwood / PR #20 for catching it.
Test coverage observation
make checkisscan-build --maxloop 100 --view make wg-fips— aClang static-analyzer run, not a functional test suite. No
genkey→pubkey round-trip test, no base64 known-answer tests, no
CLOEXEC assertions. Given that the tree handles private-key material
at a kernel/user-space interface, at least a minimal behavioural
harness would pay for itself.
Proposal:
test-encoding— round-tripwg_{to,from}_{base64,hex}across allkey sizes, plus KATs from a reference base64 implementation.
test-genkey—wg-fips genkey | wg-fips pubkey→ assert output iswell-formed base64 of
WG_BASE64_LEN(WG_PUBLIC_KEY_LEN)bytes, 100iterations.
test-cloexec— the/proc/self/fdsnapshot pattern referenced inFinding 1.
Asks
and the retry-loop bug above.
as a separate PR with the regression test described.
cleanup PR if you'd prefer a single commit, or I can leave them
for the tree's regular cleanup cadence.
Full per-section walk-through (20-section C/C++ checklist output) is
in
user-src/README-code-review-user-src.mdin my local tree;happy to push that to a branch if useful.