diff options
Diffstat (limited to 'winsup/cygwin/forkable.cc')
-rw-r--r-- | winsup/cygwin/forkable.cc | 1095 |
1 files changed, 1095 insertions, 0 deletions
diff --git a/winsup/cygwin/forkable.cc b/winsup/cygwin/forkable.cc new file mode 100644 index 0000000..cc28f9f --- /dev/null +++ b/winsup/cygwin/forkable.cc @@ -0,0 +1,1095 @@ +/* forkable.cc + + Copyright 2015 Red Hat, Inc. + +This software is a copyrighted work licensed under the terms of the +Cygwin license. Please consult the file "CYGWIN_LICENSE" for +details. */ + +#include "winsup.h" +#include "cygerrno.h" +#include "perprocess.h" +#include "sync.h" +#include "dll_init.h" +#include "environ.h" +#include "security.h" +#include "path.h" +#include "fhandler.h" +#include "dtable.h" +#include "cygheap.h" +#include "pinfo.h" +#include "shared_info.h" +#include "child_info.h" +#include "cygtls.h" +#include "exception.h" +#include <wchar.h> +#include <sys/reent.h> +#include <assert.h> +#include <tls_pbuf.h> + +/* Allow concurrent processes to use the same dll or exe + * via their hardlink while we delete our hardlink. */ +extern NTSTATUS unlink_nt_shareable (path_conv &pc); + +#define MUTEXSEP L"@" +#define PATHSEP L"\\" + +/* Create the lastsepcount directories found in ntdirname, where + counting is done along path separators (including trailing ones). + Returns true when these directories exist afterwards, false otherways. + The ntdirname is used for the path-splitting. */ +static bool +mkdirs (PWCHAR ntdirname, int lastsepcount) +{ + bool success = true; + int i = lastsepcount; + for (--i; i > 0; --i) + { + PWCHAR lastsep = wcsrchr (ntdirname, L'\\'); + if (!lastsep) + break; + *lastsep = L'\0'; + } + + for (++i; i <= lastsepcount; ++i) + { + if (success && (i == 0 || wcslen (wcsrchr (ntdirname, L'\\')) > 1)) + { + UNICODE_STRING dn; + RtlInitUnicodeString (&dn, ntdirname); + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes (&oa, &dn, 0, NULL, + sec_none_nih.lpSecurityDescriptor); + HANDLE dh = NULL; + NTSTATUS status; + IO_STATUS_BLOCK iosb; + status = NtCreateFile (&dh, GENERIC_READ | SYNCHRONIZE, + &oa, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_CREATE, + FILE_DIRECTORY_FILE + | FILE_SYNCHRONOUS_IO_NONALERT, + NULL, 0); + if (NT_SUCCESS(status)) + NtClose (dh); + else if (status != STATUS_OBJECT_NAME_COLLISION) /* already exists */ + success = false; + debug_printf ("%y = NtCreateFile (%p, dir %W)", status, dh, ntdirname); + } + if (i < lastsepcount) + ntdirname[wcslen (ntdirname)] = L'\\'; /* restore original value */ + } + return success; +} + +/* Recursively remove the directory specified in ntmaxpathbuf, + using ntmaxpathbuf as the buffer to form subsequent filenames. */ +static void +rmdirs (WCHAR ntmaxpathbuf[NT_MAX_PATH]) +{ + PWCHAR basebuf = wcsrchr (ntmaxpathbuf, L'\\'); /* find last pathsep */ + if (basebuf && *(basebuf+1)) + basebuf += wcslen (basebuf); /* last pathsep is not trailing one */ + if (!basebuf) + basebuf = ntmaxpathbuf + wcslen (ntmaxpathbuf); + *basebuf = L'\0'; /* kill trailing pathsep, if any */ + + NTSTATUS status; + HANDLE hdir = dll_list::ntopenfile (ntmaxpathbuf, &status, + FILE_DIRECTORY_FILE | + FILE_DELETE_ON_CLOSE); + if (hdir == INVALID_HANDLE_VALUE) + return; + + *basebuf++ = L'\\'; /* (re-)add trailing pathsep */ + + struct { + FILE_DIRECTORY_INFORMATION fdi; + WCHAR buf[NAME_MAX]; + } fdibuf; + IO_STATUS_BLOCK iosb; + + while (NT_SUCCESS (status = NtQueryDirectoryFile (hdir, NULL, NULL, NULL, + &iosb, + &fdibuf, sizeof (fdibuf), + FileDirectoryInformation, + FALSE, NULL, FALSE))) + { + PFILE_DIRECTORY_INFORMATION pfdi = &fdibuf.fdi; + while (true) + { + int namelen = pfdi->FileNameLength / sizeof (WCHAR); + wcsncpy (basebuf, pfdi->FileName, namelen); + basebuf[namelen] = L'\0'; + + if (pfdi->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + if (wcscmp (basebuf, L".") && wcscmp (basebuf, L"..")) + rmdirs (ntmaxpathbuf); + } + else + { + UNICODE_STRING fn; + RtlInitUnicodeString (&fn, ntmaxpathbuf); + + path_conv pc (&fn); + unlink_nt_shareable (pc); /* move to bin */ + } + + if (!pfdi->NextEntryOffset) + break; + pfdi = (PFILE_DIRECTORY_INFORMATION)((caddr_t)pfdi + + pfdi->NextEntryOffset); + } + } + if (status != STATUS_NO_MORE_FILES) + debug_printf ("%y = NtQueryDirectoryFile (%p, io %y, info %d)", + status, hdir, iosb.Status, iosb.Information); + + CloseHandle (hdir); +} + +/* Get the NTFS file id for the real file behind the dll handle. + As we may open a wrong (or no) file while the dll is renamed, + we retry until we get the same file id a second time. + We use NtQueryVirtualMemory (MemorySectionName) for the current + file name, as GetModuleFileNameW () yields the as-loaded name. + While we have the file handle open, also read the attributes. + NOTE: Uses dll_list::nt_max_path_buf (). */ +static bool +stat_real_file_once (dll *d) +{ + if (d->fbi.FileAttributes != INVALID_FILE_ATTRIBUTES) + return true; + + tmp_pathbuf tp; + + HANDLE fhandle = INVALID_HANDLE_VALUE; + NTSTATUS status, fstatus; + PMEMORY_SECTION_NAME pmsi1; + MEMORY_SECTION_NAME msi2; + pmsi1 = (PMEMORY_SECTION_NAME) dll_list::nt_max_path_buf (); + RtlInitEmptyUnicodeString (&msi2.SectionFileName, tp.w_get (), 65535); + + /* Retry opening the real file name until that does not change any more. */ + status = NtQueryVirtualMemory (NtCurrentProcess (), d->handle, + MemorySectionName, pmsi1, 65536, NULL); + while (NT_SUCCESS (status) && + !RtlEqualUnicodeString (&msi2.SectionFileName, + &pmsi1->SectionFileName, FALSE)) + { + RtlCopyUnicodeString (&msi2.SectionFileName, &pmsi1->SectionFileName); + if (fhandle != INVALID_HANDLE_VALUE) + NtClose (fhandle); + pmsi1->SectionFileName.Buffer[pmsi1->SectionFileName.Length] = L'\0'; + fhandle = dll_list::ntopenfile (pmsi1->SectionFileName.Buffer, &fstatus); + status = NtQueryVirtualMemory (NtCurrentProcess (), d->handle, + MemorySectionName, pmsi1, 65536, NULL); + } + if (!NT_SUCCESS (status)) + system_printf ("WARNING: Unable (ntstatus %y) to query real file for %W", + status, d->ntname); + else if (fhandle == INVALID_HANDLE_VALUE) + system_printf ("WARNING: Unable (ntstatus %y) to open real file for %W", + fstatus, d->ntname); + if (fhandle == INVALID_HANDLE_VALUE) + return false; + + if (!dll_list::read_fii (fhandle, &d->fii) || + !dll_list::read_fbi (fhandle, &d->fbi)) + system_printf ("WARNING: Unable to read real file attributes for %W", + pmsi1->SectionFileName.Buffer); + + NtClose (fhandle); + return d->fbi.FileAttributes != INVALID_FILE_ATTRIBUTES; +} + +/* easy use of NtOpenFile */ +HANDLE +dll_list::ntopenfile (PCWCHAR ntname, NTSTATUS *pstatus, ULONG openopts, + ACCESS_MASK access, HANDLE rootDir) +{ + NTSTATUS status; + if (!pstatus) + pstatus = &status; + + UNICODE_STRING fn; + if (openopts & FILE_OPEN_BY_FILE_ID) + RtlInitCountedUnicodeString (&fn, ntname, 8); + else + RtlInitUnicodeString (&fn, ntname); + + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes (&oa, &fn, 0, rootDir, NULL); + + access |= FILE_READ_ATTRIBUTES; + if (openopts & FILE_DELETE_ON_CLOSE) + access |= DELETE; + if (openopts & FILE_DIRECTORY_FILE) + access |= FILE_LIST_DIRECTORY; + else + openopts |= FILE_NON_DIRECTORY_FILE; + + access |= SYNCHRONIZE; + openopts |= FILE_SYNCHRONOUS_IO_NONALERT; + + HANDLE fh = INVALID_HANDLE_VALUE; + ULONG share = FILE_SHARE_VALID_FLAGS; + IO_STATUS_BLOCK iosb; + *pstatus = NtOpenFile (&fh, access, &oa, &iosb, share, openopts); + if (openopts & FILE_OPEN_BY_FILE_ID) + debug_printf ("%y = NtOpenFile (%p, a %xh, sh %xh, o %xh, io %y, by id %llX)", + *pstatus, fh, access, share, openopts, iosb.Status, *(LONGLONG*)fn.Buffer); + else + debug_printf ("%y = NtOpenFile (%p, a %xh, sh %xh, o %xh, io %y, '%W')", + *pstatus, fh, access, share, openopts, iosb.Status, fn.Buffer); + + return NT_SUCCESS(*pstatus) ? fh : INVALID_HANDLE_VALUE; +} + +bool +dll_list::read_fii (HANDLE fh, PFILE_INTERNAL_INFORMATION pfii) +{ + pfii->IndexNumber.QuadPart = -1LL; + + NTSTATUS status; + IO_STATUS_BLOCK iosb; + status = NtQueryInformationFile (fh, &iosb, + pfii, sizeof (*pfii), + FileInternalInformation); + if (!NT_SUCCESS (status)) + { + system_printf ("WARNING: %y = NtQueryInformationFile (%p," + " InternalInfo, io.Status %y)", + status, fh, iosb.Status); + pfii->IndexNumber.QuadPart = -1LL; + return false; + } + return true; +} + +bool +dll_list::read_fbi (HANDLE fh, PFILE_BASIC_INFORMATION pfbi) +{ + pfbi->FileAttributes = INVALID_FILE_ATTRIBUTES; + pfbi->LastWriteTime.QuadPart = -1LL; + + NTSTATUS status; + IO_STATUS_BLOCK iosb; + status = NtQueryInformationFile (fh, &iosb, + pfbi, sizeof (*pfbi), + FileBasicInformation); + if (!NT_SUCCESS (status)) + { + system_printf ("WARNING: %y = NtQueryInformationFile (%p," + " BasicInfo, io.Status %y)", + status, fh, iosb.Status); + pfbi->FileAttributes = INVALID_FILE_ATTRIBUTES; + pfbi->LastWriteTime.QuadPart = -1LL; + return false; + } + return true; +} + +/* Into buf if not NULL, write the IndexNumber in pli. + Return the number of characters (that would be) written. */ +static int +format_IndexNumber (PWCHAR buf, ssize_t bufsize, LARGE_INTEGER const *pli) +{ + if (!buf) + return 16; + if (bufsize >= 0 && bufsize <= 16) + return 0; + return __small_swprintf (buf, L"%016X", pli->QuadPart); +} + +/* Into buf if not NULL, write the ntname of cygwin installation_root. + Return the number of characters (that would be) written. */ +static int +rootname (PWCHAR buf, ssize_t bufsize) +{ + UNICODE_STRING &cygroot = cygheap->installation_root; + if (!buf) + return 6 /* "\??\UN" */ + cygroot.Length / sizeof (WCHAR); + return dll_list::form_ntname (buf, bufsize, cygroot.Buffer) - buf; +} + +/* Into buf if not NULL, write the string representation of current user sid. + Return the number of characters (that would be) written. */ +static int +sidname (PWCHAR buf, ssize_t bufsize) +{ + if (!buf) + return 128; + if (bufsize >= 0 && bufsize <= 128) + return 0; + UNICODE_STRING sid; + WCHAR sidbuf[128+1]; + RtlInitEmptyUnicodeString (&sid, sidbuf, sizeof (sidbuf)); + RtlConvertSidToUnicodeString (&sid, cygheap->user.sid (), FALSE); + return wcpcpy (buf, sid.Buffer) - buf; +} + +/* Into buf if not NULL, write the IndexNumber of the main executable. + Return the number of characters (that would be) written. */ +static int +exename (PWCHAR buf, ssize_t bufsize) +{ + if (!buf) + return format_IndexNumber (NULL, bufsize, NULL); + dll *d = dlls.main_executable; + return format_IndexNumber (buf, bufsize, &d->fii.IndexNumber); +} + +/* Into buf if not NULL, write the newest dll's LastWriteTime. + Return the number of characters (that would be) written. */ +static int +lwtimename (PWCHAR buf, ssize_t bufsize) +{ + if (!buf) + return sizeof (LARGE_INTEGER) * 2; + if (bufsize >= 0 && bufsize <= (int)sizeof (LARGE_INTEGER) * 2) + return 0; + + LARGE_INTEGER newest = { 0 }; + /* Need by-handle-file-information for _all_ loaded dlls, + as most recent ctime forms the hardlinks directory. */ + dll *d = &dlls.start; + while ((d = d->next)) + { + /* LastWriteTime more properly tells the last file-content modification + time, because a newly created hardlink may have a different + CreationTime compared to the original file. */ + if (d->fbi.LastWriteTime.QuadPart > newest.QuadPart) + newest = d->fbi.LastWriteTime; + } + + return __small_swprintf (buf, L"%016X", newest); +} + +struct namepart { + PCWCHAR text; /* used when no pathfunc, description otherwise */ + int (*textfunc)(PWCHAR buf, ssize_t bufsize); + bool mutex_from_dir; /* on path-separators add mutex-separator */ + bool create_dir; +}; +/* mutex name is formed along dir names */ +static namepart NO_COPY_RO const +forkable_nameparts[] = { + /* text textfunc mutex_from_dir create */ + { L"<cygroot>", rootname, false, false, }, + { L"\\var\\run\\", NULL, false, false, }, + { L"cygfork", NULL, true, false, }, + { L"<sid>", sidname, true, true, }, + { L"<exe>", exename, false, false, }, + { MUTEXSEP, NULL, false, false, }, + { L"<ctime>", lwtimename, true, true, }, + + { NULL, NULL }, +}; + +/* Nominate the hardlink to an individual DLL inside dirx_name, + that ends with the path separator (hence the "x" varname). + With NULL as dirx_name, never nominate the hardlink any more. + With "" as dirx_name, denominate the hardlink. */ +void +dll::nominate_forkable (PCWCHAR dirx_name) +{ + if (!dirx_name) + { + debug_printf ("type %d disable %W", type, ntname); + forkable_ntname = NULL; /* never create a hardlink for this dll */ + } + + if (!forkable_ntname) + return; + + PWCHAR next = wcpcpy (forkable_ntname, dirx_name); + + if (!*forkable_ntname) + return; /* denominate */ + + if (type < DLL_LOAD) + wcpcpy (next, modname); + else + { + /* Avoid lots of extra directories for loaded dll's: + * mangle full path into one single directory name, + * just keep original filename intact. The original + * filename is necessary to serve as linked + * dependencies of dynamically loaded dlls. */ + PWCHAR lastpathsep = wcsrchr (ntname, L'\\'); + if (!lastpathsep) + { + forkable_ntname = NULL; + return; + } + *lastpathsep = L'\0'; + HANDLE fh = dll_list::ntopenfile (ntname, NULL, FILE_DIRECTORY_FILE); + *lastpathsep = L'\\'; + + FILE_INTERNAL_INFORMATION fii = { 0 }; + if (fh != INVALID_HANDLE_VALUE) + { + dll_list::read_fii (fh, &fii); + NtClose (fh); + } + next += format_IndexNumber (next, -1, &fii.IndexNumber); + wcpcpy (next, lastpathsep); + } +} + +/* Create the nominated hardlink for one indivitual dll, + inside another subdirectory when dynamically loaded. */ +bool +dll::create_forkable () +{ + if (!forkable_ntname || !*forkable_ntname) + return true; /* disabled */ + + PWCHAR ntname = forkable_ntname; + + PWCHAR last = NULL; + bool success = true; + if (type >= DLL_LOAD) + { + last = wcsrchr (ntname, L'\\'); + if (!last) + return false; + *last = L'\0'; + success = mkdirs (ntname, 1); + *last = L'\\'; + if (!success) + return false; + } + + /* open device as parent handle for FILE_OPEN_BY_FILE_ID */ + PWCHAR devname = dll_list::nt_max_path_buf (); + PWCHAR n = ntname; + PWCHAR d = devname; + int pathseps = 0; + while (*n) + { + if (*d == L'\\' && ++pathseps > 4) + break; // "\\??\\UNC\\server\\share" + *d = *n++; + if (*d++ == L':') + break; // "\\??\\C:" + } + *d = L'\0'; + + HANDLE devhandle = dll_list::ntopenfile (devname); + if (devhandle == INVALID_HANDLE_VALUE) + return false; /* impossible */ + + HANDLE fh = dll_list::ntopenfile ((PCWCHAR)&fii.IndexNumber, NULL, + FILE_OPEN_BY_FILE_ID, + FILE_WRITE_ATTRIBUTES, + devhandle); + NtClose (devhandle); + if (fh == INVALID_HANDLE_VALUE) + return false; /* impossible */ + + int ntlen = wcslen (ntname); + int bufsize = sizeof (FILE_LINK_INFORMATION) + ntlen * sizeof (*ntname); + PFILE_LINK_INFORMATION pfli = (PFILE_LINK_INFORMATION) alloca (bufsize); + + wcscpy (pfli->FileName, ntname); + + pfli->FileNameLength = ntlen * sizeof (*ntname); + pfli->ReplaceIfExists = FALSE; /* allow concurrency */ + pfli->RootDirectory = NULL; + + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtSetInformationFile (fh, &iosb, pfli, bufsize, + FileLinkInformation); + NtClose (fh); + debug_printf ("%y = NtSetInformationFile (%p, FileLink %W, iosb.Status %y)", + status, fh, pfli->FileName, iosb.Status); + if (NT_SUCCESS (status) || status == STATUS_OBJECT_NAME_COLLISION) + /* We've not found a performant way yet to protect fork against updates + to main executables and/or dlls that do not reside on the same NTFS + filesystem as the <cygroot>/var/run/cygfork/ directory. + But as long as the main executable can be hardlinked, dll redirection + works for any other hardlink-able dll, while non-hardlink-able dlls + are used from their original location. */ + return true; + + return false; +} + +/* return the number of characters necessary to store one forkable name */ +size_t +dll_list::forkable_ntnamesize (dll_type type, PCWCHAR fullntname, PCWCHAR modname) +{ + if (forkables_needs == forkables_impossible) + return 0; + + if (!forkables_dirx_size) + { + DWORD forkables_mutex_size = 0; + bool needsep = false; + for (namepart const *part = forkable_nameparts; part->text; ++part) + { + if (needsep) + { + forkables_dirx_size += wcslen (PATHSEP); + forkables_mutex_size += wcslen (MUTEXSEP); + } + needsep = part->mutex_from_dir; + int len = 0; + if (part->textfunc) + len = part->textfunc (NULL, 0); + else + len = wcslen (part->text); + forkables_dirx_size += len; + forkables_mutex_size += len; + } + /* trailing path sep */ + forkables_dirx_size += wcslen (PATHSEP); + /* trailing zeros */ + ++forkables_dirx_size; + ++forkables_mutex_size; + + /* allocate here, to avoid cygheap size changes during fork */ + forkables_dirx_ntname = (PWCHAR) cmalloc (HEAP_2_DLL, + (forkables_dirx_size + forkables_mutex_size) * + sizeof (*forkables_dirx_ntname)); + *forkables_dirx_ntname = L'\0'; + + forkables_mutex_name = forkables_dirx_ntname + forkables_dirx_size; + *forkables_mutex_name = L'\0'; + } + + size_t ret = forkables_dirx_size; + if (type >= DLL_LOAD) + ret += format_IndexNumber (NULL, -1, NULL) + 1; /* one more directory */ + return ret + wcslen (modname); +} + +/* Prepare top-level names necessary to nominate individual DLL hardlinks, + eventually releasing/removing previous forkable hardlinks. */ +void +dll_list::prepare_forkables_nomination () +{ + if (!forkables_dirx_ntname) + return; + + dll *d = &dlls.start; + while ((d = d->next)) + stat_real_file_once (d); /* uses nt_max_path_buf () */ + + PWCHAR pbuf = nt_max_path_buf (); + + bool needsep = false; + bool domutex = false; + namepart const *part; + for (part = forkable_nameparts; part->text; ++part) + { + if (part->mutex_from_dir) + domutex = true; /* mutex naming starts with first mutex_from_dir */ + if (!domutex) + continue; + if (needsep) + pbuf += __small_swprintf (pbuf, L"%W", MUTEXSEP); + needsep = part->mutex_from_dir; + if (part->textfunc) + pbuf += part->textfunc (pbuf, -1); + else + pbuf += __small_swprintf (pbuf, L"%W", part->text); + } + + if (!wcscmp (forkables_mutex_name, nt_max_path_buf ())) + return; /* nothing changed */ + + if (*forkables_mutex_name && + wcscmp (forkables_mutex_name, nt_max_path_buf ())) + { + /* The mutex name has changed since last fork and we either have + dlopen'ed a more recent or dlclose'd the most recent dll, + so we will not use the current forkable hardlinks any more. + Removing from the file system is done later, upon exit. */ + close_mutex (); + denominate_forkables (); + } + wcscpy (forkables_mutex_name, nt_max_path_buf ()); + + pbuf = forkables_dirx_ntname; + needsep = false; + for (namepart const *part = forkable_nameparts; part->text; ++part) + { + if (needsep) + pbuf += __small_swprintf (pbuf, L"%W", PATHSEP); + needsep = part->mutex_from_dir; + if (part->textfunc) + pbuf += part->textfunc (pbuf, -1); + else + pbuf += __small_swprintf (pbuf, L"%W", part->text); + } + pbuf += __small_swprintf (pbuf, L"%W", PATHSEP); + + debug_printf ("forkables dir %W", forkables_dirx_ntname); + debug_printf ("forkables mutex %W", forkables_mutex_name); +} + +/* Test if creating hardlinks is necessary. If creating hardlinks is possible + in general, each individual dll is tested if its previously created + hardlink (if any, or the original file) still is the same. + Testing is protected against hardlink removal by concurrent processes. */ +void +dll_list::update_forkables_needs () +{ + dll *d; + + if (forkables_needs == forkables_unknown) + { + /* check if filesystem of forkables dir is NTFS */ + PWCHAR pbuf = nt_max_path_buf (); + for (namepart const *part = forkable_nameparts; part->text; ++part) + { + if (part->mutex_from_dir) + break; /* leading non-mutex-naming dirs, must exist anyway */ + if (part->textfunc) + pbuf += part->textfunc (pbuf, -1); + else + pbuf += __small_swprintf (pbuf, L"%W", part->text); + } + + UNICODE_STRING fn; + RtlInitUnicodeString (&fn, nt_max_path_buf ()); + + fs_info fsi; + if (fsi.update (&fn, NULL) && +/* FIXME: !fsi.is_readonly () && */ + fsi.is_ntfs ()) + forkables_needs = forkables_disabled; /* check directory itself */ + else + { + debug_printf ("impossible, not on NTFS %W", fn.Buffer); + forkables_needs = forkables_impossible; + } + } + + if (forkables_needs == forkables_impossible) + return; /* we have not created any hardlink, nothing to clean up */ + + if (forkables_needs == forkables_disabled || + forkables_needs == forkables_needless || + forkables_needs == forkables_created) + { + /* (re-)check existence of forkables dir */ + PWCHAR pbuf = nt_max_path_buf (); + for (namepart const *part = forkable_nameparts; part->text; ++part) + { + if (part->textfunc) + pbuf += part->textfunc (pbuf, -1); + else + pbuf += __small_swprintf (pbuf, L"%W", part->text); + if (part->mutex_from_dir) + break; /* up to first mutex-naming dir */ + } + pbuf = nt_max_path_buf (); + + HANDLE dh = ntopenfile (pbuf, NULL, FILE_DIRECTORY_FILE); + if (dh != INVALID_HANDLE_VALUE) + { + NtClose (dh); + if (forkables_needs == forkables_disabled) + forkables_needs = forkables_needless; + } + else if (forkables_needs != forkables_disabled) + { + debug_printf ("disabled, disappearing %W", pbuf); + close_mutex (); + denominate_forkables (); + forkables_needs = forkables_disabled; + } + else + debug_printf ("disabled, missing %W", pbuf); + } + + if (forkables_needs == forkables_disabled) + return; + + if (forkables_needs == forkables_created) + { + /* already have created hardlinks in this process, ... */ + forkables_needs = forkables_needless; + d = &start; + while ((d = d->next) != NULL) + if (d->forkable_ntname && !*d->forkable_ntname) + { + /* ... but another dll was loaded since last fork */ + debug_printf ("needed, since last fork loaded %W", d->ntname); + forkables_needs = forkables_needed; + break; + } + } + + if (forkables_needs > forkables_needless) + return; /* no need to check anything else */ + + if (forkables_needs != forkables_needless) + { + /* paranoia */ + system_printf ("WARNING: invalid forkables_needs value %d", + forkables_needs); + return; + } + + forkables_needs = forkables_needed; +} + +/* Create the nominated forkable hardlinks and directories as necessary, + mutex-protected to avoid concurrent processes removing them. */ +bool +dll_list::update_forkables () +{ + /* existence of mutex indicates that we use these hardlinks */ + if (!forkables_mutex) + { + /* neither my parent nor myself did have need for hardlinks yet */ + forkables_mutex = CreateMutexW (&sec_none, FALSE, + forkables_mutex_name); + debug_printf ("%p = CreateMutexW (%W): %E", + forkables_mutex, forkables_mutex_name); + if (!forkables_mutex) + return false; + + /* Make sure another process does not rmdirs_synchronized () */ + debug_printf ("WFSO (%p, %W, inf)...", + forkables_mutex, forkables_mutex_name); + DWORD ret = WaitForSingleObject (forkables_mutex, INFINITE); + debug_printf ("%u = WFSO (%p, %W)", + ret, forkables_mutex, forkables_mutex_name); + switch (ret) + { + case WAIT_OBJECT_0: + case WAIT_ABANDONED: + break; + default: + system_printf ("cannot wait for mutex %W: %E", + forkables_mutex_name); + return false; + } + + BOOL bret = ReleaseMutex (forkables_mutex); + debug_printf ("%d = ReleaseMutex (%p, %W)", + bret, forkables_mutex, forkables_mutex_name); + } + + return create_forkables (); +} + +/* Create the nominated forkable hardlinks and directories as necessary, + as well as the .local file for dll-redirection. */ +bool +dll_list::create_forkables () +{ + bool success = true; + + int lastsepcount = 1; /* we have trailing pathsep */ + for (namepart const *part = forkable_nameparts; part->text; ++part) + if (part->create_dir) + ++lastsepcount; + + PWCHAR ntname = nt_max_path_buf (); + wcsncpy (ntname, forkables_dirx_ntname, NT_MAX_PATH); + + if (!mkdirs (ntname, lastsepcount)) + success = false; + + if (success) + { + /* create the DotLocal file as empty file */ + wcsncat (ntname, main_executable->modname, NT_MAX_PATH); + wcsncat (ntname, L".local", NT_MAX_PATH); + + UNICODE_STRING fn; + RtlInitUnicodeString (&fn, ntname); + + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes (&oa, &fn, 0, NULL, + sec_none_nih.lpSecurityDescriptor); + HANDLE hlocal = NULL; + NTSTATUS status; + IO_STATUS_BLOCK iosb; + status = NtCreateFile (&hlocal, GENERIC_WRITE | SYNCHRONIZE, + &oa, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_CREATE, + FILE_NON_DIRECTORY_FILE + | FILE_SYNCHRONOUS_IO_NONALERT, + NULL, 0); + if (NT_SUCCESS (status)) + CloseHandle (hlocal); + else if (status != STATUS_OBJECT_NAME_COLLISION) /* already exists */ + success = false; + debug_printf ("%y = NtCreateFile (%p, %W)", status, hlocal, ntname); + } + + if (success) + { + dll *d = &start; + while ((d = d->next)) + if (!d->create_forkable ()) + d->nominate_forkable (NULL); /* never again */ + debug_printf ("hardlinks created"); + } + + return success; +} + +static void +rmdirs_synchronized (WCHAR ntbuf[NT_MAX_PATH], int depth, int maxdepth, + PFILE_DIRECTORY_INFORMATION pfdi, ULONG fdisize) +{ + if (depth == maxdepth) + { + debug_printf ("sync on %W", ntbuf); + /* calculate mutex name from path parts, using + full path name length to allocate mutex name buffer */ + WCHAR mutexname[wcslen (ntbuf)]; + mutexname[0] = L'\0'; + PWCHAR mutexnext = mutexname; + + /* mutex name is formed by dir names */ + int pathcount = 0; + for (namepart const *part = forkable_nameparts; part->text; ++part) + if (part->mutex_from_dir) + ++pathcount; + + PWCHAR pathseps[pathcount]; + + /* along the path separators split needed path parts */ + int i = pathcount; + while (--i >= 0) + if ((pathseps[i] = wcsrchr (ntbuf, L'\\'))) + *pathseps[i] = L'\0'; + else + return; /* something's wrong */ + + /* build the mutex name from dir names */ + for (i = 0; i < pathcount; ++i) + { + if (i > 0) + mutexnext = wcpcpy (mutexnext, MUTEXSEP); + mutexnext = wcpcpy (mutexnext, &pathseps[i][1]); + *pathseps[i] = L'\\'; /* restore full path */ + } + + HANDLE mutex = CreateMutexW (&sec_none_nih, TRUE, mutexname); + DWORD lasterror = GetLastError (); + debug_printf ("%p = CreateMutexW (%W): %E", mutex, mutexname); + if (mutex) + { + if (lasterror != ERROR_ALREADY_EXISTS) + { + debug_printf ("cleaning up for mutex %W", mutexname); + rmdirs (ntbuf); + } + BOOL bret = CloseHandle (mutex); + debug_printf ("%d = CloseHandle (%p, %W): %E", + bret, mutex, mutexname); + } + return; + } + + IO_STATUS_BLOCK iosb; + NTSTATUS status; + + HANDLE hdir = dll_list::ntopenfile (ntbuf, &status, + FILE_DIRECTORY_FILE | + (depth ? FILE_DELETE_ON_CLOSE : 0)); + if (hdir == INVALID_HANDLE_VALUE) + return; + + PWCHAR plast = ntbuf + wcslen (ntbuf); + while (NT_SUCCESS (status = NtQueryDirectoryFile (hdir, + NULL, NULL, NULL, &iosb, + pfdi, fdisize, + FileDirectoryInformation, + TRUE, NULL, FALSE))) + if (pfdi->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + int namelen = pfdi->FileNameLength / sizeof (WCHAR); + if (!wcsncmp (pfdi->FileName, L".", namelen) || + !wcsncmp (pfdi->FileName, L"..", namelen)) + continue; + *plast = L'\\'; + wcsncpy (plast+1, pfdi->FileName, namelen); + plast[1+namelen] = L'\0'; + rmdirs_synchronized (ntbuf, depth+1, maxdepth, pfdi, fdisize); + *plast = L'\0'; + } + if (status != STATUS_NO_MORE_FILES) + debug_printf ("%y = NtQueryDirectoryFile (%p, io %y, info %d)", + status, hdir, iosb.Status, iosb.Information); + CloseHandle (hdir); +} + +/* Try to lock the mutex handle with almost no timeout, then close the + mutex handle. Locking before closing is to get the mutex closing + promoted synchronously, otherways we might end up with no one + succeeding in create-with-lock, which is the precondition + to actually remove the hardlinks from the filesystem. */ +bool +dll_list::close_mutex () +{ + if (!forkables_mutex || !*forkables_mutex_name) + return false; + + HANDLE hmutex = forkables_mutex; + forkables_mutex = NULL; + + bool locked = false; + DWORD ret = WaitForSingleObject (hmutex, 1); + debug_printf ("%u = WFSO (%p, %W, 1)", + ret, hmutex, forkables_mutex_name); + switch (ret) + { + case WAIT_OBJECT_0: + case WAIT_ABANDONED: + locked = true; + break; + case WAIT_TIMEOUT: + break; + default: + system_printf ("error locking mutex %W: %E", forkables_mutex_name); + break; + } + BOOL bret = CloseHandle (hmutex); + debug_printf ("%d = CloseHandle (%p, %W): %E", + bret, hmutex, forkables_mutex_name); + return locked; +} + +/* Release the forkable hardlinks, and remove them if the + mutex can be create-locked after locked-closing. */ +void +dll_list::cleanup_forkables () +{ + if (!forkables_dirx_ntname) + return; + + bool locked = close_mutex (); + + /* Start the removal below with current forkables dir, + which is cleaned in denominate_forkables (). */ + PWCHAR buf = nt_max_path_buf (); + PWCHAR pathsep = wcpncpy (buf, forkables_dirx_ntname, NT_MAX_PATH); + buf[NT_MAX_PATH-1] = L'\0'; + + denominate_forkables (); + + if (!locked) + return; + + /* drop last path separator */ + while (--pathsep >= buf && *pathsep != L'\\'); + *pathsep = L'\0'; + + try_remove_forkables (buf, pathsep - buf, NT_MAX_PATH); +} + +void +dll_list::try_remove_forkables (PWCHAR dirbuf, size_t dirlen, size_t dirbufsize) +{ + /* Instead of just the current forkables, try to remove any forkables + found, to ensure some cleanup even in situations like power-loss. */ + PWCHAR end = dirbuf + wcslen (dirbuf); + int backcount = 0; + for (namepart const *part = forkable_nameparts; part->text; ++part) + if (part->create_dir) + { + /* drop one path separator per create_dir */ + while (--end >= dirbuf && *end != L'\\'); + if (end < dirbuf) + return; + *end = L'\0'; + ++backcount; + } + + /* reading one at a time to reduce stack pressure */ + struct { + FILE_DIRECTORY_INFORMATION fdi; + WCHAR buf[NAME_MAX]; + } fdibuf; + rmdirs_synchronized (dirbuf, 0, backcount, &fdibuf.fdi, sizeof (fdibuf)); +} + +void +dll_list::denominate_forkables () +{ + *forkables_dirx_ntname = L'\0'; + *forkables_mutex_name = L'\0'; + + dll *d = &start; + while ((d = d->next)) + d->nominate_forkable (forkables_dirx_ntname); +} + +/* Set or clear HANDLE_FLAG_INHERIT for all handles necessary + to maintain forkables-hardlinks. */ +void +dll_list::set_forkables_inheritance (bool inherit) +{ + DWORD mask = HANDLE_FLAG_INHERIT; + DWORD flags = inherit ? HANDLE_FLAG_INHERIT : 0; + + if (forkables_mutex) + SetHandleInformation (forkables_mutex, mask, flags); +} + +/* create the forkable hardlinks, if necessary */ +void +dll_list::request_forkables () +{ + if (!forkables_dirx_ntname) + return; + + /* Even on forkables_impossible, keep the number of open handles + stable across the fork, and close them when releasing only. */ + prepare_forkables_nomination (); + + update_forkables_needs (); + + set_forkables_inheritance (true); + + if (forkables_needs <= forkables_needless) + return; + + dll *d = &start; + while ((d = d->next)) + d->nominate_forkable (forkables_dirx_ntname); + + bool updated = update_forkables (); + + if (!updated) + forkables_needs = forkables_needless; + else + forkables_needs = forkables_created; +} + + +void +dll_list::release_forkables () +{ + if (!forkables_dirx_ntname) + return; + + set_forkables_inheritance (false); + + if (forkables_needs == forkables_impossible) + { + cleanup_forkables (); + + dll *d = &start; + while ((d = d->next)) + d->forkable_ntname = NULL; + + cfree (forkables_dirx_ntname); + forkables_dirx_ntname = NULL; + forkables_mutex_name = NULL; + } +} |