diff --git a/.github/workflows/CI-win.yml b/.github/workflows/CI-win.yml index 8dcece2e22d..b2e82d86b97 100644 --- a/.github/workflows/CI-win.yml +++ b/.github/workflows/CI-win.yml @@ -68,9 +68,17 @@ jobs: shell: cmd run: build\\RelWithDebInfo\\uv_run_tests_a.exe + - name: Test AppContainer + if: ${{ matrix.config.arch != 'arm64' }} + shell: cmd + run: + build\\RelWithDebInfo\\uv_run_appcontainer.exe build\\RelWithDebInfo\\${{ matrix.config.config == 'ASAN' && 'uv_run_tests_a.exe' || 'uv_run_tests.exe' }} build-mingw: - runs-on: ubuntu-latest + # Note: Using ubuntu-26.04 early (before it becomes ubuntu-latest) + # for access to a newer mingw for #5181. + # Can change back to ubuntu-latest once it means 26.04 or later. + runs-on: ubuntu-26.04 name: build-mingw-${{ matrix.config.arch }} strategy: fail-fast: false @@ -91,7 +99,8 @@ jobs: cmake --install build --prefix "`pwd`/build/usr" mkdir -p build/usr/test build/usr/bin cp -av test/fixtures build/usr/test - cp -av build/uv_run_tests_a.exe build/uv_run_tests.exe build/uv_run_tests_a_no_ext build/uv_run_tests_no_ext \ + cp -av build/uv_run_tests_a.exe build/uv_run_tests.exe build/uv_run_appcontainer.exe \ + build/uv_run_tests_a_no_ext build/uv_run_tests_no_ext \ `${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libgcc_s_${{ matrix.config.libgcc }}-1.dll` \ `${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libwinpthread-1.dll` \ `${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libatomic-1.dll` \ @@ -126,3 +135,7 @@ jobs: shell: cmd run: | bin\uv_run_tests.exe + - name: Test AppContainer + shell: cmd + run: | + bin\uv_run_appcontainer.exe bin\uv_run_tests.exe diff --git a/CMakeLists.txt b/CMakeLists.txt index 449dc832281..1149e3c522d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -747,6 +747,8 @@ if(LIBUV_BUILD_TESTS) COMMAND "${CMAKE_COMMAND}" -E copy "$" "$/uv_run_tests_a_no_ext") + add_executable(uv_run_appcontainer test/appcontainer.c) + target_link_libraries(uv_run_appcontainer userenv advapi32 ntdll) endif() if(QNX) diff --git a/src/win/pipe.c b/src/win/pipe.c index cbb1572f863..4bd5fbfd84a 100644 --- a/src/win/pipe.c +++ b/src/win/pipe.c @@ -106,8 +106,42 @@ static int includes_nul(const char *s, size_t n) { } +static int uv_is_app_container_; +static uv_once_t uv_is_app_container_guard_ = UV_ONCE_INIT; + + +/* Is this process running under Windows AppContainer? */ +/* Detection algorithm from https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-for-legacy-applications- */ +static void uv__init__is_app_container(void) { + HANDLE token; + DWORD ac; + DWORD len; + int ok; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + uv_is_app_container_ = 0; + return; + } + ac = 0; + len = sizeof(ac); + ok = GetTokenInformation(token, TokenIsAppContainer, &ac, len, &len); + CloseHandle(token); + uv_is_app_container_ = ok && ac != 0; +} + + +/* Is this process running under Windows AppContainer? */ +/* Cached form for repeated use in the process. */ +static int uv__is_appcontainer(void) { + uv_once(&uv_is_app_container_guard_, uv__init__is_app_container); + return uv_is_app_container_; +} + + static void uv__unique_pipe_name(unsigned long long ptr, char* name, size_t size) { - snprintf(name, size, "\\\\?\\pipe\\uv\\%llu-%lu", ptr, GetCurrentProcessId()); + snprintf(name, size, "\\\\?\\pipe\\%suv\\%llu-%lu", + uv__is_appcontainer() ? "LOCAL\\" : "", + ptr, GetCurrentProcessId()); } diff --git a/test/appcontainer.c b/test/appcontainer.c new file mode 100644 index 00000000000..e8fc761f45e --- /dev/null +++ b/test/appcontainer.c @@ -0,0 +1,610 @@ +/* Copyright libuv contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* + * appcontainer.c - Run a program inside a Windows AppContainer. + * + * Usage: appcontainer.exe program.exe [args...] + * + * Creates an AppContainer profile, grants it read/write access to the + * current directory tree and temp directory, adds loopback exemption + * for localhost network access, launches the given program inside the + * AppContainer, waits for it to exit, and returns the child's exit code. + */ + +#include +#include +#include +#include +#include +#include +#include + +/* + * Well-known capability SIDs for network access. + * See https://devblogs.microsoft.com/oldnewthing/20220503-00/?p=106557 + */ +#define INTERNET_CLIENT_SID "S-1-15-3-1" +#define INTERNET_CLIENT_SERVER_SID "S-1-15-3-2" +#define PRIVATE_NETWORK_CLIENT_SERVER_SID "S-1-15-3-3" + +typedef LONG NTSTATUS; + +NTSTATUS NTAPI NtSetSecurityObject(HANDLE, SECURITY_INFORMATION, + PSECURITY_DESCRIPTOR); +ULONG NTAPI RtlNtStatusToDosError(NTSTATUS); + +/* Modify the DACL on the given path for the AppContainer SID. + * If grant, add an ACE with the given perms and inheritance. + * If !grant, remove the SID's ACE (perms and inheritance are ignored). + * + * For non-inheritable ACEs, uses NtSetSecurityObject to set the DACL + * on the single object without a tree walk. The Win32 wrapper + * SetNamedSecurityInfo recursively re-propagates every inheritable ACE + * to all descendants even when the ACE being added is non-inheritable, + * which is extremely slow on large directory trees. + * + * For inheritable ACEs, uses SetNamedSecurityInfo so that Windows + * propagates the new ACE to existing descendants. */ +static DWORD modify_access(PSID sid, const char* path, DWORD perms, + DWORD inheritance, int grant) { + EXPLICIT_ACCESSA ea; + PACL old_acl = NULL; + PACL new_acl = NULL; + PSECURITY_DESCRIPTOR sd = NULL; + SECURITY_DESCRIPTOR sd_new; + HANDLE h; + NTSTATUS status; + DWORD err; + ULONGLONG t1 = GetTickCount64(); + + /* Open the file or directory. FILE_FLAG_BACKUP_SEMANTICS is + * required to obtain a handle to a directory. */ + h = CreateFileA(path, + READ_CONTROL | WRITE_DAC, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (h == INVALID_HANDLE_VALUE) { + err = GetLastError(); + fprintf(stderr, "appcontainer: modify_access %s %s perms=0x%lx inherit=0x%lx: " + "CreateFile error %lu (%.3f s)\n", + grant ? "grant" : "revoke", path, perms, inheritance, + err, (GetTickCount64() - t1) / 1000.0); + return err; + } + + /* Read the existing DACL. */ + err = GetSecurityInfo(h, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, NULL, &old_acl, NULL, &sd); + if (err != ERROR_SUCCESS) { + fprintf(stderr, "appcontainer: modify_access %s %s perms=0x%lx inherit=0x%lx: " + "GetSecurityInfo error %lu (%.3f s)\n", + grant ? "grant" : "revoke", path, perms, inheritance, + err, (GetTickCount64() - t1) / 1000.0); + CloseHandle(h); + return err; + } + + /* Build updated ACL. */ + memset(&ea, 0, sizeof(ea)); + ea.grfAccessPermissions = grant ? perms : 0; + ea.grfAccessMode = grant ? SET_ACCESS : REVOKE_ACCESS; + ea.grfInheritance = grant ? inheritance : NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + ea.Trustee.ptstrName = (LPSTR)sid; + + err = SetEntriesInAclA(1, &ea, old_acl, &new_acl); + if (err != ERROR_SUCCESS) { + fprintf(stderr, "appcontainer: modify_access %s %s perms=0x%lx inherit=0x%lx: " + "SetEntriesInAcl error %lu (%.3f s)\n", + grant ? "grant" : "revoke", path, perms, inheritance, + err, (GetTickCount64() - t1) / 1000.0); + LocalFree(sd); + CloseHandle(h); + return err; + } + + /* For non-inheritable ACEs (e.g. parent directory grants), use + * NtSetSecurityObject to set the DACL on this single object without + * the tree walk that SetNamedSecurityInfo performs. For inheritable + * ACEs, use SetNamedSecurityInfo so that Windows propagates the new + * ACE to existing descendants. */ + if (inheritance == NO_INHERITANCE) { + InitializeSecurityDescriptor(&sd_new, SECURITY_DESCRIPTOR_REVISION); + SetSecurityDescriptorDacl(&sd_new, TRUE, new_acl, FALSE); + status = NtSetSecurityObject(h, DACL_SECURITY_INFORMATION, &sd_new); + err = (status == 0) ? ERROR_SUCCESS : RtlNtStatusToDosError(status); + } else { + err = SetNamedSecurityInfoA((LPSTR)path, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, NULL, new_acl, NULL); + } + + fprintf(stderr, "appcontainer: modify_access %s %s perms=0x%lx inherit=0x%lx: " + "%s (%.3f s)\n", + grant ? "grant" : "revoke", path, perms, inheritance, + err == ERROR_SUCCESS ? "ok" : "error", + (GetTickCount64() - t1) / 1000.0); + + LocalFree(new_acl); + LocalFree(sd); + CloseHandle(h); + return err; +} + +/* Modify access on all parent directories up to the drive root. + * If grant is true, add read+execute; otherwise revoke. */ +static void modify_parents(PSID sid, const char* path, int grant) { + char parent[MAX_PATH]; + size_t len; + + strncpy(parent, path, MAX_PATH - 1); + parent[MAX_PATH - 1] = '\0'; + + for (;;) { + len = strlen(parent); + while (len > 0 && parent[len - 1] != '\\') + len--; + if (len == 0) + break; + /* Keep the trailing backslash only for drive roots (e.g. C:\). */ + if (len > 1 && parent[len - 2] != ':') + parent[len - 1] = '\0'; + else + parent[len] = '\0'; + + DWORD err = modify_access(sid, parent, + GENERIC_READ | GENERIC_EXECUTE, + NO_INHERITANCE, grant); + if (grant && err != ERROR_SUCCESS) + fprintf(stderr, "appcontainer: warning: grant parent %s: %lu\n", + parent, err); + + /* Stop at drive root (e.g. "C:\"). */ + if (len <= 3) + break; + parent[len - 1] = '\0'; + } +} + +/* Grant or revoke access to a directory and its parents. */ +static void modify_dir_and_parents(PSID sid, const char* path, int grant) { + ULONGLONG t1 = GetTickCount64(); + DWORD err = modify_access(sid, path, GENERIC_ALL, + CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE, + grant); + if (grant && err != ERROR_SUCCESS) { + fprintf(stderr, "appcontainer: warning: grant %s: %lu\n", path, err); + return; + } + + modify_parents(sid, path, grant); + + fprintf(stderr, "appcontainer: %s tree %s: %.3f s\n", + grant ? "grant" : "revoke", path, + (GetTickCount64() - t1) / 1000.0); +} + +/* Grant or revoke the AppContainer SID's access to the NUL device. + * uv_spawn needs to open NUL for ignored stdio handles. + * NUL is a device object, so we must use handle-based + * GetSecurityInfo/SetSecurityInfo (the named variants silently + * fail on devices). */ +static void modify_nul_access(PSID sid, int grant) { + HANDLE h; + PACL old_acl = NULL; + PACL new_acl = NULL; + PSECURITY_DESCRIPTOR sd = NULL; + EXPLICIT_ACCESSA ea; + DWORD err; + ULONGLONG t1 = GetTickCount64(); + + /* Open NUL with permission to read and modify the DACL. */ + h = CreateFileA("\\\\.\\NUL", + READ_CONTROL | WRITE_DAC, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + 0, + NULL); + if (h == INVALID_HANDLE_VALUE) { + if (grant) + fprintf(stderr, "appcontainer: warning: open NUL: %lu\n", GetLastError()); + return; + } + + err = GetSecurityInfo(h, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, NULL, &old_acl, NULL, &sd); + if (err != ERROR_SUCCESS) { + if (grant) + fprintf(stderr, "appcontainer: warning: GetSecurityInfo NUL: %lu\n", err); + CloseHandle(h); + return; + } + + memset(&ea, 0, sizeof(ea)); + ea.grfAccessPermissions = grant ? (GENERIC_READ | GENERIC_WRITE) : 0; + ea.grfAccessMode = grant ? SET_ACCESS : REVOKE_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + ea.Trustee.ptstrName = (LPSTR)sid; + + err = SetEntriesInAclA(1, &ea, old_acl, &new_acl); + if (err != ERROR_SUCCESS) { + if (grant) + fprintf(stderr, "appcontainer: warning: SetEntriesInAcl NUL: %lu\n", err); + LocalFree(sd); + CloseHandle(h); + return; + } + + err = SetSecurityInfo(h, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, NULL, new_acl, NULL); + if (grant) { + if (err != ERROR_SUCCESS) + fprintf(stderr, "appcontainer: warning: SetSecurityInfo NUL: %lu\n", err); + else + fprintf(stderr, "appcontainer: NUL device access granted\n"); + } + + fprintf(stderr, "appcontainer: %s NUL: %.3f s\n", + grant ? "grant" : "revoke", + (GetTickCount64() - t1) / 1000.0); + + LocalFree(new_acl); + LocalFree(sd); + CloseHandle(h); +} + +/* Add or remove the AppContainer SID from the loopback exemption list + * so that TCP/UDP tests can connect to localhost. */ +static void modify_loopback_exemption(PSID sid, int grant) { + /* Load NetworkIsolationGetAppContainerConfig and NetworkIsolationSetAppContainerConfig + * directly from FirewallAPI.dll, since netfw.h does not always contain their declarations. + * See https://learn.microsoft.com/en-us/windows/win32/api/netfw/nf-netfw-networkisolationsetappcontainerconfig */ + typedef DWORD (WINAPI *fnGetConfig)(DWORD*, PSID_AND_ATTRIBUTES*); + typedef DWORD (WINAPI *fnSetConfig)(DWORD, PSID_AND_ATTRIBUTES); + HMODULE hFirewall; + fnGetConfig getConfig; + fnSetConfig setConfig; + DWORD numSids = 0; + PSID_AND_ATTRIBUTES oldSids = NULL; + PSID_AND_ATTRIBUTES newSids = NULL; + DWORD newCount = 0; + DWORD err; + DWORD i; + ULONGLONG t1 = GetTickCount64(); + + hFirewall = LoadLibraryA("FirewallAPI.dll"); + if (!hFirewall) { + if (grant) + fprintf(stderr, "appcontainer: warning: LoadLibrary(FirewallAPI.dll) " + "failed: %lu\n", GetLastError()); + return; + } + + getConfig = (fnGetConfig)GetProcAddress(hFirewall, "NetworkIsolationGetAppContainerConfig"); + setConfig = (fnSetConfig)GetProcAddress(hFirewall, "NetworkIsolationSetAppContainerConfig"); + if (!getConfig || !setConfig) { + if (grant) + fprintf(stderr, "appcontainer: warning: NetworkIsolation API not found\n"); + FreeLibrary(hFirewall); + return; + } + + /* Get current exemption list. */ + err = getConfig(&numSids, &oldSids); + if (err != ERROR_SUCCESS) { + if (grant) + fprintf(stderr, "appcontainer: warning: NetworkIsolationGetAppContainerConfig: %lu\n", err); + FreeLibrary(hFirewall); + return; + } + + /* Build new list: copy existing entries (excluding our SID to avoid + * duplicates), then append our SID if granting. */ + newSids = (PSID_AND_ATTRIBUTES)malloc( + (numSids + 1) * sizeof(SID_AND_ATTRIBUTES)); + if (!newSids) { + FreeLibrary(hFirewall); + return; + } + + for (i = 0; i < numSids; i++) { + if (!EqualSid(oldSids[i].Sid, sid)) + newSids[newCount++] = oldSids[i]; + } + if (grant) { + newSids[newCount].Sid = sid; + newSids[newCount].Attributes = SE_GROUP_ENABLED; + newCount++; + } + + err = setConfig(newCount, newSids); + if (grant) { + if (err != ERROR_SUCCESS) + fprintf(stderr, "appcontainer: warning: NetworkIsolationSetAppContainerConfig: %lu\n", err); + else + fprintf(stderr, "appcontainer: loopback exemption added\n"); + } + + fprintf(stderr, "appcontainer: %s loopback exemption: %.3f s\n", + grant ? "grant" : "revoke", + (GetTickCount64() - t1) / 1000.0); + + free(newSids); + FreeLibrary(hFirewall); +} + +/* Launch a child process inside the AppContainer and return its + * exit code. */ +static int run_child(const char* abs_exe, const char* cmdline, + SECURITY_CAPABILITIES* sc) { + STARTUPINFOEXA si; + PROCESS_INFORMATION pi; + SIZE_T attr_size; + DWORD exit_code = 1; + + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + + /* Allocate the proc thread attribute list. */ + attr_size = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); + si.StartupInfo.cb = sizeof(si); + si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attr_size); + if (!si.lpAttributeList) { + fprintf(stderr, "malloc failed\n"); + return 1; + } + if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, + &attr_size)) { + fprintf(stderr, "InitializeProcThreadAttributeList failed: %lu\n", + GetLastError()); + free(si.lpAttributeList); + return 1; + } + + if (!UpdateProcThreadAttribute(si.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, + sc, + sizeof(*sc), + NULL, + NULL)) { + fprintf(stderr, "UpdateProcThreadAttribute failed: %lu\n", GetLastError()); + DeleteProcThreadAttributeList(si.lpAttributeList); + free(si.lpAttributeList); + return 1; + } + + /* Inherit handles so stdout/stderr flow through. */ + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + si.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + + if (!CreateProcessA(abs_exe, + (LPSTR)cmdline, + NULL, + NULL, + TRUE, + EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, + NULL, + NULL, + &si.StartupInfo, + &pi)) { + fprintf(stderr, "CreateProcessA failed: %lu\n", GetLastError()); + DeleteProcThreadAttributeList(si.lpAttributeList); + free(si.lpAttributeList); + return 1; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exit_code); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + DeleteProcThreadAttributeList(si.lpAttributeList); + free(si.lpAttributeList); + return (int)exit_code; +} + +int main(int argc, char* argv[]) { + /* These must be wide strings: CreateAppContainerProfile has no A variant. */ + static const wchar_t profile_name[] = L"libuv-test-appcontainer"; + static const wchar_t profile_display[] = L"libuv test"; + static const wchar_t profile_desc[] = L"AppContainer for libuv tests"; + + PSID sid = NULL; + HRESULT hr; + char cwd[MAX_PATH]; + char abs_exe[MAX_PATH]; + char tmpdir[MAX_PATH]; + char cmdline[32768]; + SECURITY_CAPABILITIES sc; + LPSTR sid_str = NULL; + int i; + int exit_code; + size_t pos; + ULONGLONG start_time; + + /* Capability SIDs to add. */ + PSID net_sid; + SID_AND_ATTRIBUTES caps[1]; + int num_caps = 0; + + start_time = GetTickCount64(); + + if (argc < 2) { + fprintf(stderr, "usage: appcontainer.exe program.exe [args...]\n"); + return 1; + } + + /* Get the current directory. */ + if (!GetCurrentDirectoryA(MAX_PATH, cwd)) { + fprintf(stderr, "GetCurrentDirectoryA failed: %lu\n", GetLastError()); + return 1; + } + + /* Resolve the exe to an absolute path. */ + if (!GetFullPathNameA(argv[1], MAX_PATH, abs_exe, NULL)) { + fprintf(stderr, "GetFullPathNameA failed: %lu\n", GetLastError()); + return 1; + } + + /* Get the temp directory. */ + if (!GetTempPathA(MAX_PATH, tmpdir)) { + fprintf(stderr, "GetTempPathA failed: %lu\n", GetLastError()); + return 1; + } + { + size_t tlen = strlen(tmpdir); + if (tlen > 3 && tmpdir[tlen - 1] == '\\') + tmpdir[tlen - 1] = '\0'; + } + + fprintf(stderr, "appcontainer: cwd=%s\n", cwd); + fprintf(stderr, "appcontainer: exe=%s\n", abs_exe); + fprintf(stderr, "appcontainer: tmp=%s\n", tmpdir); + + /* Verify the exe exists. */ + if (GetFileAttributesA(abs_exe) == INVALID_FILE_ATTRIBUTES) { + fprintf(stderr, "appcontainer: exe not found: %s (error %lu)\n", + abs_exe, GetLastError()); + return 1; + } + + /* Create capability SIDs for network access. */ + if (!ConvertStringSidToSidA(INTERNET_CLIENT_SERVER_SID, &net_sid)) { + fprintf(stderr, "appcontainer: warning: ConvertStringSidToSidA(%s): %lu\n", + INTERNET_CLIENT_SERVER_SID, GetLastError()); + } else { + caps[num_caps].Sid = net_sid; + caps[num_caps].Attributes = SE_GROUP_ENABLED; + num_caps++; + } + + /* Delete any leftover profile from a previous run. */ + DeleteAppContainerProfile(profile_name); + + /* Create the AppContainer profile. */ + { + ULONGLONG t1 = GetTickCount64(); + hr = CreateAppContainerProfile(profile_name, + profile_display, + profile_desc, + num_caps > 0 ? caps : NULL, + num_caps, + &sid); + fprintf(stderr, "appcontainer: CreateAppContainerProfile: %.3f s\n", + (GetTickCount64() - t1) / 1000.0); + } + if (FAILED(hr)) { + fprintf(stderr, "CreateAppContainerProfile failed: 0x%08lx\n", hr); + return 1; + } + + if (ConvertSidToStringSidA(sid, &sid_str)) { + fprintf(stderr, "appcontainer: SID=%s\n", sid_str); + LocalFree(sid_str); + } + + /* Grant access. */ + { + ULONGLONG t1 = GetTickCount64(); + modify_dir_and_parents(sid, cwd, 1); + modify_dir_and_parents(sid, tmpdir, 1); + modify_nul_access(sid, 1); + modify_loopback_exemption(sid, 1); + fprintf(stderr, "appcontainer: total setup grants: %.3f s\n", + (GetTickCount64() - t1) / 1000.0); + } + + /* Set up SECURITY_CAPABILITIES. */ + memset(&sc, 0, sizeof(sc)); + sc.AppContainerSid = sid; + if (num_caps > 0) { + sc.Capabilities = caps; + sc.CapabilityCount = num_caps; + } + + /* Build the command line. */ + pos = 0; + cmdline[pos++] = '"'; + { + size_t elen = strlen(abs_exe); + memcpy(&cmdline[pos], abs_exe, elen); + pos += elen; + } + cmdline[pos++] = '"'; + + for (i = 2; i < argc; i++) { + size_t arglen = strlen(argv[i]); + cmdline[pos++] = ' '; + cmdline[pos++] = '"'; + memcpy(&cmdline[pos], argv[i], arglen); + pos += arglen; + cmdline[pos++] = '"'; + } + cmdline[pos] = '\0'; + + fprintf(stderr, "appcontainer: launching: %s\n", cmdline); + + { + ULONGLONG child_start = GetTickCount64(); + exit_code = run_child(abs_exe, cmdline, &sc); + fprintf(stderr, "appcontainer: child exited with code %d (%.3f s)\n", + exit_code, (GetTickCount64() - child_start) / 1000.0); + } + + /* Tear down the sandbox: revoke all grants so we don't leave + * stale SIDs on directories, the NUL device, or the firewall. */ + { + ULONGLONG t1 = GetTickCount64(); + modify_dir_and_parents(sid, cwd, 0); + modify_dir_and_parents(sid, tmpdir, 0); + modify_nul_access(sid, 0); + modify_loopback_exemption(sid, 0); + fprintf(stderr, "appcontainer: total teardown revokes: %.3f s\n", + (GetTickCount64() - t1) / 1000.0); + } + + if (net_sid) + LocalFree(net_sid); + FreeSid(sid); + DeleteAppContainerProfile(profile_name); + fprintf(stderr, "appcontainer: elapsed %.3f s\n", + (GetTickCount64() - start_time) / 1000.0); + return exit_code; +} diff --git a/test/task.h b/test/task.h index e16af147722..c2e4edd1e83 100644 --- a/test/task.h +++ b/test/task.h @@ -50,9 +50,9 @@ #define TEST_PORT_3 9125 #ifdef _WIN32 -# define TEST_PIPENAME "\\\\.\\pipe\\uv-test" -# define TEST_PIPENAME_2 "\\\\.\\pipe\\uv-test2" -# define TEST_PIPENAME_3 "\\\\.\\pipe\\uv-test3" +# define TEST_PIPENAME "\\\\.\\pipe\\LOCAL\\uv-test" +# define TEST_PIPENAME_2 "\\\\.\\pipe\\LOCAL\\uv-test2" +# define TEST_PIPENAME_3 "\\\\.\\pipe\\LOCAL\\uv-test3" #else # define TEST_PIPENAME "uv-test-sock" # define TEST_PIPENAME_2 "uv-test-sock2" @@ -291,6 +291,14 @@ typedef enum { /* Format big numbers nicely. */ char* fmt(char (*buf)[32], double d); +#if defined(__clang__) || \ + defined(__GNUC__) || \ + defined(__INTEL_COMPILER) +# define UNUSED __attribute__((unused)) +#else +# define UNUSED +#endif + /* Reserved test exit codes. */ enum test_status { TEST_OK = 0, @@ -309,6 +317,31 @@ enum test_status { return TEST_SKIP; \ } while (0) +#ifdef _WIN32 +UNUSED static int is_in_appcontainer(void) { + HANDLE token; + DWORD ac; + DWORD len; + int ok; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return 0; + ac = 0; + len = sizeof(ac); + ok = GetTokenInformation(token, TokenIsAppContainer, &ac, len, &len); + CloseHandle(token); + return ok && ac != 0; +} + +# define RETURN_SKIP_IN_APPCONTAINER(reason) \ + do { \ + if (is_in_appcontainer()) \ + RETURN_SKIP("AppContainer: " reason); \ + } while (0) +#else +# define RETURN_SKIP_IN_APPCONTAINER(reason) do {} while (0) +#endif + #if !defined(_WIN32) # define TEST_FILE_LIMIT(num) \ @@ -330,14 +363,6 @@ enum test_status { extern int snprintf(char*, size_t, const char*, ...); #endif -#if defined(__clang__) || \ - defined(__GNUC__) || \ - defined(__INTEL_COMPILER) -# define UNUSED __attribute__((unused)) -#else -# define UNUSED -#endif - #if defined(_WIN32) #define notify_parent_process() ((void) 0) #else diff --git a/test/test-fs-readdir.c b/test/test-fs-readdir.c index bacea653587..84c877b282a 100644 --- a/test/test-fs-readdir.c +++ b/test/test-fs-readdir.c @@ -524,6 +524,8 @@ TEST_IMPL(fs_readdir_symlink) { uv_fs_t symlink_req; int r; + RETURN_SKIP_IN_APPCONTAINER("symlink creation requires elevated privilege"); + cleanup_symlink_test_files(); r = uv_fs_mkdir(uv_default_loop(), &mkdir_req, "test_symlink_dir", 0755, NULL); diff --git a/test/test-fs.c b/test/test-fs.c index 4ce059c39b3..b03c3d7ffda 100644 --- a/test/test-fs.c +++ b/test/test-fs.c @@ -1653,6 +1653,8 @@ TEST_IMPL(fs_fstat_st_dev) { char* test_file = "tmp_st_dev"; char* symlink_file = "tmp_st_dev_link"; + RETURN_SKIP_IN_APPCONTAINER("symlink creation requires elevated privilege"); + unlink(test_file); unlink(symlink_file); @@ -2589,6 +2591,7 @@ TEST_IMPL(fs_symlink_dir) { } TEST_IMPL(fs_symlink_junction) { + RETURN_SKIP_IN_APPCONTAINER("junction lstat not supported"); return test_symlink_dir_impl(UV_FS_SYMLINK_JUNCTION); } @@ -4627,6 +4630,8 @@ TEST_IMPL(fs_open_readonly_acl) { uv_fs_t req; int r; + RETURN_SKIP_IN_APPCONTAINER("cannot modify file ACLs"); + /* Based on Node.js test from https://github.com/nodejs/node/commit/3ba81e34e86a5c32658e218cb6e65b13e8326bc5 diff --git a/test/test-pipe-connect-multiple.c b/test/test-pipe-connect-multiple.c index 69a09ec6cdd..fc06e7aea0d 100644 --- a/test/test-pipe-connect-multiple.c +++ b/test/test-pipe-connect-multiple.c @@ -73,6 +73,7 @@ TEST_IMPL(pipe_connect_multiple) { #if defined(NO_SELF_CONNECT) RETURN_SKIP(NO_SELF_CONNECT); #endif + RETURN_SKIP_IN_APPCONTAINER("flaky crash in child process spawning"); int i; int r; uv_loop_t* loop; @@ -139,6 +140,7 @@ TEST_IMPL(pipe_connect_close_multiple) { #if defined(NO_SELF_CONNECT) RETURN_SKIP(NO_SELF_CONNECT); #endif + RETURN_SKIP_IN_APPCONTAINER("flaky crash in child process spawning"); int i; int r; uv_loop_t* loop; diff --git a/test/test-spawn.c b/test/test-spawn.c index eaaa8bbeac3..49d4d890488 100644 --- a/test/test-spawn.c +++ b/test/test-spawn.c @@ -1097,6 +1097,7 @@ TEST_IMPL(spawn_detect_pipe_name_collisions_on_windows) { uv_pipe_t out; char name[64]; HANDLE pipe_handle; + HANDLE pipe_handle_local; uv_stdio_container_t stdio[2]; init_process_options("spawn_helper2", exit_cb); @@ -1108,7 +1109,12 @@ TEST_IMPL(spawn_detect_pipe_name_collisions_on_windows) { options.stdio[1].data.stream = (uv_stream_t*) &out; options.stdio_count = 2; - /* Create a pipe that'll cause a collision. */ + /* Create pipes that might cause a collision. + * Inside an AppContainer, libuv uses \\?\pipe\LOCAL\uv\... and the + * non-LOCAL CreateNamedPipe will fail with access denied. + * Outside an AppContainer, libuv uses \\?\pipe\uv\... and both + * CreateNamedPipe calls succeed but only the non-LOCAL one can collide. + * Either way, uv_spawn should handle collisions gracefully. */ snprintf(name, sizeof(name), "\\\\.\\pipe\\uv\\%p-%lu", @@ -1122,7 +1128,24 @@ TEST_IMPL(spawn_detect_pipe_name_collisions_on_windows) { 65536, 0, NULL); - ASSERT_PTR_NE(pipe_handle, INVALID_HANDLE_VALUE); + + snprintf(name, + sizeof(name), + "\\\\.\\pipe\\LOCAL\\uv\\%p-%lu", + &out, + GetCurrentProcessId()); + pipe_handle_local = CreateNamedPipeA(name, + PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 10, + 65536, + 65536, + 0, + NULL); + + /* At least one must succeed. */ + ASSERT(pipe_handle != INVALID_HANDLE_VALUE || + pipe_handle_local != INVALID_HANDLE_VALUE); r = uv_spawn(uv_default_loop(), &process, &options); ASSERT_OK(r); @@ -1138,6 +1161,11 @@ TEST_IMPL(spawn_detect_pipe_name_collisions_on_windows) { printf("output is: %s", output); ASSERT_OK(strcmp("hello world\n", output)); + if (pipe_handle != INVALID_HANDLE_VALUE) + CloseHandle(pipe_handle); + if (pipe_handle_local != INVALID_HANDLE_VALUE) + CloseHandle(pipe_handle_local); + MAKE_VALGRIND_HAPPY(uv_default_loop()); return 0; } @@ -1686,6 +1714,8 @@ TEST_IMPL(spawn_fs_open) { const char dev_null[] = "/dev/null"; #endif + RETURN_SKIP_IN_APPCONTAINER("child process crashes with invalid handle"); + r = uv_fs_open(NULL, &fs_req, dev_null, UV_FS_O_RDWR, 0, NULL); ASSERT_NE(r, -1); fd = uv_get_osfhandle((uv_file) fs_req.result); diff --git a/test/test-tcp-write-fail.c b/test/test-tcp-write-fail.c index 530329a3ac6..07139cf7f16 100644 --- a/test/test-tcp-write-fail.c +++ b/test/test-tcp-write-fail.c @@ -93,6 +93,8 @@ TEST_IMPL(tcp_write_fail) { uv_tcp_t client; int r; + RETURN_SKIP_IN_APPCONTAINER("child process crashes with invalid handle"); + ASSERT_OK(uv_ip4_addr("127.0.0.1", TEST_PORT, &addr)); r = uv_tcp_init(uv_default_loop(), &client);