Skip to content

Commit fe48ae0

Browse files
ejohnstownclaude
andcommitted
Cap open SFTP file handles per session
Limit the number of open file handles tracked per session to WOLFSSH_MAX_SFTP_HANDLES (default 64). This bounds memory use and keeps the linear handle lookup from becoming a CPU DoS vector when a client opens many handles. SFTP_AddFileHandle now refuses to add a handle past the cap. Add a regress test for the cap, reusing the wolfSSH_SFTP_TestFileHandleCount helper to observe the tracking list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b51dd6f commit fe48ae0

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

src/wolfsftp.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3904,11 +3904,20 @@ static int SFTP_AddFileHandle(WOLFSSH* ssh,
39043904
WS_FILE_LIST* cur = NULL;
39053905
char* fileNameCopy = NULL;
39063906
word32 fileNameSz;
3907+
word32 count = 0;
39073908

39083909
if (ssh == NULL || fileName == NULL) {
39093910
return WS_BAD_ARGUMENT;
39103911
}
39113912

3913+
/* enforce a per-session cap on the number of open file handles */
3914+
for (cur = ssh->fileList; cur != NULL; cur = cur->next) {
3915+
if (++count >= WOLFSSH_MAX_SFTP_HANDLES) {
3916+
WLOG(WS_LOG_SFTP, "Too many open file handles for session");
3917+
return WS_MEMORY_E;
3918+
}
3919+
}
3920+
39123921
cur = (WS_FILE_LIST*)WMALLOC(sizeof(WS_FILE_LIST), ssh->ctx->heap,
39133922
DYNTYPE_SFTP);
39143923
if (cur == NULL) {

tests/regress.c

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,6 +2238,106 @@ static void TestSftpHandleNamespaceIsolation(void)
22382238
}
22392239
#endif /* NO_WOLFSSH_DIR */
22402240

2241+
/* The per-session open-file-handle count is capped at WOLFSSH_MAX_SFTP_HANDLES
2242+
* to bound memory and keep the linear handle lookup from becoming a CPU DoS
2243+
* vector. Open exactly the cap's worth of handles (all must succeed), confirm
2244+
* the next open is refused, then close one and confirm a fresh open succeeds
2245+
* again -- proving the cap tracks the live count rather than latching shut. */
2246+
static void TestSftpHandleLimit(void)
2247+
{
2248+
WOLFSSH_CTX* ctx;
2249+
WOLFSSH* ssh;
2250+
int rid = 300;
2251+
int i;
2252+
word32 idx;
2253+
word32 replySz;
2254+
const byte* reply;
2255+
const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */
2256+
byte handles[WOLFSSH_MAX_SFTP_HANDLES][WOLFSSH_HANDLE_ID_SZ];
2257+
byte pkt[256];
2258+
char cwd[WOLFSSH_MAX_FILENAME];
2259+
const char path[] = "wolfssh_limit.tmp";
2260+
2261+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
2262+
AssertNotNull(ctx);
2263+
ssh = wolfSSH_new(ctx);
2264+
AssertNotNull(ssh);
2265+
AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS);
2266+
2267+
WMEMSET(cwd, 0, sizeof(cwd));
2268+
AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1));
2269+
AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS);
2270+
2271+
/* open the cap's worth of handles against one file; all must succeed */
2272+
for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) {
2273+
idx = 0;
2274+
SftpPutU32((word32)(sizeof(path) - 1), pkt + idx); idx += UINT32_SZ;
2275+
WMEMCPY(pkt + idx, path, sizeof(path) - 1);
2276+
idx += (word32)(sizeof(path) - 1);
2277+
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT,
2278+
pkt + idx); idx += UINT32_SZ;
2279+
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
2280+
AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS);
2281+
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
2282+
AssertNotNull(reply);
2283+
AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ);
2284+
WMEMCPY(handles[i], reply + hOff, WOLFSSH_HANDLE_ID_SZ);
2285+
}
2286+
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh),
2287+
WOLFSSH_MAX_SFTP_HANDLES);
2288+
2289+
/* one past the cap must be refused, and must not grow the list */
2290+
idx = 0;
2291+
SftpPutU32((word32)(sizeof(path) - 1), pkt + idx); idx += UINT32_SZ;
2292+
WMEMCPY(pkt + idx, path, sizeof(path) - 1);
2293+
idx += (word32)(sizeof(path) - 1);
2294+
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT,
2295+
pkt + idx); idx += UINT32_SZ;
2296+
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
2297+
AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx) != WS_SUCCESS);
2298+
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh),
2299+
WOLFSSH_MAX_SFTP_HANDLES);
2300+
2301+
/* free one slot; a fresh open must now succeed again */
2302+
idx = 0;
2303+
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
2304+
WMEMCPY(pkt + idx, handles[0], WOLFSSH_HANDLE_ID_SZ);
2305+
idx += WOLFSSH_HANDLE_ID_SZ;
2306+
AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS);
2307+
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh),
2308+
WOLFSSH_MAX_SFTP_HANDLES - 1);
2309+
2310+
idx = 0;
2311+
SftpPutU32((word32)(sizeof(path) - 1), pkt + idx); idx += UINT32_SZ;
2312+
WMEMCPY(pkt + idx, path, sizeof(path) - 1);
2313+
idx += (word32)(sizeof(path) - 1);
2314+
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT,
2315+
pkt + idx); idx += UINT32_SZ;
2316+
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
2317+
AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS);
2318+
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
2319+
AssertNotNull(reply);
2320+
AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ);
2321+
WMEMCPY(handles[0], reply + hOff, WOLFSSH_HANDLE_ID_SZ);
2322+
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh),
2323+
WOLFSSH_MAX_SFTP_HANDLES);
2324+
2325+
/* close every handle and clean up */
2326+
for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) {
2327+
idx = 0;
2328+
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
2329+
WMEMCPY(pkt + idx, handles[i], WOLFSSH_HANDLE_ID_SZ);
2330+
idx += WOLFSSH_HANDLE_ID_SZ;
2331+
AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS);
2332+
}
2333+
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), 0);
2334+
2335+
(void)WREMOVE(ssh->fs, path);
2336+
wolfSSH_SFTP_TestRecvStateFree(ssh);
2337+
wolfSSH_free(ssh);
2338+
wolfSSH_CTX_free(ctx);
2339+
}
2340+
22412341
/* A failed close() must still drop the handle from the session tracking list;
22422342
* otherwise the stale descriptor lingers and is closed a second time when the
22432343
* session is torn down. Open a file, invalidate its descriptor out of band so
@@ -2304,7 +2404,6 @@ static void TestSftpCloseFailureRemovesHandle(void)
23042404
wolfSSH_free(ssh);
23052405
wolfSSH_CTX_free(ctx);
23062406
}
2307-
23082407
#endif /* !NO_WOLFSSH_SERVER && !USE_WINDOWS_API && !NO_FILESYSTEM */
23092408

