fix: ParseScpCommand leaks scpBasePathDynamic on repeated -t/-f - #1125
Open
MarkAtwood wants to merge 1 commit into
Open
fix: ParseScpCommand leaks scpBasePathDynamic on repeated -t/-f#1125MarkAtwood wants to merge 1 commit into
MarkAtwood wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a memory leak in ParseScpCommand() (SCP command parsing) where repeated -t/-f options could overwrite ssh->scpBasePathDynamic without freeing the prior allocation, causing per-connection lifetime leaks on peer-controlled commands.
Changes:
- Free
ssh->scpBasePathDynamicbefore re-allocating it in the-tparsing branch. - Free
ssh->scpBasePathDynamicbefore re-allocating it in the-fparsing branch.
Comments suppressed due to low confidence (2)
src/wolfscp.c:1526
- After freeing scpBasePathDynamic, ssh->scpBasePath can still point at the freed buffer. If the subsequent WMALLOC fails (or if the option has no trailing path and the later assignment isn’t hit), the session may retain a dangling scpBasePath pointer. Clear scpBasePath when freeing and set it to the newly allocated buffer immediately after allocation/memset so it always points at valid storage.
This issue also appears on line 1547 of the same file.
if (ssh->scpBasePathDynamic != NULL) {
WFREE(ssh->scpBasePathDynamic, ssh->ctx->heap,
DYNTYPE_BUFFER);
ssh->scpBasePathDynamic = NULL;
}
ssh->scpBasePathDynamic = (char*)WMALLOC(
ssh->scpBasePathSz,
ssh->ctx->heap, DYNTYPE_BUFFER);
if (ssh->scpBasePathDynamic == NULL) {
return WS_MEMORY_E;
}
WMEMSET(ssh->scpBasePathDynamic, 0, ssh->scpBasePathSz);
src/wolfscp.c:1558
- Same as the -t case: after freeing scpBasePathDynamic, ssh->scpBasePath may still reference freed memory. Clearing scpBasePath on free and setting it to the newly allocated buffer avoids leaving a dangling pointer behind if allocation fails or if later code paths don’t reassign scpBasePath.
if (ssh->scpBasePathDynamic != NULL) {
WFREE(ssh->scpBasePathDynamic, ssh->ctx->heap,
DYNTYPE_BUFFER);
ssh->scpBasePathDynamic = NULL;
}
ssh->scpBasePathDynamic = (char*)WMALLOC(
ssh->scpBasePathSz,
ssh->ctx->heap, DYNTYPE_BUFFER);
if (ssh->scpBasePathDynamic == NULL) {
return WS_MEMORY_E;
}
WMEMSET(ssh->scpBasePathDynamic, 0, ssh->scpBasePathSz);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
A peer command that repeats the direction option (scp -t a -t b, scp -f a -f b) re-enters the case and overwrites scpBasePathDynamic, leaking the earlier buffer for the connection lifetime. Free and clear the old buffer before each reallocation, and keep scpBasePathSz in lockstep with the pointer the way wolfSSH_free() already does. scpBasePath aliases that buffer and is only reassigned when the option carries a path, so clear it alongside the free; otherwise a later pathless -t/-f leaves it dangling into freed memory that DoScpSource()/DoScpSink() hand to the scp callback. Clearing the alias exposes a pre-existing case: a direction option with no path parses to WS_SUCCESS with a NULL base path, which the callbacks then dereference. Reject that after the parse loop instead. Issue: F-6813
ejohnstown
force-pushed
the
fix/scp-basepath-leak
branch
from
July 28, 2026 22:24
1d52148 to
3ee43e0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
ParseScpCommand()(src/wolfscp.c) allocatesssh->scpBasePathDynamicin boththe
-tand-fcases and assigns straight to the struct member withoutfreeing a previous allocation. The option scan does not consume the copied
remainder of the command, so a peer command that repeats the direction option
(
scp -t a -t b,scp -f a -f b) re-enters the case, allocates again, andoverwrites the pointer, leaking the earlier buffer. The command string is
peer-controlled (
wolfSSH_GetSessionCommand). The single teardown free(internal.c) releases only the final pointer, so every prior allocation leaks
for the connection lifetime.
The cost is quadratic in the command length: both the allocation size
(
cmdSz + WOLFSSH_MAX_FILENAME) and the number of repeats scale with thepeer-supplied command. Driving
ParseScpCommand()withscpfollowed by Nrepetitions of
-f, peak RSS growth:One authenticated
execchannel request of ~30 KB costs the server ~195 MBheld for the connection lifetime.
Fix
Free and clear
ssh->scpBasePathDynamicbefore each (re)allocation.Three things fell out of review and are included:
ssh->scpBasePathaliases that buffer. It is a separateconst char*pointing at the same allocation, and it is only reassigned inside the
idx + 2 < cmdSzbranch -- that is, only when the option actually carries apath. Freeing without clearing the alias leaves it dangling, and
ParseScpCommand()still returnsWS_SUCCESSwith a validscpDirection,so the caller proceeds into
DoScpSource()/DoScpSink()and hands thedangling pointer to the scp callback. Confirmed under AddressSanitizer with
the command
scp -f /tmp -f:Note this does not reproduce on a normal build -- the allocator hands the
freed block straight back, so the two pointers compare equal and the read
silently succeeds. It needs a quarantining allocator to surface.
A direction option with no path leaves
scpBasePathNULL. This ispre-existing, not introduced here: on master,
scp -tandscp -falreadyreturn
WS_SUCCESSwith a NULL base path, which then reachesWCHDIR(ssh->fs, basePath)in the default recv callback and any applicationcallback registered through
wolfSSH_SetScpRecvCallback(). Clearing thealias widens the set of commands that reach it, so it is rejected after the
parse loop with
WS_SCP_CMD_E.This is a behaviour change beyond the leak. A bare
scp -torscp -fwith no path now fails where it previously returned success. That is
deliberate; a direction option with no path is a malformed scp command.
scpBasePathSzis kept in lockstep with the pointer, cleared on free andset only after the allocation succeeds, matching what
wolfSSH_free()(internal.c) already does.
Verification
--enable-allagainst wolfSSL master; compiles clean.make check: 10 PASS / 1 SKIP / 0 FAIL, includingscripts/scp.test.ParseScpCommand()directly: use-after-free reproducedbefore the alias clear, clean after.
scp -f /tmp -fnow yields a NULL basepath and
WS_SCP_CMD_E;scp -f /tmp -f /tmp2correctly ends on/tmp2;scp -f /tmpis unchanged.No test in
tests/currently coversParseScpCommand(). Adding awolfSSH_TestParseScpCommandwrapper alongside the existingwolfSSH_TestScp*helpers would close that gap, and is not done here.
Reported by static analysis (Fenrir finding F-6813).