/** * @file dll/security.c * * @copyright 2015-2022 Bill Zissimopoulos */ /* * This file is part of WinFsp. * * You can redistribute it and/or modify it under the terms of the GNU * General Public License version 3 as published by the Free Software * Foundation. * * Licensees holding a valid commercial license may use this software * in accordance with the commercial license agreement provided in * conjunction with the software. The terms and conditions of any such * commercial license agreement shall govern, supersede, and render * ineffective any application of the GPLv3 license to this software, * notwithstanding of any reference thereto in the software or * associated repository. */ #include static GENERIC_MAPPING FspFileGenericMapping = { .GenericRead = FILE_GENERIC_READ, .GenericWrite = FILE_GENERIC_WRITE, .GenericExecute = FILE_GENERIC_EXECUTE, .GenericAll = FILE_ALL_ACCESS, }; FSP_API PGENERIC_MAPPING FspGetFileGenericMapping(VOID) { return &FspFileGenericMapping; } static NTSTATUS FspGetSecurityByName(FSP_FILE_SYSTEM *FileSystem, PWSTR FileName, PUINT32 PFileAttributes, PSECURITY_DESCRIPTOR *PSecurityDescriptor, SIZE_T *PSecurityDescriptorSize) { for (;;) { NTSTATUS Result = FileSystem->Interface->GetSecurityByName(FileSystem, FileName, PFileAttributes, *PSecurityDescriptor, PSecurityDescriptorSize); if (STATUS_BUFFER_OVERFLOW != Result) return Result; MemFree(*PSecurityDescriptor); *PSecurityDescriptor = MemAlloc(*PSecurityDescriptorSize); if (0 == *PSecurityDescriptor) return STATUS_INSUFFICIENT_RESOURCES; } } FSP_API NTSTATUS FspAccessCheckEx(FSP_FILE_SYSTEM *FileSystem, FSP_FSCTL_TRANSACT_REQ *Request, BOOLEAN CheckParentOrMain, BOOLEAN AllowTraverseCheck, UINT32 DesiredAccess, PUINT32 PGrantedAccess, PSECURITY_DESCRIPTOR *PSecurityDescriptor) { BOOLEAN CheckParentDirectory, CheckMainFile; CheckParentDirectory = CheckMainFile = FALSE; if (CheckParentOrMain) { if (!Request->Req.Create.NamedStream) CheckParentDirectory = TRUE; else CheckMainFile = TRUE; } *PGrantedAccess = 0; if (0 != PSecurityDescriptor) *PSecurityDescriptor = 0; if (FspFsctlTransactCreateKind != Request->Kind) return STATUS_INVALID_PARAMETER; if (CheckParentDirectory && L'\\' == ((PWSTR)Request->Buffer)[0] && L'\0' == ((PWSTR)Request->Buffer)[1]) return STATUS_INVALID_PARAMETER; if (0 == FileSystem->Interface->GetSecurityByName || (!Request->Req.Create.UserMode && 0 == PSecurityDescriptor)) { *PGrantedAccess = (MAXIMUM_ALLOWED & DesiredAccess) ? FspFileGenericMapping.GenericAll : DesiredAccess; return STATUS_SUCCESS; } NTSTATUS Result; WCHAR Root[2] = L"\\", TraverseCheckRoot[2] = L"\\"; PWSTR FileName, Suffix, Prefix, Remain; UINT32 FileAttributes = 0; PSECURITY_DESCRIPTOR SecurityDescriptor = 0; SIZE_T SecurityDescriptorSize; UINT8 PrivilegeSetBuf[sizeof(PRIVILEGE_SET) + 15 * sizeof(LUID_AND_ATTRIBUTES)]; PPRIVILEGE_SET PrivilegeSet = (PVOID)PrivilegeSetBuf; DWORD PrivilegeSetLength = sizeof PrivilegeSetBuf; UINT32 TraverseAccess, ParentAccess, DesiredAccess2; UINT16 NamedStreamSave; BOOL AccessStatus; if (CheckParentDirectory) FspPathSuffix((PWSTR)Request->Buffer, &FileName, &Suffix, Root); else if (CheckMainFile) { ((PWSTR)Request->Buffer)[Request->Req.Create.NamedStream / sizeof(WCHAR)] = L'\0'; FileName = (PWSTR)Request->Buffer; } else FileName = (PWSTR)Request->Buffer; SecurityDescriptorSize = 1024; SecurityDescriptor = MemAlloc(SecurityDescriptorSize); if (0 == SecurityDescriptor) { Result = STATUS_INSUFFICIENT_RESOURCES; goto exit; } if (Request->Req.Create.UserMode && AllowTraverseCheck && !Request->Req.Create.HasTraversePrivilege && !(L'\\' == FileName[0] && L'\0' == FileName[1])/* no need to traverse check for root */) { Remain = FileName; for (;;) { while (L'\\' != *Remain) { if (L'\0' == *Remain || L':' == *Remain) goto traverse_check_done; Remain++; } *Remain = L'\0'; Prefix = Remain > FileName ? FileName : TraverseCheckRoot; FileAttributes = 0; Result = FspGetSecurityByName(FileSystem, Prefix, &FileAttributes, &SecurityDescriptor, &SecurityDescriptorSize); /* * We check to see if this is a reparse point and then compute the ReparsePointIndex * and place it in FileAttributes. We do this check BEFORE the directory check, * because contrary to NTFS we want to allow non-directory symlinks to directories. */ if (NT_SUCCESS(Result) && STATUS_REPARSE != Result && (FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { FileAttributes = FspPathSuffixIndex(Prefix); Result = STATUS_REPARSE; } *Remain = L'\\'; do { Remain++; } while (L'\\' == *Remain); if (!NT_SUCCESS(Result) || STATUS_REPARSE == Result) { if (STATUS_OBJECT_NAME_NOT_FOUND == Result) Result = STATUS_OBJECT_PATH_NOT_FOUND; goto exit; } /* * Check if this is a directory, otherwise the path is invalid. */ if (0 == (FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { Result = STATUS_OBJECT_PATH_NOT_FOUND; /* use STATUS_OBJECT_PATH_INVALID? */ goto exit; } if (0 < SecurityDescriptorSize) { if (AccessCheck(SecurityDescriptor, FSP_FSCTL_TRANSACT_REQ_TOKEN_HANDLE(Request->Req.Create.AccessToken), FILE_TRAVERSE, &FspFileGenericMapping, PrivilegeSet, &PrivilegeSetLength, &TraverseAccess, &AccessStatus)) Result = AccessStatus ? STATUS_SUCCESS : STATUS_ACCESS_DENIED; else Result = FspNtStatusFromWin32(GetLastError()); if (!NT_SUCCESS(Result)) goto exit; } } traverse_check_done: ; } FileAttributes = 0; Result = FspGetSecurityByName(FileSystem, FileName, &FileAttributes, &SecurityDescriptor, &SecurityDescriptorSize); if (!NT_SUCCESS(Result) || STATUS_REPARSE == Result) goto exit; if (!CheckParentOrMain && Request->Req.Create.HasTrailingBackslash && !(FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { Result = STATUS_OBJECT_NAME_INVALID; goto exit; } if (Request->Req.Create.UserMode && 0 < SecurityDescriptorSize) { if (0 == DesiredAccess) Result = STATUS_SUCCESS; else if (AccessCheck(SecurityDescriptor, FSP_FSCTL_TRANSACT_REQ_TOKEN_HANDLE(Request->Req.Create.AccessToken), DesiredAccess, &FspFileGenericMapping, PrivilegeSet, &PrivilegeSetLength, PGrantedAccess, &AccessStatus)) Result = AccessStatus ? STATUS_SUCCESS : STATUS_ACCESS_DENIED; else Result = FspNtStatusFromWin32(GetLastError()); if (!NT_SUCCESS(Result)) { /* * If the desired access includes the DELETE or FILE_READ_ATTRIBUTES * (or MAXIMUM_ALLOWED) rights we must still check with our parent to * see if it gives us access (through the FILE_DELETE_CHILD and * FILE_LIST_DIRECTORY rights). * * Does the Windows security model suck? Ermmmm... */ if (STATUS_ACCESS_DENIED != Result || 0 == ((MAXIMUM_ALLOWED | DELETE | FILE_READ_ATTRIBUTES) & DesiredAccess)) goto exit; NamedStreamSave = Request->Req.Create.NamedStream; Request->Req.Create.NamedStream = 0; Result = FspAccessCheck(FileSystem, Request, TRUE, FALSE, (MAXIMUM_ALLOWED & DesiredAccess) ? (FILE_DELETE_CHILD | FILE_LIST_DIRECTORY) : ( ((DELETE & DesiredAccess) ? FILE_DELETE_CHILD : 0) | ((FILE_READ_ATTRIBUTES & DesiredAccess) ? FILE_LIST_DIRECTORY : 0) ), &ParentAccess); Request->Req.Create.NamedStream = NamedStreamSave; if (!NT_SUCCESS(Result)) { /* any failure just becomes ACCESS DENIED at this point */ Result = STATUS_ACCESS_DENIED; goto exit; } /* redo the access check but remove the DELETE and/or FILE_READ_ATTRIBUTES rights */ DesiredAccess2 = DesiredAccess & ~( ((FILE_DELETE_CHILD & ParentAccess) ? DELETE : 0) | ((FILE_LIST_DIRECTORY & ParentAccess) ? FILE_READ_ATTRIBUTES : 0)); if (0 != DesiredAccess2) { if (AccessCheck(SecurityDescriptor, FSP_FSCTL_TRANSACT_REQ_TOKEN_HANDLE(Request->Req.Create.AccessToken), DesiredAccess2, &FspFileGenericMapping, PrivilegeSet, &PrivilegeSetLength, PGrantedAccess, &AccessStatus)) Result = AccessStatus ? STATUS_SUCCESS : STATUS_ACCESS_DENIED; else /* any failure just becomes ACCESS DENIED at this point */ Result = STATUS_ACCESS_DENIED; if (!NT_SUCCESS(Result)) goto exit; } if (FILE_DELETE_CHILD & ParentAccess) *PGrantedAccess |= DELETE; if (FILE_LIST_DIRECTORY & ParentAccess) *PGrantedAccess |= FILE_READ_ATTRIBUTES; } } if (CheckParentDirectory) { /* * We check to see if this is a reparse point and then immediately return * STATUS_REPARSE. We do this check BEFORE the directory check, because * contrary to NTFS we want to allow non-directory symlinks to directories. */ if (FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { FileAttributes = FspPathSuffixIndex(FileName); Result = STATUS_REPARSE; goto exit; } if (0 == (FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { Result = STATUS_NOT_A_DIRECTORY; goto exit; } } else if (CheckMainFile) { /* * We check to see if this is a reparse point and FILE_OPEN_REPARSE_POINT * was not specified, in which case we return STATUS_REPARSE. */ if (0 != (FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && 0 == (Request->Req.Create.CreateOptions & FILE_OPEN_REPARSE_POINT)) { FileAttributes = FspPathSuffixIndex(FileName); Result = STATUS_REPARSE; goto exit; } } else { /* * We check to see if this is a reparse point and FILE_OPEN_REPARSE_POINT * was not specified, in which case we return STATUS_REPARSE. */ if (0 != (FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && 0 == (Request->Req.Create.CreateOptions & FILE_OPEN_REPARSE_POINT)) { FileAttributes = FspPathSuffixIndex(FileName); Result = STATUS_REPARSE; goto exit; } /* * We allow some file systems (notably FUSE) to open reparse points * regardless of the FILE_DIRECTORY_FILE / FILE_NON_DIRECTORY_FILE options. */ if (0 != (FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && FileSystem->UmNoReparsePointsDirCheck) goto skip_reparse_dir_check; if ((Request->Req.Create.CreateOptions & FILE_DIRECTORY_FILE) && 0 == (FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { Result = STATUS_NOT_A_DIRECTORY; goto exit; } if ((Request->Req.Create.CreateOptions & FILE_NON_DIRECTORY_FILE) && 0 != (FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { Result = STATUS_FILE_IS_A_DIRECTORY; goto exit; } skip_reparse_dir_check: ; } if (Request->Req.Create.UserMode) { if (FILE_ATTRIBUTE_READONLY == (FileAttributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_DIRECTORY)) && (DesiredAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD))) { Result = STATUS_ACCESS_DENIED; goto exit; } if (FILE_ATTRIBUTE_READONLY == (FileAttributes & FILE_ATTRIBUTE_READONLY) && (Request->Req.Create.CreateOptions & FILE_DELETE_ON_CLOSE)) { Result = STATUS_CANNOT_DELETE; goto exit; } if (0 == SecurityDescriptorSize) *PGrantedAccess = (MAXIMUM_ALLOWED & DesiredAccess) ? FspFileGenericMapping.GenericAll : DesiredAccess; if (FILE_ATTRIBUTE_READONLY == (FileAttributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_DIRECTORY)) && 0 != (MAXIMUM_ALLOWED & DesiredAccess)) *PGrantedAccess &= ~(FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD); } else *PGrantedAccess = (MAXIMUM_ALLOWED & DesiredAccess) ? FspFileGenericMapping.GenericAll : DesiredAccess; Result = STATUS_SUCCESS; exit: if (STATUS_REPARSE == Result) *PGrantedAccess = FileAttributes; /* FileAttributes contains ReparsePointIndex */ else if (0 != PSecurityDescriptor && 0 < SecurityDescriptorSize && NT_SUCCESS(Result)) *PSecurityDescriptor = SecurityDescriptor; else MemFree(SecurityDescriptor); if (CheckParentDirectory) { FspPathCombine((PWSTR)Request->Buffer, Suffix); if (STATUS_OBJECT_NAME_NOT_FOUND == Result) Result = STATUS_OBJECT_PATH_NOT_FOUND; } else if (CheckMainFile) ((PWSTR)Request->Buffer)[Request->Req.Create.NamedStream / sizeof(WCHAR)] = L':'; return Result; } FSP_API NTSTATUS FspCreateSecurityDescriptor(FSP_FILE_SYSTEM *FileSystem, FSP_FSCTL_TRANSACT_REQ *Request, PSECURITY_DESCRIPTOR ParentDescriptor, PSECURITY_DESCRIPTOR *PSecurityDescriptor) { *PSecurityDescriptor = 0; if (FspFsctlTransactCreateKind != Request->Kind) return STATUS_INVALID_PARAMETER; /* stream support: return NULL security descriptor when creating named stream */ if (Request->Req.Create.NamedStream) return STATUS_SUCCESS; if (!CreatePrivateObjectSecurity( ParentDescriptor, 0 != Request->Req.Create.SecurityDescriptor.Offset ? (PSECURITY_DESCRIPTOR)(Request->Buffer + Request->Req.Create.SecurityDescriptor.Offset) : 0, PSecurityDescriptor, 0 != (Request->Req.Create.CreateOptions & FILE_DIRECTORY_FILE), FSP_FSCTL_TRANSACT_REQ_TOKEN_HANDLE(Request->Req.Create.AccessToken), &FspFileGenericMapping)) return FspNtStatusFromWin32(GetLastError()); //DEBUGLOGSD("SDDL=%s", *PSecurityDescriptor); return STATUS_SUCCESS; } FSP_API NTSTATUS FspSetSecurityDescriptor( PSECURITY_DESCRIPTOR InputDescriptor, SECURITY_INFORMATION SecurityInformation, PSECURITY_DESCRIPTOR ModificationDescriptor, PSECURITY_DESCRIPTOR *PSecurityDescriptor) { *PSecurityDescriptor = 0; if (0 == InputDescriptor) return STATUS_NO_SECURITY_ON_OBJECT; /* * SetPrivateObjectSecurity is a broken API. It assumes that the passed * descriptor resides on memory allocated by CreatePrivateObjectSecurity * or SetPrivateObjectSecurity and frees the descriptor on success. * * In our case the security descriptor comes from the user mode file system, * which may conjure it any way it sees fit. So we have to somehow make a copy * of the InputDescriptor and place it in memory that SetPrivateObjectSecurity * can then free. To complicate matters there is no API that can be used for * this purpose. What a PITA! */ /* !!!: HACK! HACK! HACK! * * Turns out that SetPrivateObjectSecurity and friends really use RtlProcessHeap * internally, which is just another name for GetProcessHeap(). * * I wish there was a cleaner way to do this! */ HANDLE ProcessHeap = GetProcessHeap(); DWORD InputDescriptorSize = GetSecurityDescriptorLength(InputDescriptor); PSECURITY_DESCRIPTOR CopiedDescriptor; CopiedDescriptor = HeapAlloc(ProcessHeap, 0, InputDescriptorSize); if (0 == CopiedDescriptor) return STATUS_INSUFFICIENT_RESOURCES; memcpy(CopiedDescriptor, InputDescriptor, InputDescriptorSize); InputDescriptor = CopiedDescriptor; if (!SetPrivateObjectSecurity( SecurityInformation, ModificationDescriptor, &InputDescriptor, &FspFileGenericMapping, 0)) { HeapFree(ProcessHeap, 0, CopiedDescriptor); return FspNtStatusFromWin32(GetLastError()); } /* CopiedDescriptor has been freed by SetPrivateObjectSecurity! */ *PSecurityDescriptor = InputDescriptor; //DEBUGLOGSD("SDDL=%s", *PSecurityDescriptor); return STATUS_SUCCESS; } FSP_API VOID FspDeleteSecurityDescriptor(PSECURITY_DESCRIPTOR SecurityDescriptor, NTSTATUS (*CreateFunc)()) { /* stream support: allow NULL security descriptors */ if (0 == SecurityDescriptor) return; if ((NTSTATUS (*)())FspAccessCheckEx == CreateFunc || (NTSTATUS (*)())FspPosixMapPermissionsToSecurityDescriptor == CreateFunc || (NTSTATUS (*)())FspPosixMergePermissionsToSecurityDescriptor == CreateFunc) MemFree(SecurityDescriptor); else if ((NTSTATUS (*)())FspCreateSecurityDescriptor == CreateFunc || (NTSTATUS (*)())FspSetSecurityDescriptor == CreateFunc) DestroyPrivateObjectSecurity(&SecurityDescriptor); }