diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 1783ffa43..1742b6030 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; @@ -2750,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 @@ -2769,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; @@ -2796,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, @@ -2882,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; @@ -2920,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); @@ -4003,6 +4058,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 +4092,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) { @@ -6313,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 07d44cc16..ca80a4f59 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -3101,6 +3101,260 @@ 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); +} + +#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 @@ -5619,6 +5873,12 @@ 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(); + #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 267319053..a685906fb 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -893,6 +893,20 @@ WOLFSSH_LOCAL int wolfSSH_GetPath(const char* defaultPath, byte* in, #ifdef WOLFSSH_SFTP #define WOLFSSH_MAX_SFTPOFST 3 +/* 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 +/* 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 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)