From 417aa08eddede0a3a2729e8709556f86a80d47eb Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 25 Aug 2026 09:51:20 +0900 Subject: [PATCH] wolfsftp, port: apply the attributes SETSTAT and FSETSTAT acknowledge - SFTP_SetFileAttributes() and SFTP_SetFileAttributesHandle() carry out the size, ownership and timestamp requests, and set WS_UNIMPLEMENTED_E where the port defines no wrapper for one. Every branch runs only while ret is WS_SUCCESS, and the size is checked by wResolveOffset() against WOLFSSH_MAX_FILE_OFFSET. - SFTP_SetMode() swaps its USE_WINDOWS_API guard for _WIN32_WCE, and the permission step of SFTP_SetFileAttributes() drops the matching guard, so permissions are applied on Windows. - port.c adds WS_ChmodA(), which trims the SFTP leading root and calls _wchmod(); WCHMOD uses it on Windows SFTP and SCP builds. - wolfSSH_SFTP_RecvSetSTAT() and wolfSSH_SFTP_RecvFSetSTAT() answer WOLFSSH_FTP_UNSUPPORTED for WS_UNIMPLEMENTED_E. - wolfSSH_SFTP_CHMOD() sets the attribute flags to WOLFSSH_FILEATRB_PERM before sending. - port.h adds WTRUNCATE, WFTRUNCATE, WCHOWN and WFCHOWN for the POSIX port, and defines WSETTIME and WFSETTIME over the existing WUTIMES and WFUTIMES helpers in place of their (0) definitions. - tests/regress.c adds TestSftpSetStatAttributes(); tests/sftp.c adds a chmod of a directory. Issue: F-11658 --- src/port.c | 34 ++++++ src/wolfsftp.c | 104 ++++++++++++++----- tests/regress.c | 270 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/sftp.c | 26 +++++ wolfssh/port.h | 56 +++++++++- 5 files changed, 461 insertions(+), 29 deletions(-) diff --git a/src/port.c b/src/port.c index 1f39d1d2a..a4399b4a8 100644 --- a/src/port.c +++ b/src/port.c @@ -542,6 +542,40 @@ int WS_DeleteFileA(const char* fileName, void* heap) } + +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 /* USE_WINDOWS_API WOLFSSH_SFTP WOLFSSH_SCP */ #if !defined(NO_FILESYSTEM) && \ diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 86c30208a..149f9aea7 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -6042,7 +6042,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 */ @@ -6078,29 +6078,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 */ @@ -6124,29 +6142,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 */ @@ -6155,9 +6196,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 ; @@ -6182,6 +6223,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; @@ -6220,8 +6262,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; } @@ -6257,6 +6305,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; @@ -6311,8 +6360,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; } @@ -7661,7 +7716,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/regress.c b/tests/regress.c index 826ed1d12..a30a75a3e 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -3522,6 +3522,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) { @@ -6387,6 +6655,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..bb4cb129a 100644 --- a/tests/sftp.c +++ b/tests/sftp.c @@ -207,6 +207,26 @@ static int checkCreatStripsSpecialBits(void) } return 0; } + +/* Verify chmod of a directory reached it and left the directory intact. The + * client sends only the permission flag, so the server is never asked to + * truncate or chown the directory alongside it. */ +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 +294,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 +328,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 }, diff --git a/wolfssh/port.h b/wolfssh/port.h index 44e1f572f..7d4f9d949 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,56 @@ 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) + /* _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 +1630,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*