23102409
#if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME)
@@ -4019,6 +4118,8 @@ int main(int argc, char** argv)
40194118
/* file and directory handle IDs share one namespace and never cross-close */
40204119
TestSftpHandleNamespaceIsolation();
40214120
#endif
4121+
/* open file handles are capped per session */
4122+
TestSftpHandleLimit();
40224123
/* a failed close still drops the handle from the tracking list */
40234124
TestSftpCloseFailureRemovesHandle();
40244125
#endif

wolfssh/internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,12 @@ WOLFSSH_LOCAL int wolfSSH_GetPath(const char* defaultPath, byte* in,
714714
#define WOLFSSH_MAX_SFTPOFST 3
715715
#define WOLFSSH_HANDLE_ID_SZ (sizeof(word32) * 2)
716716

717+
/* Maximum number of open file handles tracked per session. Bounds memory use
718+
* and keeps the linear handle lookup from becoming a CPU DoS vector. */
719+
#ifndef WOLFSSH_MAX_SFTP_HANDLES
720+
#define WOLFSSH_MAX_SFTP_HANDLES 64
721+
#endif
722+
717723
#ifndef NO_WOLFSSH_DIR
718724
typedef struct WS_DIR_LIST WS_DIR_LIST;
719725
#endif

0 commit comments

Comments
 (0)