@@ -1898,6 +1898,196 @@ static int test_OpenSecureFile(void)
18981898
18991899 return ret ;
19001900}
1901+
1902+ /* load a config whose PidFile is 'pidTarget' and run wolfSSHD_ConfigSavePID
1903+ * under umask(0), restoring the umask afterward so later tests are unaffected */
1904+ static int pidSave (const char * confPath , const char * pidTarget )
1905+ {
1906+ int ret ;
1907+ mode_t old ;
1908+ WOLFSSHD_CONFIG * cfg ;
1909+ FILE * f ;
1910+
1911+ f = fopen (confPath , "w" );
1912+ if (f == NULL ) {
1913+ return WS_FATAL_ERROR ;
1914+ }
1915+ fprintf (f , "PidFile %s\n" , pidTarget );
1916+ fclose (f );
1917+
1918+ cfg = wolfSSHD_ConfigNew (NULL );
1919+ if (cfg == NULL ) {
1920+ return WS_MEMORY_E ;
1921+ }
1922+ ret = wolfSSHD_ConfigLoad (cfg , confPath );
1923+ if (ret == WS_SUCCESS ) {
1924+ /* deliberate worst case: prove the explicit 0644 mode holds even when
1925+ * the umask would not mask any bits */
1926+ old = umask (0 );
1927+ wolfSSHD_ConfigSavePID (cfg );
1928+ umask (old );
1929+ }
1930+ wolfSSHD_ConfigFree (cfg );
1931+ return ret ;
1932+ }
1933+
1934+ /* wolfSSHD_ConfigSavePID must refuse a symlink at the PID path so a planted
1935+ * link cannot truncate another file, and must create the PID file without
1936+ * group or world write even under a permissive umask. */
1937+ static int test_ConfigSavePID (void )
1938+ {
1939+ int ret = WS_SUCCESS ;
1940+ int rd ;
1941+ long pid = -1 ;
1942+ char base [] = "/tmp/wolfsshd_pidXXXXXX" ;
1943+ char conf [80 ] = "" ;
1944+ char pidPath [96 ] = "" ;
1945+ char victim [96 ] = "" ;
1946+ char linkPath [96 ] = "" ;
1947+ char hvictim [96 ] = "" ;
1948+ char hlink [96 ] = "" ;
1949+ char fifo [96 ] = "" ;
1950+ const char * secret = "VICTIM-CONTENTS\n" ;
1951+ FILE * f = NULL ;
1952+ struct stat st ;
1953+ char rbuf [64 ];
1954+
1955+ if (mkdtemp (base ) == NULL ) {
1956+ Log (" mkdtemp failed.\n" );
1957+ ret = WS_FATAL_ERROR ;
1958+ }
1959+
1960+ if (ret == WS_SUCCESS ) {
1961+ snprintf (conf , sizeof (conf ), "%s/sshd_config" , base );
1962+ snprintf (pidPath , sizeof (pidPath ), "%s/wolfsshd.pid" , base );
1963+ snprintf (victim , sizeof (victim ), "%s/victim" , base );
1964+ snprintf (linkPath , sizeof (linkPath ), "%s/link.pid" , base );
1965+ snprintf (hvictim , sizeof (hvictim ), "%s/hvictim" , base );
1966+ snprintf (hlink , sizeof (hlink ), "%s/hlink.pid" , base );
1967+ snprintf (fifo , sizeof (fifo ), "%s/fifo.pid" , base );
1968+ }
1969+
1970+ /* Scenario 1: a normal path is written with our PID and is not group or
1971+ * world writable despite umask(0). */
1972+ if (ret == WS_SUCCESS ) {
1973+ ret = pidSave (conf , pidPath );
1974+ }
1975+ if (ret == WS_SUCCESS ) {
1976+ if (stat (pidPath , & st ) != 0 ) {
1977+ ret = WS_FATAL_ERROR ;
1978+ }
1979+ else {
1980+ f = fopen (pidPath , "r" );
1981+ rd = (f != NULL ) ? fscanf (f , "%ld" , & pid ) : 0 ;
1982+ if (f != NULL ) {
1983+ fclose (f );
1984+ }
1985+ ret = smExpect ("normal PID file written with our PID" ,
1986+ (rd == 1 && pid == (long )getpid ()) ? WS_SUCCESS
1987+ : WS_FATAL_ERROR , 1 );
1988+ if (ret == WS_SUCCESS ) {
1989+ ret = smExpect ("PID file not group or world writable" ,
1990+ (st .st_mode & (S_IWGRP | S_IWOTH )) ? WS_FATAL_ERROR
1991+ : WS_SUCCESS , 1 );
1992+ }
1993+ }
1994+ }
1995+
1996+ /* Scenario 2: a symlink at the PID path is refused, link target untouched.
1997+ * The common build exercises the atomic O_NOFOLLOW path; the lstat fallback
1998+ * needs a symlink-capable platform without O_NOFOLLOW. */
1999+ if (ret == WS_SUCCESS ) {
2000+ f = fopen (victim , "w" );
2001+ if (f == NULL ) {
2002+ ret = WS_FATAL_ERROR ;
2003+ }
2004+ else {
2005+ fputs (secret , f );
2006+ fclose (f );
2007+ }
2008+ }
2009+ if (ret == WS_SUCCESS && symlink (victim , linkPath ) != 0 ) {
2010+ ret = WS_FATAL_ERROR ;
2011+ }
2012+ if (ret == WS_SUCCESS ) {
2013+ ret = pidSave (conf , linkPath );
2014+ }
2015+ if (ret == WS_SUCCESS ) {
2016+ f = fopen (victim , "r" );
2017+ WMEMSET (rbuf , 0 , sizeof (rbuf ));
2018+ rd = (f != NULL ) ? (int )fread (rbuf , 1 , sizeof (rbuf ) - 1 , f ) : -1 ;
2019+ if (f != NULL ) {
2020+ fclose (f );
2021+ }
2022+ (void )rd ;
2023+ /* A WOLFSSH_NO_SYMLINK_CHECK build does no symlink check by design, so
2024+ * only assert the target survived when the check is compiled in. */
2025+ #ifdef WOLFSSH_HAVE_SYMLINK
2026+ ret = smExpect ("symlinked PID path not followed, target intact" ,
2027+ (WSTRCMP (rbuf , secret ) == 0 ) ? WS_SUCCESS : WS_FATAL_ERROR , 1 );
2028+ #else
2029+ (void )rbuf ;
2030+ #endif
2031+ }
2032+
2033+ /* Scenario 3: a hard link at the PID path is refused (O_NOFOLLOW stops a
2034+ * symlink but not a hard link), leaving the link target untouched. The
2035+ * st_nlink check that enforces this is unconditional, so this always runs. */
2036+ if (ret == WS_SUCCESS ) {
2037+ f = fopen (hvictim , "w" );
2038+ if (f == NULL ) {
2039+ ret = WS_FATAL_ERROR ;
2040+ }
2041+ else {
2042+ fputs (secret , f );
2043+ fclose (f );
2044+ }
2045+ }
2046+ if (ret == WS_SUCCESS && link (hvictim , hlink ) != 0 ) {
2047+ ret = WS_FATAL_ERROR ;
2048+ }
2049+ if (ret == WS_SUCCESS ) {
2050+ ret = pidSave (conf , hlink );
2051+ }
2052+ if (ret == WS_SUCCESS ) {
2053+ f = fopen (hvictim , "r" );
2054+ WMEMSET (rbuf , 0 , sizeof (rbuf ));
2055+ rd = (f != NULL ) ? (int )fread (rbuf , 1 , sizeof (rbuf ) - 1 , f ) : -1 ;
2056+ if (f != NULL ) {
2057+ fclose (f );
2058+ }
2059+ (void )rd ;
2060+ ret = smExpect ("hard-linked PID path refused, target intact" ,
2061+ (WSTRCMP (rbuf , secret ) == 0 ) ? WS_SUCCESS : WS_FATAL_ERROR , 1 );
2062+ }
2063+
2064+ /* Scenario 4: a FIFO at the PID path is rejected (O_NONBLOCK fast-fails the
2065+ * open and the S_ISREG check refuses it), and the FIFO is left in place
2066+ * rather than replaced by a regular PID file. */
2067+ if (ret == WS_SUCCESS && mkfifo (fifo , 0600 ) != 0 ) {
2068+ ret = WS_FATAL_ERROR ;
2069+ }
2070+ if (ret == WS_SUCCESS ) {
2071+ ret = pidSave (conf , fifo );
2072+ }
2073+ if (ret == WS_SUCCESS ) {
2074+ ret = smExpect ("FIFO PID path refused, still a FIFO" ,
2075+ (lstat (fifo , & st ) == 0 && S_ISFIFO (st .st_mode )) ? WS_SUCCESS
2076+ : WS_FATAL_ERROR , 1 );
2077+ }
2078+
2079+ /* cleanup */
2080+ unlink (fifo );
2081+ unlink (linkPath );
2082+ unlink (victim );
2083+ unlink (hlink );
2084+ unlink (hvictim );
2085+ unlink (pidPath );
2086+ unlink (conf );
2087+ rmdir (base );
2088+
2089+ return ret ;
2090+ }
19012091#endif /* !_WIN32 */
19022092
19032093const TEST_CASE testCases [] = {
@@ -1920,6 +2110,7 @@ const TEST_CASE testCases[] = {
19202110 TEST_DECL (test_ConfigFree ),
19212111#ifndef _WIN32
19222112 TEST_DECL (test_OpenSecureFile ),
2113+ TEST_DECL (test_ConfigSavePID ),
19232114#endif
19242115#ifdef WOLFSSL_BASE64_ENCODE
19252116 TEST_DECL (test_CheckAuthKeysLine ),
0 commit comments