@@ -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
0 commit comments