From 97240ceaf5eba211912be67fda4c28fb270e0e30 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 12 Jun 2026 11:42:27 -0700 Subject: [PATCH 1/2] Cap open SFTP file handles per session An authenticated peer could open SFTP file handles without limit, growing ssh->fileList until allocation failed and making the linear handle lookup progressively more expensive. - Limit tracked file handles per session to WOLFSSH_MAX_SFTP_HANDLES (default 64), bounding memory and the linear-lookup DoS surface. - Check the cap in SFTP_FileHandleCapped() from both RecvOpen branches after the path is resolved and before the file is opened. The open flags derived from the request may carry O_CREAT and O_TRUNC, so refusing after the open would create or truncate the target on a request the peer is told failed. SFTP_AddFileHandle() keeps the check as a backstop. - Refuse with FTP_FAILURE "Too Many Open File Handles" so the peer can tell a resource limit from a server malfunction. - Add a regress test via wolfSSH_SFTP_TestFileHandleCount() covering the cap, the refusal status reply, and that a refused open leaves the target file untouched. --- src/wolfsftp.c | 47 +++++++++++++ tests/regress.c | 164 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 11 +++ 3 files changed, 222 insertions(+) diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 1783ffa43..6a75da663 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -416,6 +416,7 @@ static int SFTP_AddFileHandle(WOLFSSH* ssh, #endif const char* fileName, word32 id[2]); static int SFTP_RemoveFileHandle(WOLFSSH* ssh, word32 id[2]); +static int SFTP_FileHandleCapped(WOLFSSH* ssh); #endif /* !NO_WOLFSSH_SERVER */ /* Returns WS_SUCCESS if a server-side inbound SFTP message body of the given @@ -2348,6 +2349,7 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) char oer[] = "Open File Error"; char naf[] = "Not A File"; char per[] = "Permission denied"; + char tmf[] = "Too Many Open File Handles"; int handleStored = 0; if (ssh == NULL) { @@ -2386,6 +2388,16 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) goto cleanup; } + /* Check the cap before opening. The open flags may carry O_CREAT/O_TRUNC, + * so refusing after the fact would leave the file created or truncated on + * a request the peer is told failed. */ + if (SFTP_FileHandleCapped(ssh)) { + WLOG(WS_LOG_SFTP, "Too many open file handles for session"); + rc = SFTP_SendStatus(ssh, WOLFSSH_FTP_FAILURE, reqId, tmf); + ret = (rc == WS_SUCCESS) ? WS_BAD_FILE_E : rc; + goto cleanup; + } + /* get reason for opening file */ if (GetUint32(&reason, data, maxSz, &idx) != WS_SUCCESS) { ret = WS_BUFFER_E; @@ -2572,6 +2584,7 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) char ier[] = "Internal Failure"; char oer[] = "Open File Error"; char per[] = "Permission denied"; + char tmf[] = "Too Many Open File Handles"; int handleStored = 0; fileHandle = INVALID_HANDLE_VALUE; @@ -2606,6 +2619,16 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) goto cleanup; } + /* Check the cap before opening. The open flags may carry O_CREAT/O_TRUNC, + * so refusing after the fact would leave the file created or truncated on + * a request the peer is told failed. */ + if (SFTP_FileHandleCapped(ssh)) { + WLOG(WS_LOG_SFTP, "Too many open file handles for session"); + rc = SFTP_SendStatus(ssh, WOLFSSH_FTP_FAILURE, reqId, tmf); + ret = (rc == WS_SUCCESS) ? WS_BAD_FILE_E : rc; + goto cleanup; + } + /* get reason for opening file */ if (GetUint32(&reason, data, maxSz, &idx) != WS_SUCCESS) { ret = WS_BUFFER_E; @@ -4003,6 +4026,22 @@ int wolfSSH_SFTP_RecvCloseDir(WOLFSSH* ssh, byte* handle, word32 handleSz) #endif /* NO_WOLFSSH_DIR */ +/* returns 1 when the session already holds the maximum file handles */ +static int SFTP_FileHandleCapped(WOLFSSH* ssh) +{ + WS_FILE_LIST* cur; + word32 count = 0; + + for (cur = ssh->fileList; cur != NULL; cur = cur->next) { + if (++count >= WOLFSSH_MAX_SFTP_HANDLES) { + return 1; + } + } + + return 0; +} + + /* Add a file handle to the tracking list keeping track of open files * returns WS_SUCCESS on success */ static int SFTP_AddFileHandle(WOLFSSH* ssh, @@ -4021,6 +4060,14 @@ static int SFTP_AddFileHandle(WOLFSSH* ssh, return WS_BAD_ARGUMENT; } + /* Backstop only. RecvOpen checks the cap before opening the file, so a + * refused request leaves no O_CREAT/O_TRUNC side effect behind; reaching + * the cap here means a caller skipped that check. */ + if (SFTP_FileHandleCapped(ssh)) { + WLOG(WS_LOG_SFTP, "Too many open file handles for session"); + return WS_MEMORY_E; + } + cur = (WS_FILE_LIST*)WMALLOC(sizeof(WS_FILE_LIST), ssh->ctx->heap, DYNTYPE_SFTP); if (cur == NULL) { diff --git a/tests/regress.c b/tests/regress.c index 07d44cc16..1513959ac 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -3101,6 +3101,168 @@ static void TestSftpHandleNamespaceIsolation(void) } #endif /* NO_WOLFSSH_DIR */ +/* A refused request must still answer the peer with an FXP_STATUS carrying the + * expected code. Asserting only that the call returned non-success would not + * catch a refusal that dropped the reply and left the session hung. + * The request id is checked too: TestRecvReply returns whatever is currently + * buffered, so a handler that dropped its reply would otherwise pass here by + * re-presenting the previous request's status. */ +static void AssertSftpStatusReply(WOLFSSH* ssh, int reqId, word32 code) +{ + const byte* reply; + word32 replySz; + + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= WOLFSSH_SFTP_HEADER + UINT32_SZ); + AssertIntEQ(reply[LENGTH_SZ], WOLFSSH_FTP_STATUS); + AssertIntEQ((int)SftpGetU32(reply + LENGTH_SZ + MSG_ID_SZ), reqId); + AssertIntEQ((int)SftpGetU32(reply + WOLFSSH_SFTP_HEADER), (int)code); +} + +/* The per-session open-file-handle count is capped at WOLFSSH_MAX_SFTP_HANDLES + * to bound memory and keep the linear handle lookup from becoming a CPU DoS + * vector. Open exactly the cap's worth of handles (all must succeed), confirm + * the next open is refused, then close one and confirm a fresh open succeeds + * again -- proving the cap tracks the live count rather than latching shut. */ +static void TestSftpHandleLimit(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + int rid = 300; + int reqId; + int i; + word32 idx; + word32 replySz; + const byte* reply; + const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */ + byte handles[WOLFSSH_MAX_SFTP_HANDLES][WOLFSSH_HANDLE_ID_SZ]; + byte pkt[256]; + char cwd[WOLFSSH_MAX_FILENAME]; + char path[64]; + char victim[64]; + word32 pathSz; + FILE* vf; + long vsz; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS); + + /* unique per-process fixture names (see TestSftpForgedHandleRejected) */ + WSNPRINTF(path, sizeof(path), "wolfssh_limit_%d.tmp", (int)getpid()); + WSNPRINTF(victim, sizeof(victim), "wolfssh_limit_victim_%d.tmp", + (int)getpid()); + pathSz = (word32)WSTRLEN(path); + + WMEMSET(cwd, 0, sizeof(cwd)); + AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1)); + AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS); + + /* open the cap's worth of handles against one file; all must succeed */ + for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) { + idx = 0; + SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, pathSz); + idx += pathSz; + SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, + pkt + idx); idx += UINT32_SZ; + SftpPutU32(0, pkt + idx); idx += UINT32_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); + WMEMCPY(handles[i], reply + hOff, WOLFSSH_HANDLE_ID_SZ); + } + AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + + /* one past the cap must be refused, and must not grow the list */ + idx = 0; + SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, pathSz); + idx += pathSz; + SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, + pkt + idx); idx += UINT32_SZ; + SftpPutU32(0, pkt + idx); idx += UINT32_SZ; + reqId = rid++; + AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + /* the peer must be told, and told it was a failure rather than silence */ + AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); + + /* A refused open must leave the filesystem alone. The cap is checked + * before the open, so the O_CREAT|O_TRUNC a "put" carries must not reach + * the file -- otherwise the peer's data is destroyed by a request the + * server reported as failed. Seed a victim file, aim a truncating open at + * it while over the cap, and confirm the contents survive. */ + vf = fopen(victim, "wb"); + AssertNotNull(vf); + AssertIntEQ((int)fwrite("0123456789", 1, 10, vf), 10); + fclose(vf); + + idx = 0; + SftpPutU32((word32)WSTRLEN(victim), pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, victim, WSTRLEN(victim)); + idx += (word32)WSTRLEN(victim); + SftpPutU32(WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC, + pkt + idx); idx += UINT32_SZ; + SftpPutU32(0, pkt + idx); idx += UINT32_SZ; + reqId = rid++; + AssertTrue(wolfSSH_SFTP_RecvOpen(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); + + vf = fopen(victim, "rb"); + AssertNotNull(vf); + fseek(vf, 0, SEEK_END); + vsz = ftell(vf); + fclose(vf); + AssertIntEQ((int)vsz, 10); + (void)WREMOVE(ssh->fs, victim); + + /* free one slot; a fresh open must now succeed again */ + idx = 0; + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handles[0], WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES - 1); + + idx = 0; + SftpPutU32(pathSz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, pathSz); + idx += pathSz; + SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT, + pkt + idx); idx += UINT32_SZ; + SftpPutU32(0, pkt + idx); idx += UINT32_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS); + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); + WMEMCPY(handles[0], reply + hOff, WOLFSSH_HANDLE_ID_SZ); + AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + + /* close every handle and clean up */ + for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) { + idx = 0; + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handles[i], WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + } + AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), 0); + + (void)WREMOVE(ssh->fs, path); + wolfSSH_SFTP_TestRecvStateFree(ssh); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + /* A failed close() must still drop the handle from the session tracking list; * otherwise the stale descriptor lingers and is closed a second time when the * session is torn down. Open a file, invalidate its descriptor out of band so @@ -5619,6 +5781,8 @@ int main(int argc, char** argv) /* file and directory handle IDs share one namespace and never cross-close */ TestSftpHandleNamespaceIsolation(); #endif + /* open file handles are capped per session */ + TestSftpHandleLimit(); /* a failed close still drops the handle from the tracking list */ TestSftpCloseFailureRemovesHandle(); #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 267319053..e67a2ffed 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -893,6 +893,17 @@ WOLFSSH_LOCAL int wolfSSH_GetPath(const char* defaultPath, byte* in, #ifdef WOLFSSH_SFTP #define WOLFSSH_MAX_SFTPOFST 3 +/* Maximum number of open file handles tracked per session. Bounds memory use + * and keeps the linear handle lookup from becoming a CPU DoS vector. */ +#ifndef WOLFSSH_MAX_SFTP_HANDLES + #define WOLFSSH_MAX_SFTP_HANDLES 64 +#endif +/* The counts are unsigned, so a value below 1 would either cap at one handle + * or, for a negative value, never compare true and drop the cap entirely. */ +#if WOLFSSH_MAX_SFTP_HANDLES < 1 + #error "WOLFSSH_MAX_SFTP_HANDLES must be at least 1" +#endif + #ifndef NO_WOLFSSH_DIR typedef struct WS_DIR_LIST WS_DIR_LIST; #endif From f06b9f79d7435f05ddfc2dac427bf301859e9975 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 29 Jul 2026 10:51:52 -0700 Subject: [PATCH 2/2] Cap open SFTP directory handles per session Directory handles are tracked on ssh->dirList, a separate list from the file handles, so the SFTP_AddFileHandle cap does not reach them. A peer could loop on OPENDIR and grow that list until allocation failed. - Add SFTP_DirHandleCapped() and check it in both RecvOpenDir branches before the directory is opened or the name buffer allocated, so the rejection has nothing to unwind. Refusal sends an SFTP status like the existing permission-denied path rather than dropping the request. - Reuse WOLFSSH_MAX_SFTP_HANDLES, so each list is bounded separately. - Add wolfSSH_SFTP_TestDirHandleCount() and a regress test. --- src/wolfsftp.c | 49 +++++++++++++++++++++++ tests/regress.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 7 +++- wolfssh/wolfsftp.h | 3 ++ 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 6a75da663..1742b6030 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -2773,6 +2773,22 @@ struct WS_DIR_LIST { }; +/* returns 1 when the session already holds the maximum directory handles */ +static int SFTP_DirHandleCapped(WOLFSSH* ssh) +{ + WS_DIR_LIST* cur; + word32 count = 0; + + for (cur = ssh->dirList; cur != NULL; cur = cur->next) { + if (++count >= WOLFSSH_MAX_SFTP_HANDLES) { + return 1; + } + } + + return 0; +} + + /* Handles packet to open a directory * * returns WS_SUCCESS on success @@ -2792,6 +2808,7 @@ int wolfSSH_SFTP_RecvOpenDir(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) byte* out = NULL; byte idFlat[WOLFSSH_HANDLE_ID_SZ]; char per[] = "Permission denied"; + char tooMany[] = "Too Many Open Directory Handles"; if (ssh == NULL) { return WS_BAD_ARGUMENT; @@ -2819,6 +2836,13 @@ int wolfSSH_SFTP_RecvOpenDir(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) return WS_BUFFER_E; } + /* check the cap before opening, so there is nothing to unwind */ + if (SFTP_DirHandleCapped(ssh)) { + WLOG(WS_LOG_SFTP, "Too many open directory handles for session"); + rc = SFTP_SendStatus(ssh, WOLFSSH_FTP_FAILURE, reqId, tooMany); + return (rc == WS_SUCCESS) ? WS_BAD_FILE_E : rc; + } + if (WOPENDIR(ssh->fs, ssh->ctx->heap, &ctx, dir) != 0) { WLOG(WS_LOG_SFTP, "Error with opening directory: %s", dir); if (wolfSSH_SFTP_CreateStatus(ssh, WOLFSSH_FTP_NOFILE, reqId, @@ -2905,6 +2929,7 @@ int wolfSSH_SFTP_RecvOpenDir(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) char name[MAX_PATH]; char clean[WOLFSSH_MAX_FILENAME]; char per[] = "Permission denied"; + char tooMany[] = "Too Many Open Directory Handles"; if (ssh == NULL) { return WS_BAD_ARGUMENT; @@ -2943,6 +2968,13 @@ int wolfSSH_SFTP_RecvOpenDir(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) return WS_BUFFER_E; } + /* check the cap before allocating, so there is nothing to unwind */ + if (SFTP_DirHandleCapped(ssh)) { + WLOG(WS_LOG_SFTP, "Too many open directory handles for session"); + rc = SFTP_SendStatus(ssh, WOLFSSH_FTP_FAILURE, reqId, tooMany); + return (rc == WS_SUCCESS) ? WS_BAD_FILE_E : rc; + } + /* plus one to make sure is null terminated */ dirNameSz = (word32)WSTRLEN(clean) + 1; dirName = (char*)WMALLOC(dirNameSz, ssh->ctx->heap, DYNTYPE_BUFFER); @@ -6360,6 +6392,23 @@ int wolfSSH_SFTP_TestFileHandleCount(WOLFSSH* ssh) return count; } +#ifndef NO_WOLFSSH_DIR +/* Return the number of open directory handles tracked for the session. */ +int wolfSSH_SFTP_TestDirHandleCount(WOLFSSH* ssh) +{ + WS_DIR_LIST* cur; + int count = 0; + + if (ssh == NULL) { + return 0; + } + for (cur = ssh->dirList; cur != NULL; cur = cur->next) { + count++; + } + return count; +} +#endif /* NO_WOLFSSH_DIR */ + /* Close the underlying descriptor of the head tracked file handle out of band, * leaving the node in the list with a now-stale fd. The next RecvClose on that * handle will see its close() fail, exercising the path that must still drop diff --git a/tests/regress.c b/tests/regress.c index 1513959ac..ca80a4f59 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -3263,6 +3263,98 @@ static void TestSftpHandleLimit(void) wolfSSH_CTX_free(ctx); } +#ifndef NO_WOLFSSH_DIR +/* Directory handles live on ssh->dirList, a separate list from the file + * handles, so the SFTP_AddFileHandle cap does not apply to them. Without its + * own limit a peer could loop on OPENDIR and grow that list without bound. + * Same shape as TestSftpHandleLimit: fill to the cap, confirm the next OPENDIR + * is refused, close one and confirm a fresh OPENDIR succeeds again. */ +static void TestSftpDirHandleLimit(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + int rid = 400; + int reqId; + int i; + word32 idx; + word32 replySz; + const byte* reply; + const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */ + byte handles[WOLFSSH_MAX_SFTP_HANDLES][WOLFSSH_HANDLE_ID_SZ]; + byte pkt[256]; + char cwd[WOLFSSH_MAX_FILENAME]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS); + + WMEMSET(cwd, 0, sizeof(cwd)); + AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1)); + AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS); + + /* open the cap's worth of handles on "."; all must succeed */ + for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) { + idx = 0; + SftpPutU32(1, pkt + idx); idx += UINT32_SZ; + pkt[idx++] = '.'; + AssertIntEQ(wolfSSH_SFTP_RecvOpenDir(ssh, rid++, pkt, idx), WS_SUCCESS); + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); + WMEMCPY(handles[i], reply + hOff, WOLFSSH_HANDLE_ID_SZ); + } + AssertIntEQ(wolfSSH_SFTP_TestDirHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + + /* one past the cap must be refused, and must not grow the list */ + idx = 0; + SftpPutU32(1, pkt + idx); idx += UINT32_SZ; + pkt[idx++] = '.'; + reqId = rid++; + AssertTrue(wolfSSH_SFTP_RecvOpenDir(ssh, reqId, pkt, idx) != WS_SUCCESS); + AssertIntEQ(wolfSSH_SFTP_TestDirHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + /* the peer must be told, and told it was a failure rather than silence */ + AssertSftpStatusReply(ssh, reqId, WOLFSSH_FTP_FAILURE); + + /* free one slot; a fresh OPENDIR must now succeed again */ + idx = 0; + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handles[0], WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + AssertIntEQ(wolfSSH_SFTP_TestDirHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES - 1); + + idx = 0; + SftpPutU32(1, pkt + idx); idx += UINT32_SZ; + pkt[idx++] = '.'; + AssertIntEQ(wolfSSH_SFTP_RecvOpenDir(ssh, rid++, pkt, idx), WS_SUCCESS); + reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz); + AssertNotNull(reply); + AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ); + WMEMCPY(handles[0], reply + hOff, WOLFSSH_HANDLE_ID_SZ); + AssertIntEQ(wolfSSH_SFTP_TestDirHandleCount(ssh), + WOLFSSH_MAX_SFTP_HANDLES); + + /* close every handle and clean up */ + for (i = 0; i < WOLFSSH_MAX_SFTP_HANDLES; i++) { + idx = 0; + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handles[i], WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + } + AssertIntEQ(wolfSSH_SFTP_TestDirHandleCount(ssh), 0); + + wolfSSH_SFTP_TestRecvStateFree(ssh); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} +#endif /* NO_WOLFSSH_DIR */ + /* A failed close() must still drop the handle from the session tracking list; * otherwise the stale descriptor lingers and is closed a second time when the * session is torn down. Open a file, invalidate its descriptor out of band so @@ -5783,6 +5875,10 @@ int main(int argc, char** argv) #endif /* open file handles are capped per session */ TestSftpHandleLimit(); + #ifndef NO_WOLFSSH_DIR + /* open directory handles are capped per session */ + TestSftpDirHandleLimit(); + #endif /* a failed close still drops the handle from the tracking list */ TestSftpCloseFailureRemovesHandle(); #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index e67a2ffed..a685906fb 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -893,8 +893,11 @@ WOLFSSH_LOCAL int wolfSSH_GetPath(const char* defaultPath, byte* in, #ifdef WOLFSSH_SFTP #define WOLFSSH_MAX_SFTPOFST 3 -/* Maximum number of open file handles tracked per session. Bounds memory use - * and keeps the linear handle lookup from becoming a CPU DoS vector. */ +/* Maximum number of open handles tracked per session, applied separately to + * the file list and the directory list, so the worst case for one session is + * twice this value. Bounds memory use and keeps the linear handle lookup from + * becoming a CPU DoS vector. Must be at least 1; there is no "unlimited" + * setting, and 0 yields a cap of one handle per list. */ #ifndef WOLFSSH_MAX_SFTP_HANDLES #define WOLFSSH_MAX_SFTP_HANDLES 64 #endif diff --git a/wolfssh/wolfsftp.h b/wolfssh/wolfsftp.h index eb3b46621..72daf10e7 100644 --- a/wolfssh/wolfsftp.h +++ b/wolfssh/wolfsftp.h @@ -324,6 +324,9 @@ WOLFSSH_LOCAL void wolfSSH_SFTP_ShowSizes(void); word32* sz); WOLFSSH_API void wolfSSH_SFTP_TestRecvStateFree(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_SFTP_TestFileHandleCount(WOLFSSH* ssh); + #ifndef NO_WOLFSSH_DIR + WOLFSSH_API int wolfSSH_SFTP_TestDirHandleCount(WOLFSSH* ssh); + #endif WOLFSSH_API int wolfSSH_SFTP_TestInvalidateHeadFd(WOLFSSH* ssh); #endif #if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME)