Skip to content

Commit 034fe9d

Browse files
Bind SCP file timestamps to open descriptor
1 parent 36e2765 commit 034fe9d

4 files changed

Lines changed: 195 additions & 11 deletions

File tree

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ AC_ARG_WITH(wolfssl,
8787
)
8888

8989
AC_CHECK_LIB([wolfssl],[wolfCrypt_Init],,[AC_MSG_ERROR([libwolfssl is required for ${PACKAGE}. It can be obtained from https://www.wolfssl.com/download.html/ .])])
90-
AC_CHECK_FUNCS([gethostbyname getaddrinfo gettimeofday inet_ntoa memset socket wc_ecc_set_rng])
90+
AC_CHECK_FUNCS([gethostbyname getaddrinfo gettimeofday inet_ntoa memset socket wc_ecc_set_rng futimes])
9191
AC_CHECK_DECLS([[pread],[pwrite]],,[unistd.h])
9292

9393
# DEBUG

src/wolfscp.c

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,14 +1857,32 @@ static INLINE int wolfSSH_LastError(void)
18571857
}
18581858

18591859

1860+
/* WOLFSSH_SCP_FD_UTIMES is defined for platforms that can set file timestamps
1861+
* on the still-open descriptor, binding the update to the inode. On those the
1862+
* timestamp is applied before the file is closed. Other platforms fall back to
1863+
* applying the timestamp by path after the file is closed. */
1864+
#if defined(USE_WINDOWS_API) || defined(WFUTIMES)
1865+
#define WOLFSSH_SCP_FD_UTIMES
1866+
#endif
1867+
18601868
/* set file access and modification times
1869+
*
1870+
* On descriptor-capable platforms (fp != NULL and WOLFSSH_SCP_FD_UTIMES) the
1871+
* timestamps are applied to the open descriptor so the update is bound to the
1872+
* inode. This avoids a race where the path could be replaced by a symlink
1873+
* between closing the file and a path-based time update, which would let a
1874+
* peer-supplied timestamp be applied to an arbitrary target. Buffered file
1875+
* data is flushed first so the later close does not write and overwrite the
1876+
* modification time. When fp is NULL the file has already been closed and the
1877+
* timestamp is applied by path.
1878+
*
18611879
* Returns WS_SUCCESS on success, or negative upon error */
1862-
static int SetTimestampInfo(const char* fileName, word64 mTime, word64 aTime)
1880+
static int SetTimestampInfo(WFILE* fp, const char* fileName,
1881+
word64 mTime, word64 aTime)
18631882
{
18641883
int ret = WS_SUCCESS;
18651884
#ifdef USE_WINDOWS_API
18661885
struct _utimbuf tmp;
1867-
int fd;
18681886
#else
18691887
struct timeval tmp[2];
18701888
#endif
@@ -1874,18 +1892,33 @@ static int SetTimestampInfo(const char* fileName, word64 mTime, word64 aTime)
18741892

18751893
if (ret == WS_SUCCESS) {
18761894
#ifdef USE_WINDOWS_API
1877-
tmp.actime = aTime;
1878-
tmp.modtime = mTime;
1879-
_sopen_s(&fd, fileName, _O_RDWR, _SH_DENYNO, 0);
1880-
_futime(fd, &tmp);
1881-
_close(fd);
1895+
if (fp == NULL) {
1896+
ret = WS_BAD_ARGUMENT;
1897+
}
1898+
else {
1899+
tmp.actime = aTime;
1900+
tmp.modtime = mTime;
1901+
if (WFFLUSH(fp) != 0 || _futime(_fileno(fp), &tmp) != 0)
1902+
ret = WS_FATAL_ERROR;
1903+
}
18821904
#else
18831905
tmp[0].tv_sec = (time_t)aTime;
18841906
tmp[0].tv_usec = 0;
18851907
tmp[1].tv_sec = (time_t)mTime;
18861908
tmp[1].tv_usec = 0;
18871909

1888-
ret = WUTIMES(fileName, tmp);
1910+
#ifdef WFUTIMES
1911+
if (fp != NULL) {
1912+
if (WFFLUSH(fp) != 0 || WFUTIMES(fileno(fp), tmp) != 0)
1913+
ret = WS_FATAL_ERROR;
1914+
}
1915+
else
1916+
#endif
1917+
{
1918+
(void)fp;
1919+
if (WUTIMES(fileName, tmp) != 0)
1920+
ret = WS_FATAL_ERROR;
1921+
}
18891922
#endif
18901923
}
18911924

@@ -2093,15 +2126,30 @@ int wsScpRecvCallback(WOLFSSH* ssh, int state, const char* basePath,
20932126
(void)WFFLUSH(fp);
20942127
(void)fsync(fileno(fp));
20952128
flush_bytes = 0;
2129+
#endif
2130+
#ifdef WOLFSSH_SCP_FD_UTIMES
2131+
/* set timestamp info on the open file, before closing, so the
2132+
* update is bound to the inode and cannot be redirected to a
2133+
* symlink swapped in over the path */
2134+
if (mTime != 0 || aTime != 0)
2135+
ret = SetTimestampInfo(fp, fileName, mTime, aTime);
20962136
#endif
20972137
WFCLOSE(ssh->fs, fp);
20982138
fp = NULL;
20992139
}
21002140

21012141
/* set timestamp info */
21022142
if (mTime != 0 || aTime != 0) {
2103-
ret = SetTimestampInfo(fileName, mTime, aTime);
2104-
2143+
#ifndef WOLFSSH_SCP_FD_UTIMES
2144+
/* no descriptor-based update available, set by path now that
2145+
* the file is closed so the close cannot overwrite the time */
2146+
ret = SetTimestampInfo(NULL, fileName, mTime, aTime);
2147+
#else
2148+
/* on descriptor-based platforms the timestamp was applied
2149+
* above while the file was open; if fp was NULL here (file
2150+
* never opened) we intentionally abort rather than fall back
2151+
* to a path-based update that could follow a swapped symlink */
2152+
#endif
21052153
if (ret == WS_SUCCESS) {
21062154
ret = WS_SCP_CONTINUE;
21072155
} else {

tests/unit.c

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3932,6 +3932,132 @@ static int test_ScpRecvCallback_NewDirChdirFail(void)
39323932
return result;
39333933
}
39343934

3935+
/* Drive the default SCP receive callback through a full single-file receive
3936+
* and confirm the peer-supplied modification/access times end up on the
3937+
* written file.
3938+
*
3939+
* Which code path applies the timestamp is decided at build time, not by this
3940+
* test: when futimes is detected (HAVE_FUTIMES) the callback sets it on the
3941+
* open descriptor before the closing flush, otherwise it sets it by path after
3942+
* close. Both must yield the peer-supplied times, which is what this end-to-end
3943+
* test asserts. When the descriptor path is compiled in, this also regresses
3944+
* its ordering relative to the closing flush: applying the timestamp before the
3945+
* buffered data was flushed would leave the modification time at the current
3946+
* time rather than the peer value. */
3947+
static int test_ScpRecvCallback_Timestamp(void)
3948+
{
3949+
char tmpDir[] = "/tmp/wolfssh_scptsXXXXXX";
3950+
char filePath[PATH_MAX];
3951+
char origCwd[PATH_MAX];
3952+
const char data[] = "wolfssh scp timestamp regression\n";
3953+
const word64 mTime = 1234567890; /* 2009-02-13 23:31:30 UTC */
3954+
const word64 aTime = 1000000000; /* 2001-09-09 01:46:40 UTC */
3955+
struct stat st;
3956+
WOLFSSH_CTX* ctx = NULL;
3957+
WOLFSSH* ssh = NULL;
3958+
char* basePath = NULL;
3959+
int origCwdSaved = 0;
3960+
int baseReady = 0;
3961+
int ret;
3962+
int result = 0;
3963+
3964+
filePath[0] = '\0';
3965+
3966+
if (getcwd(origCwd, sizeof(origCwd)) == NULL)
3967+
return -850;
3968+
origCwdSaved = 1;
3969+
3970+
if (mkdtemp(tmpDir) == NULL)
3971+
return -851;
3972+
3973+
basePath = realpath(tmpDir, NULL);
3974+
if (basePath == NULL) {
3975+
result = -852;
3976+
goto cleanup;
3977+
}
3978+
baseReady = 1;
3979+
3980+
ret = snprintf(filePath, sizeof(filePath), "%s/ts_file.txt", basePath);
3981+
if (!scpTestSnprintfOk(ret, sizeof(filePath))) {
3982+
result = -853;
3983+
goto cleanup;
3984+
}
3985+
3986+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
3987+
if (ctx == NULL) {
3988+
result = -854;
3989+
goto cleanup;
3990+
}
3991+
ssh = wolfSSH_new(ctx);
3992+
if (ssh == NULL) {
3993+
result = -855;
3994+
goto cleanup;
3995+
}
3996+
3997+
/* enter the destination directory */
3998+
ret = wsScpRecvCallback(ssh, WOLFSSH_SCP_NEW_REQUEST, basePath,
3999+
NULL, 0, 0, 0, 0, NULL, 0, 0, NULL);
4000+
if (ret != WS_SCP_CONTINUE) {
4001+
result = -856;
4002+
goto cleanup;
4003+
}
4004+
4005+
/* open the destination file */
4006+
ret = wsScpRecvCallback(ssh, WOLFSSH_SCP_NEW_FILE, basePath,
4007+
"ts_file.txt", 0644, mTime, aTime, sizeof(data) - 1, NULL, 0, 0,
4008+
NULL);
4009+
if (ret != WS_SCP_CONTINUE) {
4010+
result = -857;
4011+
goto cleanup;
4012+
}
4013+
4014+
/* write the file contents */
4015+
ret = wsScpRecvCallback(ssh, WOLFSSH_SCP_FILE_PART, basePath,
4016+
"ts_file.txt", 0644, mTime, aTime, sizeof(data) - 1,
4017+
(byte*)data, sizeof(data) - 1, 0, wolfSSH_GetScpRecvCtx(ssh));
4018+
if (ret != WS_SCP_CONTINUE) {
4019+
result = -858;
4020+
goto cleanup;
4021+
}
4022+
4023+
/* close the file and apply the peer-supplied timestamps */
4024+
ret = wsScpRecvCallback(ssh, WOLFSSH_SCP_FILE_DONE, basePath,
4025+
"ts_file.txt", 0644, mTime, aTime, sizeof(data) - 1, NULL, 0, 0,
4026+
wolfSSH_GetScpRecvCtx(ssh));
4027+
if (ret != WS_SCP_CONTINUE) {
4028+
result = -859;
4029+
goto cleanup;
4030+
}
4031+
4032+
/* the written file must carry the peer-supplied timestamps */
4033+
if (stat(filePath, &st) != 0) {
4034+
result = -860;
4035+
goto cleanup;
4036+
}
4037+
if ((word64)st.st_mtime != mTime) {
4038+
result = -861;
4039+
goto cleanup;
4040+
}
4041+
if ((word64)st.st_atime != aTime) {
4042+
result = -862;
4043+
goto cleanup;
4044+
}
4045+
4046+
cleanup:
4047+
if (ssh != NULL)
4048+
wolfSSH_free(ssh);
4049+
if (ctx != NULL)
4050+
wolfSSH_CTX_free(ctx);
4051+
if (filePath[0] != '\0')
4052+
(void)remove(filePath);
4053+
free(basePath);
4054+
if (baseReady)
4055+
(void)rmdir(tmpDir);
4056+
if (origCwdSaved && chdir(origCwd) != 0 && result == 0)
4057+
result = -863;
4058+
return result;
4059+
}
4060+
39354061
#endif /* WOLFSSH_SCP recv callback depth guard test */
39364062

39374063

@@ -4552,6 +4678,11 @@ int wolfSSH_UnitTest(int argc, char** argv)
45524678
printf("ScpRecvCallback_NewDirChdirFail: %s\n",
45534679
(unitResult == 0 ? "SUCCESS" : "FAILED"));
45544680
testResult = testResult || unitResult;
4681+
4682+
unitResult = test_ScpRecvCallback_Timestamp();
4683+
printf("ScpRecvCallback_Timestamp: %s\n",
4684+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
4685+
testResult = testResult || unitResult;
45554686
#endif
45564687

45574688
#ifdef WOLFSSH_TEST_CAPTURING_ALLOCATOR

wolfssh/port.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ extern "C" {
461461
#include <sys/utime.h>
462462
#else
463463
#define WUTIMES(f,t) utimes((f),(t))
464+
/* futimes() is a BSD extension, not POSIX; only use it when the build
465+
* detected it, otherwise fall back to the path-based WUTIMES */
466+
#ifdef HAVE_FUTIMES
467+
#define WFUTIMES(fd,t) futimes((fd),(t))
468+
#endif
464469
#endif
465470

466471
#ifndef USE_WINDOWS_API

0 commit comments

Comments
 (0)