diff --git a/src/port.c b/src/port.c index 1f39d1d2a..0758704bd 100644 --- a/src/port.c +++ b/src/port.c @@ -542,6 +542,44 @@ int WS_DeleteFileA(const char* fileName, void* heap) } + +#ifndef _WIN32_WCE + +int WS_ChmodA(const char* fileName, int mode, void* heap) +{ + int ret = -1; + wchar_t* unicodeFileName; + size_t unicodeFileNameSz = 0; + size_t returnSz = 0; + size_t fileNameSz = 0; + errno_t error; + + fileNameSz = WSTRLEN(fileName); + fileName = TrimFileName(fileName, &fileNameSz); + + error = mbstowcs_s(&unicodeFileNameSz, NULL, 0, fileName, 0); + if (error != 0) + return -1; + + unicodeFileName = (wchar_t*)WMALLOC((unicodeFileNameSz+1)*sizeof(wchar_t), + heap, PORT_DYNTYPE_STRING); + if (unicodeFileName == NULL) + return -1; + + error = mbstowcs_s(&returnSz, unicodeFileName, unicodeFileNameSz, + fileName, fileNameSz); + + if (error == 0) { + ret = _wchmod(unicodeFileName, mode); + } + + WFREE(unicodeFileName, heap, PORT_DYNTYPE_STRING); + + return ret; +} + +#endif /* !_WIN32_WCE */ + #endif /* USE_WINDOWS_API WOLFSSH_SFTP WOLFSSH_SCP */ #if !defined(NO_FILESYSTEM) && \ diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 2f1ca3215..1cb37d031 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -6046,7 +6046,7 @@ int wolfSSH_SFTP_RecvLSTAT(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) return ret; } -#if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_ZEPHYR) \ +#if !defined(_WIN32_WCE) && !defined(WOLFSSH_ZEPHYR) \ && !defined(WOLFSSH_SFTP_SETMODE) && !defined(WOLFSSH_FATFS) /* Set the files mode * return WS_SUCCESS on success */ @@ -6082,29 +6082,47 @@ static int SFTP_SetFileAttributes(WOLFSSH* ssh, char* name, WS_SFTP_FILEATRB* atr) { int ret = WS_SUCCESS; +#ifdef WTRUNCATE + word64 sz; +#endif /* check if size attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_SIZE) { - /* @TODO set file size */ + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_SIZE)) { +#ifdef WTRUNCATE + if (wResolveOffset(atr->sz, WOLFSSH_MAX_FILE_OFFSET, &sz) != 0 + || WTRUNCATE(ssh->fs, name, sz) != 0) { + ret = WS_BAD_FILE_E; + } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } /* check if uid and gid attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_UIDGID) { - /* @TODO set group and user id */ + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_UIDGID)) { +#ifdef WCHOWN + if (WCHOWN(ssh->fs, name, atr->uid, atr->gid) != 0) { + ret = WS_BAD_FILE_E; + } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } -#if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_ZEPHYR) /* check if permissions attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_PERM) { + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_PERM)) { ret = SFTP_SetMode(ssh->fs, name, WOLFSSH_SFTP_SAFE_MODE(atr->per)); } -#endif /* check if time attribute present */ if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_TIME)) { +#ifdef WSETTIME if (WSETTIME(ssh->fs, name, atr->atime, atr->mtime) != 0) { ret = WS_BAD_FILE_E; } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } /* check if extended attributes are present */ @@ -6128,29 +6146,52 @@ static int SFTP_SetFileAttributesHandle(WOLFSSH* ssh, WS_SFTP_FILEATRB* atr) { int ret = WS_SUCCESS; +#ifdef WFTRUNCATE + word64 sz; +#endif /* check if size attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_SIZE) { - /* @TODO set file size */ + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_SIZE)) { +#ifdef WFTRUNCATE + if (wResolveOffset(atr->sz, WOLFSSH_MAX_FILE_OFFSET, &sz) != 0 + || WFTRUNCATE(ssh->fs, handle, sz) != 0) { + ret = WS_BAD_FILE_E; + } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } /* check if uid and gid attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_UIDGID) { - /* @TODO set group and user id */ + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_UIDGID)) { +#ifdef WFCHOWN + if (WFCHOWN(ssh->fs, handle, atr->uid, atr->gid) != 0) { + ret = WS_BAD_FILE_E; + } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } -#ifndef USE_WINDOWS_API /* check if permissions attribute present */ - if (atr->flags & WOLFSSH_FILEATRB_PERM) { - ret = SFTP_SetModeHandle(ssh->fs, handle, WOLFSSH_SFTP_SAFE_MODE(atr->per)); - } + if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_PERM)) { +#ifndef USE_WINDOWS_API + ret = SFTP_SetModeHandle(ssh->fs, handle, + WOLFSSH_SFTP_SAFE_MODE(atr->per)); +#else + ret = WS_UNIMPLEMENTED_E; #endif + } /* check if time attribute present */ if (ret == WS_SUCCESS && (atr->flags & WOLFSSH_FILEATRB_TIME)) { +#ifdef WFSETTIME if (WFSETTIME(ssh->fs, handle, atr->atime, atr->mtime) != 0) { ret = WS_BAD_FILE_E; } +#else + ret = WS_UNIMPLEMENTED_E; +#endif } /* check if extended attributes are present */ @@ -6159,9 +6200,9 @@ static int SFTP_SetFileAttributesHandle(WOLFSSH* ssh, } (void)ssh; -#ifdef USE_WINDOWS_API - /* On Windows the only consumers (SFTP_SetModeHandle / WFSETTIME) are - * compiled out or no-ops, so the handle goes unused here. */ +#if defined(USE_WINDOWS_API) && !defined(WFTRUNCATE) && !defined(WFCHOWN) \ + && !defined(WFSETTIME) + /* no consumer of the handle is compiled in on this port */ (void)handle; #endif return ret ; @@ -6186,6 +6227,7 @@ int wolfSSH_SFTP_RecvSetSTAT(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) char ser[] = "Unable to set attributes error"; char per[] = "Unable to parse attributes error"; char pdn[] = "Permission denied"; + char uns[] = "Attribute not supported"; char* res = suc; byte type = WOLFSSH_FTP_OK; @@ -6224,8 +6266,14 @@ int wolfSSH_SFTP_RecvSetSTAT(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) != WS_SUCCESS) { /* tell peer that was not ok */ WLOG(WS_LOG_SFTP, "Unable to get set attributes of file/directory"); - type = WOLFSSH_FTP_FAILURE; - res = ser; + if (ret == WS_UNIMPLEMENTED_E) { + type = WOLFSSH_FTP_UNSUPPORTED; + res = uns; + } + else { + type = WOLFSSH_FTP_FAILURE; + res = ser; + } ret = WS_BAD_FILE_E; } @@ -6261,6 +6309,7 @@ int wolfSSH_SFTP_RecvFSetSTAT(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) char suc[] = "Set Attributes"; char ser[] = "Unable to set attributes error"; char per[] = "Unable to parse attributes error"; + char uns[] = "Attribute not supported"; char* res = suc; byte type = WOLFSSH_FTP_OK; @@ -6315,8 +6364,14 @@ int wolfSSH_SFTP_RecvFSetSTAT(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) != WS_SUCCESS) { /* tell peer that was not ok */ WLOG(WS_LOG_SFTP, "Unable to get set attributes of open file"); - type = WOLFSSH_FTP_FAILURE; - res = ser; + if (ret == WS_UNIMPLEMENTED_E) { + type = WOLFSSH_FTP_UNSUPPORTED; + res = uns; + } + else { + type = WOLFSSH_FTP_FAILURE; + res = ser; + } ret = WS_BAD_FILE_E; } @@ -7665,7 +7720,8 @@ int wolfSSH_SFTP_CHMOD(WOLFSSH* ssh, char* n, char* oct) break; } - /* update permissions */ + /* only the permissions change here */ + state->atr.flags = WOLFSSH_FILEATRB_PERM; state->atr.per = mode; state->state = STATE_CHMOD_SEND; FALL_THROUGH; diff --git a/tests/api.c b/tests/api.c index 58f7f5719..711708985 100644 --- a/tests/api.c +++ b/tests/api.c @@ -4848,6 +4848,9 @@ static void test_wolfSSH_SFTP_PartialSend(void) wolfSSH_SFTP_Close(ssh, handle, handleSz); if (wolfSSH_SFTP_STAT(ssh, atrName, &atr) == WS_SUCCESS) { + /* Send only the permissions back. A port without WTRUNCATE + * answers a size request with SSH_FX_OP_UNSUPPORTED. */ + atr.flags = WOLFSSH_FILEATRB_PERM; AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS); ret = WS_FATAL_ERROR; sawPartial = 0; diff --git a/tests/regress.c b/tests/regress.c index fc739fce6..597581aef 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -4890,6 +4890,274 @@ static void TestSftpCloseFailureRemovesHandle(void) wolfSSH_CTX_free(ctx); } +/* Build the attribute block of a SETSTAT/FSETSTAT payload. Optional fields go + * in flag-bit order, matching SFTP_ParseAttributes_buffer. */ +static word32 SftpBuildAttrs(byte* out, const WS_SFTP_FILEATRB* atr) +{ + word32 idx = 0; + + SftpPutU32(atr->flags, out + idx); idx += UINT32_SZ; + if (atr->flags & WOLFSSH_FILEATRB_SIZE) { + SftpPutU32(atr->sz[1], out + idx); idx += UINT32_SZ; + SftpPutU32(atr->sz[0], out + idx); idx += UINT32_SZ; + } + if (atr->flags & WOLFSSH_FILEATRB_UIDGID) { + SftpPutU32(atr->uid, out + idx); idx += UINT32_SZ; + SftpPutU32(atr->gid, out + idx); idx += UINT32_SZ; + } + if (atr->flags & WOLFSSH_FILEATRB_PERM) { + SftpPutU32(atr->per, out + idx); idx += UINT32_SZ; + } + if (atr->flags & WOLFSSH_FILEATRB_TIME) { + SftpPutU32(atr->atime, out + idx); idx += UINT32_SZ; + SftpPutU32(atr->mtime, out + idx); idx += UINT32_SZ; + } + return idx; +} + + +/* Sends an FXP_SETSTAT for path and returns the handler's return code. */ +static int SftpSendSetSTAT(WOLFSSH* ssh, int reqId, const char* path, + const WS_SFTP_FILEATRB* atr) +{ + byte pkt[UINT32_SZ + WOLFSSH_MAX_FILENAME + (UINT32_SZ * 8)]; + word32 idx = 0; + word32 sz = (word32)WSTRLEN(path); + + SftpPutU32(sz, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, sz); idx += sz; + idx += SftpBuildAttrs(pkt + idx, atr); + + return wolfSSH_SFTP_RecvSetSTAT(ssh, reqId, pkt, idx); +} + + +/* Sends an FXP_FSETSTAT for an open handle and returns the handler's code. */ +static int SftpSendFSetSTAT(WOLFSSH* ssh, int reqId, const byte* handle, + const WS_SFTP_FILEATRB* atr) +{ + byte pkt[UINT32_SZ + WOLFSSH_HANDLE_ID_SZ + (UINT32_SZ * 8)]; + word32 idx = 0; + + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + idx += SftpBuildAttrs(pkt + idx, atr); + + return wolfSSH_SFTP_RecvFSetSTAT(ssh, reqId, pkt, idx); +} + + +/* SETSTAT and FSETSTAT must apply every attribute they acknowledge. Answering + * FTP_OK for a size, ownership or timestamp change that never reached the file + * leaves the client believing a truncate or a chown happened. */ +static void TestSftpSetStatAttributes(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WS_SFTP_FILEATRB atr; + WSTAT_T st; + const byte* reply; + const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */ +#if defined(WSETTIME) || defined(WFSETTIME) + const word32 when = 1000000000; +#endif + const char body[] = "0123456789abcdef"; + word32 ofst[2] = {0, 0}; + word32 idx; + word32 replySz; + word32 origUid; + word32 origGid; + int rid = 500; + int fd; + byte handle[WOLFSSH_HANDLE_ID_SZ]; + byte pkt[256]; + char cwd[WOLFSSH_MAX_FILENAME]; + char path[64]; + + 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 name (see TestSftpForgedHandleRejected) */ + WSNPRINTF(path, sizeof(path), "wolfssh_setstat_%d.tmp", (int)getpid()); + + WMEMSET(cwd, 0, sizeof(cwd)); + AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1)); + AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS); + AssertIntEQ(wolfSSH_SFTP_SetConfinePath(ssh, cwd), WS_SUCCESS); + + /* fixture: a file of known length owned by this process */ + fd = WOPEN(ssh->fs, path, + WOLFSSH_O_RDWR | WOLFSSH_O_CREAT | WOLFSSH_O_TRUNC, 0600); + AssertTrue(fd >= 0); + AssertIntEQ(WPWRITE(ssh->fs, fd, (byte*)body, (word32)(sizeof(body) - 1), + ofst), (int)(sizeof(body) - 1)); + AssertIntEQ(WCLOSE(ssh->fs, fd), 0); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, (int)(sizeof(body) - 1)); + + /* a SETSTAT that shrinks the file has to truncate it */ + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_SIZE; + atr.sz[0] = 4; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, 4); + + /* and one that grows it has to extend the file */ + atr.sz[0] = 32; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, 32); + + /* a size past the offset ceiling of the port is refused outright rather + * than wrapped through the cast, and the file is left alone */ + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_SIZE; + atr.sz[1] = 0x80000000; + AssertTrue(SftpSendSetSTAT(ssh, rid, path, &atr) != WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, 32); + +#ifdef WSETTIME + /* a requested timestamp has to reach the file */ + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_TIME; + atr.atime = when; + atr.mtime = when; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_mtime, (int)when); +#endif + + /* a chown to the file's existing owner is a no-op and stays accepted */ + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_UIDGID; + atr.uid = (word32)st.st_uid; + atr.gid = (word32)st.st_gid; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + + /* giving the file away is refused for an unprivileged server, and that + * refusal has to reach the client instead of an OK */ + origUid = (word32)st.st_uid; + origGid = (word32)st.st_gid; + if (geteuid() != 0) { + atr.uid = origUid + 1; + AssertTrue(SftpSendSetSTAT(ssh, rid, path, &atr) != WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)geteuid()); + } + else { + /* root may give the file away, so prove the chown reached the file + * instead of relying on the no-op case above, then put it back */ + atr.uid = origUid + 1; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)(origUid + 1)); + + atr.uid = origUid; + atr.gid = origGid; + AssertIntEQ(SftpSendSetSTAT(ssh, rid, path, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)origUid); + } + + /* the same contract holds for FSETSTAT against an open handle */ + idx = 0; + SftpPutU32((word32)WSTRLEN(path), pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, path, WSTRLEN(path)); + idx += (word32)WSTRLEN(path); + SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE, 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(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ); + + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_SIZE; + atr.sz[0] = 8; + AssertIntEQ(SftpSendFSetSTAT(ssh, rid, handle, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, 8); + + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_SIZE; + atr.sz[1] = 0x80000000; + AssertTrue(SftpSendFSetSTAT(ssh, rid, handle, &atr) != WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_size, 8); + + /* ownership over the handle, the same pair of cases as the path form */ + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_UIDGID; + atr.uid = (word32)st.st_uid; + atr.gid = (word32)st.st_gid; + AssertIntEQ(SftpSendFSetSTAT(ssh, rid, handle, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + + origUid = (word32)st.st_uid; + origGid = (word32)st.st_gid; + if (geteuid() != 0) { + atr.uid = origUid + 1; + AssertTrue(SftpSendFSetSTAT(ssh, rid, handle, &atr) != WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_FAILURE); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)geteuid()); + } + else { + atr.uid = origUid + 1; + AssertIntEQ(SftpSendFSetSTAT(ssh, rid, handle, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)(origUid + 1)); + + atr.uid = origUid; + atr.gid = origGid; + AssertIntEQ(SftpSendFSetSTAT(ssh, rid, handle, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_uid, (int)origUid); + } + +#ifdef WFSETTIME + WMEMSET(&atr, 0, sizeof(atr)); + atr.flags = WOLFSSH_FILEATRB_TIME; + atr.atime = when + 100; + atr.mtime = when + 100; + AssertIntEQ(SftpSendFSetSTAT(ssh, rid, handle, &atr), WS_SUCCESS); + AssertSftpStatusReply(ssh, rid++, WOLFSSH_FTP_OK); + AssertIntEQ(WSTAT(ssh->fs, path, &st), 0); + AssertIntEQ((int)st.st_mtime, (int)(when + 100)); +#endif + + idx = 0; + SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ; + WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ); + idx += WOLFSSH_HANDLE_ID_SZ; + AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS); + + (void)WREMOVE(ssh->fs, path); + wolfSSH_SFTP_TestRecvStateFree(ssh); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + /* Sends an FXP_STAT for path and returns the handler's return code. */ static int SftpStatPath(WOLFSSH* ssh, int reqId, const char* path) { @@ -7772,6 +8040,8 @@ int main(int argc, char** argv) TestSftpCloseFailureRemovesHandle(); /* confinement follows the confine root, not the start path */ TestSftpStartPathInsideConfineRoot(); + /* SETSTAT/FSETSTAT apply the attributes they acknowledge */ + TestSftpSetStatAttributes(); #endif #if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME) TestNucleusMonthConversion(); diff --git a/tests/sftp.c b/tests/sftp.c index 2d8d940b7..d79edc739 100644 --- a/tests/sftp.c +++ b/tests/sftp.c @@ -149,9 +149,9 @@ static int checkCdNonexistent(void) #if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_FATFS) && \ !defined(WOLFSSH_ZEPHYR) -/* Captured once before any threads start; used to compute the expected - * post-open file mode without changing process-wide umask state. */ -static mode_t sftpTestUmask = 0; +/* The umask wolfSSH_SftpTest() installs before any threads start, so the + * modes the server creates files and directories with are known. */ +#define SFTP_TEST_UMASK 0022 /* Verify SFTP_SetFileAttributes stripped setuid/setgid/sticky bits when * the client sent chmod 4777 (setuid + rwxrwxrwx). */ @@ -181,13 +181,13 @@ static int checkChmodStripsSpecialBits(void) /* Verify wolfSSH_SFTP_RecvOpen stripped setuid/setgid/sticky bits when the * client sent creat 04755 (setuid + rwxr-xr-x). The expected base mode is - * 0755 with the process umask applied, captured before any threads start. */ + * 0755 with SFTP_TEST_UMASK applied. */ static int checkCreatStripsSpecialBits(void) { WSTAT_T st; unsigned int expectedMode; - expectedMode = (unsigned int)(0755 & ~sftpTestUmask); + expectedMode = (unsigned int)(0755 & ~SFTP_TEST_UMASK); WMEMSET(&st, 0, sizeof(WSTAT_T)); if (WSTAT(NULL, "test-creat-special", &st) != 0) { fprintf(stderr, "stat test-creat-special failed\n"); @@ -207,6 +207,25 @@ static int checkCreatStripsSpecialBits(void) } return 0; } + +/* Verify the chmod of a directory landed: mkdir asks for 0777, which + * SFTP_TEST_UMASK leaves at 0755, so only the chmod can make it 0700. */ +static int checkChmodDirectory(void) +{ + WSTAT_T st; + + WMEMSET(&st, 0, sizeof(WSTAT_T)); + if (WSTAT(NULL, "test-chmod-dir", &st) != 0) { + fprintf(stderr, "stat test-chmod-dir failed\n"); + return 1; + } + if ((st.st_mode & 0777) != 0700) { + fprintf(stderr, "chmod on directory: mode=%06o, want 0700\n", + (unsigned)(st.st_mode & 07777)); + return 1; + } + return 0; +} #endif /* !USE_WINDOWS_API && !WOLFSSH_FATFS && !WOLFSSH_ZEPHYR */ #if !defined(NO_WOLFSSH_DIR) && !defined(WOLFSSH_FATFS) @@ -274,6 +293,7 @@ static const SftpTestCmd cmds[] = { { "rm test-get", NULL }, { "rm test-get-2", NULL }, { "rm test-creat-special", NULL }, + { "rmdir test-chmod-dir", NULL }, /* --- test sequence starts here --- */ { "mkdir a", NULL }, @@ -307,6 +327,11 @@ static const SftpTestCmd cmds[] = { * wolfSSH_SFTP_RecvOpen applied WOLFSSH_SFTP_SAFE_MODE. */ { "creat 04755 test-creat-special", checkCreatStripsSpecialBits }, { "rm test-creat-special", NULL }, + /* chmod of a directory; checkChmodDirectory verifies the mode landed + * and the directory survived. */ + { "mkdir test-chmod-dir", NULL }, + { "chmod 700 test-chmod-dir", checkChmodDirectory }, + { "rmdir test-chmod-dir", NULL }, #endif { "chmod 600 test-get-2", NULL }, { "rm test-get-2", NULL }, @@ -393,6 +418,10 @@ int wolfSSH_SftpTest(int flag) #ifndef USE_WINDOWS_API char portNumber[8]; #endif +#if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_FATFS) && \ + !defined(WOLFSSH_ZEPHYR) + mode_t callerUmask; +#endif THREAD_TYPE serThread; @@ -406,9 +435,9 @@ int wolfSSH_SftpTest(int flag) #if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_FATFS) && \ !defined(WOLFSSH_ZEPHYR) - /* Read umask non-destructively before spawning threads. */ - sftpTestUmask = umask(0); - umask(sftpTestUmask); + /* Fix the umask before spawning threads so the file and directory + * modes the checks expect do not depend on the caller's umask. */ + callerUmask = umask(SFTP_TEST_UMASK); #endif argsCount = 0; @@ -460,6 +489,11 @@ int wolfSSH_SftpTest(int flag) wolfSSH_Cleanup(); FreeTcpReady(&ready); +#if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_FATFS) && \ + !defined(WOLFSSH_ZEPHYR) + umask(callerUmask); +#endif + return ret; } #endif /* WOLFSSH_SFTP */ diff --git a/wolfssh/port.h b/wolfssh/port.h index 44e1f572f..972acc76a 100644 --- a/wolfssh/port.h +++ b/wolfssh/port.h @@ -438,8 +438,6 @@ extern "C" { #define WFPUTS(b,f) SYS_FS_FileStringPut((f), (b)) #define WOLFSSH_NO_UTIMES #define WUTIMES(a,b) (0) /* Not ported yet */ - #define WSETTIME(fs,f,a,m) (0) - #define WFSETTIME(fs,fd,a,m) (0) #define WCHDIR(fs,b) SYS_FS_DirectryChange((b)) #define WFSEEK_SUCCESS(r) ((int)(r) >= 0) @@ -464,8 +462,6 @@ extern "C" { clearerr((s)); } while (0) #define WSEEK_END SEEK_END #define WBADFILE NULL - #define WSETTIME(fs,f,a,m) (0) - #define WFSETTIME(fs,fd,a,m) (0) #ifdef WOLFSSL_VXWORKS #define WOLFSSH_NO_UTIMES #define WUTIMES(f,t) (WS_SUCCESS) @@ -516,11 +512,57 @@ extern "C" { #endif #endif + /* Set a file's access and modification times from seconds since the + * epoch. Ports with no way to set them leave these undefined. */ + #if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_NO_UTIMES) + #include + static inline int wSetTime(const char* f, unsigned int atime, + unsigned int mtime) + { + struct timeval t[2]; + + t[0].tv_sec = (time_t)atime; + t[0].tv_usec = 0; + t[1].tv_sec = (time_t)mtime; + t[1].tv_usec = 0; + #ifdef WUTIMES_NOFOLLOW + return WUTIMES_NOFOLLOW(f, t); + #else + return WUTIMES(f, t); + #endif + } + #define WSETTIME(fs,f,a,m) wSetTime((f),(a),(m)) + + #ifdef WFUTIMES + static inline int wFSetTime(int fd, unsigned int atime, + unsigned int mtime) + { + struct timeval t[2]; + + t[0].tv_sec = (time_t)atime; + t[0].tv_usec = 0; + t[1].tv_sec = (time_t)mtime; + t[1].tv_usec = 0; + return WFUTIMES(fd, t); + } + #define WFSETTIME(fs,fd,a,m) wFSetTime((fd),(a),(m)) + #endif + #endif + #ifndef USE_WINDOWS_API #define WCHMOD(fs,f,m) chmod((f),(m)) #define WFCHMOD(fs,fd,m) fchmod((fd),(m)) #else - #define WCHMOD(fs,f,m) _chmod((f),(m)) + #if (defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP)) && \ + !defined(_WIN32_WCE) + /* _chmod() wants a Windows path, but SFTP names arrive with a + * leading root. WS_ChmodA() trims it as the other wrappers do. */ + WOLFSSH_LOCAL int WS_ChmodA(const char* fileName, int mode, + void* heap); + #define WCHMOD(fs,f,m) WS_ChmodA((f),(m),NULL) + #else + #define WCHMOD(fs,f,m) _chmod((f),(m)) + #endif #define WFCHMOD(fs,fd,m) _fchmod((fd),(m)) #endif @@ -1589,6 +1631,11 @@ extern "C" { #define WPWRITE(fs,fd,b,s,o) wPwrite((fd),(b),(s),(o)) #define WPREAD(fs,fd,b,s,o) wPread((fd),(b),(s),(o)) + #define WTRUNCATE(fs,f,sz) truncate((f),(off_t)(sz)) + #define WFTRUNCATE(fs,fd,sz) ftruncate((fd),(off_t)(sz)) + #define WCHOWN(fs,f,u,g) chown((f),(uid_t)(u),(gid_t)(g)) + #define WFCHOWN(fs,fd,u,g) fchown((fd),(uid_t)(u),(gid_t)(g)) + #ifndef NO_WOLFSSH_DIR #include /* used for opendir, readdir, and closedir */ #define WDIR DIR*