Discussion:
RFC: [PATCH 0/6] When fork fails, retry with hardlinks.
Michael Haubenwallner
2016-12-07 10:58:24 UTC
Permalink
Hi Corinna (et al.),

now with NtQueryVirtualMemory(MemorySectionName) I've been able to drop
the need to early open any file, and any check for modified binaries.
Instead, only the failing fork call is the trigger to create the
hardlinks (if possible) - followed by a second fork call then.

One question: During dlopen rework I've learned about tmp_pathbuf:
Unlike in child process startup, where tmp_pathbuf is not initialised
yet (have nt_max_path_buffer instead), do you see a reason to not use
tmp_pathbuf during hardlink creation in parent process (patch 3)?

The idea there is to really catch the right file for when that file is
just being renamed - that needs the second filename buffer to compare.

Other than that: What do you think about these patches now?

Thanks!
/haubi/
Michael Haubenwallner
2016-12-07 10:58:30 UTC
Permalink
* faq-api.xml: Mention hardlink creation by fork.
* highlights.xml: Describe hardlink creation.
---
winsup/doc/faq-api.xml | 5 +++++
winsup/doc/highlights.xml | 41 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 46 insertions(+)

diff --git a/winsup/doc/faq-api.xml b/winsup/doc/faq-api.xml
index 993274a..e584cec 100644
--- a/winsup/doc/faq-api.xml
+++ b/winsup/doc/faq-api.xml
@@ -155,6 +155,11 @@ child, releases the mutex the child is waiting on and returns from the
fork call. Child wakes from blocking on mutex, recreates any mmapped
areas passed to it via shared area and then returns from fork itself.
</para>
+<para>When the executable or any dll in use by the parent was renamed or
+moved into the hidden recycle bin, fork retries with creating hardlinks
+for the old executable and any dll into per-user subdirectories in the
+/var/run/cygfork/ directory, when that one exists and resides on NTFS.
+</para>
</answer></qandaentry>

<qandaentry id="faq.api.globbing">
diff --git a/winsup/doc/highlights.xml b/winsup/doc/highlights.xml
index ec9fcd6..25b2273 100644
--- a/winsup/doc/highlights.xml
+++ b/winsup/doc/highlights.xml
@@ -195,6 +195,47 @@ difficult to implement correctly. Currently, the Cygwin fork is a
non-copy-on-write implementation similar to what was present in early
flavors of UNIX.</para>

+<para>As the child process is created as new process, both the main
+executable and all the dlls loaded either statically or dynamically have
+to be identical as to when the parent process has started or loaded a dll.
+While Windows does not allow to remove binaries in use from the file
+system, they still can be renamed or moved into the recycle bin, as
+outlined for unlink(2) in <xref linkend="ov-new1.7-file"></xref>.
+To allow an existing process to fork, the original binary files need to be
+available via their original file names, but they may reside in
+different directories when using the <ulink
+url="https://social.msdn.microsoft.com/search/en-US?query=dotlocal%20dll%20redirection"
+>DotLocal (.local) Dll Redirection</ulink> feature.
+Since NTFS does support hardlinks, when the fork fails we try again, but
+create a private directory containing hardlinks to the original files as
+well as the .local file now. The private directory for the hardlinks is
+/var/run/cygfork/, which you have to create manually for now if you need to
+protect fork against exe- and dll- updates on your Cygwin instance. As
+hardlinks cannot be used across multiple NTFS file systems, please make sure
+your exe- and dll- replacing operations operate on the same single NTFS file
+system as your Cygwin instance and the /var/run/cygfork/ directory.</para>
+
+<para>We create one directory per user, application and application age,
+and remove it when no more processes use that directory. To indicate
+whether a directory still is in use, we define a mutex name similar to
+the directory name. As mutexes are destroyed when no process holds a
+handle open any more, we can clean up even after power loss or similar:
+Both the parent and child process, at exit they lock the mutex with
+almost no timeout and close it, to get the closure promoted synchronously.
+If the lock succeeded before closing, directory cleanup is started:
+For each directory found, the corresponding mutex is created with lock.
+If that succeeds, the directory is removed, as it is unused now, and the
+corresponding mutex handle is closed.</para>
+
+<para>Before fork, when about to create hardlinks for the first time, the
+mutex is opened and locked with infinite timeout, to wait for the cleanup
+that may run at the same time. Once locked, the mutex is unlocked
+immediately, but the mutex handle stays open until exit, and the hardlinks
+are created. It is fine for multiple processes to concurrently create
+the same hardlinks, as the result really should be identical. Once the
+mutex is open, we can create more hardlinks within this one directory
+without the need to lock the mutex again.</para>
+
<para>The first thing that happens when a parent process
forks a child process is that the parent initializes a space in the
Cygwin process table for the child. It then creates a suspended
--
2.7.3
Michael Haubenwallner
2016-12-07 10:58:26 UTC
Permalink
Even for the main executable and cygwin1.dll store the file name as full
NT path. Create the child process using the main executable's file name
converted from the full NT path stored before.

* dll_init.cc (dll_list::alloc): Search for DLL_SELF type entry
with module name like for DLL_LINK, use full NT path to search
for DLL_LOAD type only. For DLL_SELF type do not indicate
having a destructor to be called.
(dll_list::find): Ignore DLL_SELF type entries.
(dll_list::init): Ditto. Call track_self method.
(dll_list::track_self): New.
(dll_list::load_after_fork): Call track_self method.
* dll_init.h (enum dll_type): Add DLL_SELF, for the main
executable and cygwin1.dll.
(struct dll_list): Declare private method track_self. Declare
member variable main_executable.
* fork.cc (frok::parent): Use ntname from dlls.main_executable
to create child process, converted to short path using
dll_list::buffered_shortname.
---
winsup/cygwin/dll_init.cc | 26 ++++++++++++++++++++------
winsup/cygwin/dll_init.h | 4 ++++
winsup/cygwin/fork.cc | 15 ++++++++++++---
3 files changed, 36 insertions(+), 9 deletions(-)

diff --git a/winsup/cygwin/dll_init.cc b/winsup/cygwin/dll_init.cc
index 6bdd437..8c208d5 100644
--- a/winsup/cygwin/dll_init.cc
+++ b/winsup/cygwin/dll_init.cc
@@ -307,8 +307,9 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
guard (true);
/* Already loaded? For linked DLLs, only compare the basenames. Linked
DLLs are loaded using just the basename and the default DLL search path.
- The Windows loader picks up the first one it finds. */
- dll *d = (type == DLL_LINK) ? dlls.find_by_modname (modname) : dlls[ntname];
+ The Windows loader picks up the first one it finds.
+ This also applies to cygwin1.dll and the main-executable (DLL_SELF). */
+ dll *d = (type != DLL_LOAD) ? dlls.find_by_modname (modname) : dlls[ntname];
if (d)
{
if (!in_forkee)
@@ -344,7 +345,8 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
wcscpy (d->ntname, ntname);
d->modname = d->ntname + (modname - ntname);
d->handle = h;
- d->has_dtors = true;
+ /* DLL_SELF dtors (main-executable, cygwin1.dll) are run elsewhere */
+ d->has_dtors = type != DLL_SELF;
d->p = p;
d->ndeps = 0;
d->deps = NULL;
@@ -488,7 +490,7 @@ dll_list::find (void *retaddr)

dll *d = &start;
while ((d = d->next))
- if (d->handle == h)
+ if (d->type != DLL_SELF && d->handle == h)
break;
return d;
}
@@ -533,11 +535,22 @@ dll_list::detach (void *retaddr)
void
dll_list::init ()
{
+ track_self ();
+
/* Walk the dll chain, initializing each dll */
dll *d = &start;
dll_global_dtors_recorded = d->next != NULL;
while ((d = d->next))
- d->init ();
+ if (d->type != DLL_SELF) /* linked and early loaded dlls */
+ d->init ();
+}
+
+void
+dll_list::track_self ()
+{
+ /* for cygwin1.dll and main-executable: maintain hardlinks only */
+ alloc (cygwin_hmodule, user_data, DLL_SELF);
+ main_executable = alloc (GetModuleHandle (NULL), user_data, DLL_SELF);
}

#define A64K (64 * 1024)
@@ -604,7 +617,7 @@ dll_list::reserve_space ()

/* Reload DLLs after a fork. Iterates over the list of dynamically loaded
DLLs and attempts to load them in the same place as they were loaded in the
- parent. */
+ parent. Updates main-executable and cygwin1.dll tracking. */
void
dll_list::load_after_fork (HANDLE parent)
{
@@ -614,6 +627,7 @@ dll_list::load_after_fork (HANDLE parent)
in_load_after_fork = true;
if (reload_on_fork)
load_after_fork_impl (parent, dlls.istart (DLL_LOAD), 0);
+ track_self ();
in_load_after_fork = false;
}

diff --git a/winsup/cygwin/dll_init.h b/winsup/cygwin/dll_init.h
index e85fdb1..a132adb 100644
--- a/winsup/cygwin/dll_init.h
+++ b/winsup/cygwin/dll_init.h
@@ -40,6 +40,7 @@ struct per_module
typedef enum
{
DLL_NONE,
+ DLL_SELF, /* main-program.exe, cygwin1.dll */
DLL_LINK,
DLL_LOAD,
DLL_ANY
@@ -75,6 +76,8 @@ struct dll

class dll_list
{
+ void track_self ();
+
dll *end;
dll *hold;
dll_type hold_type;
@@ -94,6 +97,7 @@ public:
return nt_max_path_buffer;
}

+ dll *main_executable;
dll start;
int loaded_dlls;
int reload_on_fork;
diff --git a/winsup/cygwin/fork.cc b/winsup/cygwin/fork.cc
index 86689b4..45c571b 100644
--- a/winsup/cygwin/fork.cc
+++ b/winsup/cygwin/fork.cc
@@ -181,7 +181,7 @@ frok::child (volatile char * volatile here)
if (fixup_shms_after_fork ())
api_fatal ("recreate_shm areas after fork failed");

- /* load dynamic dlls, if any */
+ /* load dynamic dlls, if any, re-track main-executable and cygwin1.dll */
dlls.load_after_fork (hParent);

cygheap->fdtab.fixup_after_fork (hParent);
@@ -345,11 +345,20 @@ frok::parent (volatile char * volatile stack_here)

while (1)
{
+ PCWCHAR forking_progname = NULL;
+ if (dlls.main_executable)
+ forking_progname = dll_list::buffered_shortname
+ (dlls.main_executable->ntname);
+ if (!forking_progname || !*forking_progname)
+ forking_progname = myself->progname;
+
syscall_printf ("CreateProcessW (%W, %W, 0, 0, 1, %y, 0, 0, %p, %p)",
- myself->progname, myself->progname, c_flags, &si, &pi);
+ forking_progname, myself->progname, c_flags, &si, &pi);

hchild = NULL;
- rc = CreateProcessW (myself->progname, /* image to run */
+ /* cygwin1.dll may reuse the forking_progname buffer, even
+ in case of failure: don't reuse forking_progname later */
+ rc = CreateProcessW (forking_progname, /* image to run */
GetCommandLineW (), /* Take same space for command
line as in parent to make
sure child stack is allocated
--
2.7.3
Michael Haubenwallner
2016-12-07 10:58:29 UTC
Permalink
To avoid the need for each process to check the filesystem to detect
that hardlink creation is impossible or disabled, cache this fact in
shared memory. Removing cygfork directory while in use does disable
hardlinks creation. To (re-)enable hardlinks creation, the cygfork
directory has to exist before the first cygwin process does fork.

* forkable.cc (dll_list::forkable_ntnamesize): Short cut
forkables needs to impossible when disabled via shared memory.
(dll_list::update_forkables_needs): When detecting hardlink
creation as impossible (not on NTFS) while still (we are the
first one checking) enabled via shared memory, disable the
shared memory value.
(dll_list::request_forkables): Disable the shared memory value
when hardlinks creation became disabled, that is when the
cygfork directory was removed.
* include/cygwin/version.h: Bump CYGWIN_VERSION_SHARED_DATA 6.
* shared_info.h (struct shared_info): Add member
prefer_forkable_hardlinks. Update CURR_SHARED_MAGIC.
* shared.cc (shared_info::initialize): Initialize
prefer_forkable_hardlinks to 1 (Yes).
---
winsup/cygwin/forkable.cc | 13 +++++++++++++
winsup/cygwin/include/cygwin/version.h | 2 +-
winsup/cygwin/shared.cc | 1 +
winsup/cygwin/shared_info.h | 3 ++-
4 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/winsup/cygwin/forkable.cc b/winsup/cygwin/forkable.cc
index c92a44f..e34be55 100644
--- a/winsup/cygwin/forkable.cc
+++ b/winsup/cygwin/forkable.cc
@@ -522,6 +522,11 @@ dll::create_forkable ()
size_t
dll_list::forkable_ntnamesize (dll_type type, PCWCHAR fullntname, PCWCHAR modname)
{
+ /* per process, this is the first forkables-method ever called */
+ if (forkables_needs == forkables_unknown &&
+ !cygwin_shared->prefer_forkable_hardlinks)
+ forkables_needs = forkables_impossible; /* short cut */
+
if (forkables_needs == forkables_impossible)
return 0;

@@ -667,6 +672,7 @@ dll_list::update_forkables_needs ()
{
debug_printf ("impossible, not on NTFS %W", fn.Buffer);
forkables_needs = forkables_impossible;
+ cygwin_shared->prefer_forkable_hardlinks = 0;
}
}

@@ -1056,6 +1062,13 @@ dll_list::request_forkables ()

set_forkables_inheritance (true);

+ if (forkables_needs == forkables_disabled)
+ {
+ /* we do not support (re-)enabling on the fly */
+ forkables_needs = forkables_impossible;
+ cygwin_shared->prefer_forkable_hardlinks = 0;
+ }
+
if (forkables_needs <= forkables_needless)
return;

diff --git a/winsup/cygwin/include/cygwin/version.h b/winsup/cygwin/include/cygwin/version.h
index 6ba602e..b996e26 100644
--- a/winsup/cygwin/include/cygwin/version.h
+++ b/winsup/cygwin/include/cygwin/version.h
@@ -481,7 +481,7 @@ details. */
regions. It is incremented when incompatible changes are made to the shared
memory region *or* to any named shared mutexes, semaphores, etc. */

-#define CYGWIN_VERSION_SHARED_DATA 5
+#define CYGWIN_VERSION_SHARED_DATA 6

/* An identifier used in the names used to create shared objects. The full
names include the CYGWIN_VERSION_SHARED_DATA version as well as this
diff --git a/winsup/cygwin/shared.cc b/winsup/cygwin/shared.cc
index 4ed4c11..4d754d0 100644
--- a/winsup/cygwin/shared.cc
+++ b/winsup/cygwin/shared.cc
@@ -328,6 +328,7 @@ shared_info::initialize ()
init_obcaseinsensitive (); /* Initialize obcaseinsensitive */
tty.init (); /* Initialize tty table */
mt.initialize (); /* Initialize shared tape information */
+ prefer_forkable_hardlinks = 1; /* Yes */
/* Defer debug output printing the installation root and installation key
up to this point. Debug output except for system_printf requires
the global shared memory to exist. */
diff --git a/winsup/cygwin/shared_info.h b/winsup/cygwin/shared_info.h
index ce17c15..ad7c8f4 100644
--- a/winsup/cygwin/shared_info.h
+++ b/winsup/cygwin/shared_info.h
@@ -32,7 +32,7 @@ public:
/* Data accessible to all tasks */


-#define CURR_SHARED_MAGIC 0x8fe4d9eeU
+#define CURR_SHARED_MAGIC 0xbc5d6a9cU

#define USER_VERSION 1

@@ -48,6 +48,7 @@ class shared_info
LONG last_used_bindresvport;
DWORD obcaseinsensitive;
mtinfo mt;
+ char prefer_forkable_hardlinks; /* single byte access always is atomic */

void initialize ();
void init_obcaseinsensitive ();
--
2.7.3
Michael Haubenwallner
2016-12-07 10:58:28 UTC
Permalink
To support in-cygwin package managers, the fork() implementation must
not rely on .exe and .dll files to stay in their original location, as
the package manager's job is to replace these files. Instead, when the
first fork try fails, and we have NTFS, we use hardlinks to the original
binaries in /var/run/cygfork/ to create the child process during the
second fork try, along the main.exe.local file to enable the "DotLocal
Dll Redirection" feature for the dlls.

The (probably few) users that need an update-safe fork manually have to
create the /var/run/cygfork/ directory for now, using:
mkdir --mode=a=rwxt /var/run/cygfork

* child_info.h: Bump CURR_CHILD_INFO_MAGIC.
(enum child_status): Add _CI_SILENTFAIL flag.
(struct child_info): Add silentfail setter and getter.
* winsup.h (child_copy): Add bool silentfail parameter.
* cygheap.cc: Pass silentfail parameter to child_copy.
* dcrt0.cc: Ditto.
* dll_init.h (struct dll): Define public inline method forkedntname.
(struct dll_list): Declare private method find_by_forkedntname.
* dll_init.cc (struct dll_list): Implement find_by_forkedntname.
(dll_list::alloc): Use find_by_forkedntname when in load after fork.
(dll_list::load_after_fork_impl): Load dlls using dll::forkedntname.
* fork.cc (frok::parent): Set silentfail child info flag. Pass
silentfail parameter to child_copy. Use forkedntname of
dlls.main_executable.
(fork): When first dofork run failed and did not use forkables,
run dofork again with_forkables set to true.
(child_copy): Use debug_printf if silentfail is true,
system_printf otherwise.
---
winsup/cygwin/child_info.h | 13 +++++++++++--
winsup/cygwin/cygheap.cc | 3 ++-
winsup/cygwin/dcrt0.cc | 4 ++--
winsup/cygwin/dll_init.cc | 47 ++++++++++++++++++++++++++++++++--------------
winsup/cygwin/dll_init.h | 5 +++++
winsup/cygwin/fork.cc | 39 ++++++++++++++++++++++++++------------
winsup/cygwin/sigproc.cc | 5 ++++-
winsup/cygwin/winsup.h | 2 +-
8 files changed, 85 insertions(+), 33 deletions(-)

diff --git a/winsup/cygwin/child_info.h b/winsup/cygwin/child_info.h
index 5c449e1..3bcd41b 100644
--- a/winsup/cygwin/child_info.h
+++ b/winsup/cygwin/child_info.h
@@ -21,7 +21,8 @@ enum child_status
{
_CI_STRACED = 0x01,
_CI_ISCYGWIN = 0x02,
- _CI_SAW_CTRL_C = 0x04
+ _CI_SAW_CTRL_C = 0x04,
+ _CI_SILENTFAIL = 0x08
};

#define OPROC_MAGIC_MASK 0xff00ff00
@@ -36,7 +37,7 @@ enum child_status
#define EXEC_MAGIC_SIZE sizeof(child_info)

/* Change this value if you get a message indicating that it is out-of-sync. */
-#define CURR_CHILD_INFO_MAGIC 0x30ea98f6U
+#define CURR_CHILD_INFO_MAGIC 0x99dedf39U

#define NPROCS 256

@@ -82,6 +83,7 @@ public:
bool isstraced () const {return !!(flag & _CI_STRACED);}
bool iscygwin () const {return !!(flag & _CI_ISCYGWIN);}
bool saw_ctrl_c () const {return !!(flag & _CI_SAW_CTRL_C);}
+ bool silentfail () const {return !!(flag & _CI_SILENTFAIL);}
void prefork (bool = false);
void cleanup ();
void postfork (pinfo& child)
@@ -91,6 +93,13 @@ public:
child.set_rd_proc_pipe (rd_proc_pipe);
rd_proc_pipe = NULL;
}
+ void silentfail (bool f)
+ {
+ if (f)
+ flag |= _CI_SILENTFAIL;
+ else
+ flag &= ~_CI_SILENTFAIL;
+ }
};

class mount_info;
diff --git a/winsup/cygwin/cygheap.cc b/winsup/cygwin/cygheap.cc
index 87a5eb9..bb8bc46 100644
--- a/winsup/cygwin/cygheap.cc
+++ b/winsup/cygwin/cygheap.cc
@@ -78,7 +78,8 @@ cygheap_fixup_in_child (bool execed)
{
cygheap_max = cygheap = (init_cygheap *) _cygheap_start;
_csbrk ((char *) child_proc_info->cygheap_max - (char *) cygheap);
- child_copy (child_proc_info->parent, false, "cygheap", cygheap, cygheap_max, NULL);
+ child_copy (child_proc_info->parent, false, child_proc_info->silentfail (),
+ "cygheap", cygheap, cygheap_max, NULL);
cygheap_init ();
debug_fixup_after_fork_exec ();
if (execed)
diff --git a/winsup/cygwin/dcrt0.cc b/winsup/cygwin/dcrt0.cc
index 8ddee0c..eea5407 100644
--- a/winsup/cygwin/dcrt0.cc
+++ b/winsup/cygwin/dcrt0.cc
@@ -601,7 +601,7 @@ child_info_fork::handle_fork ()
myself->uid = cygheap->user.real_uid;
myself->gid = cygheap->user.real_gid;

- child_copy (parent, false,
+ child_copy (parent, false, silentfail (),
"dll data", dll_data_start, dll_data_end,
"dll bss", dll_bss_start, dll_bss_end,
"user heap", cygheap->user_heap.base, cygheap->user_heap.ptr,
@@ -625,7 +625,7 @@ child_info_fork::handle_fork ()

/* step 2 now that the dll has its heap filled in, we can fill in the
user's data and bss since user_data is now filled out. */
- child_copy (parent, false,
+ child_copy (parent, false, silentfail (),
"data", user_data->data_start, user_data->data_end,
"bss", user_data->bss_start, user_data->bss_end,
NULL);
diff --git a/winsup/cygwin/dll_init.cc b/winsup/cygwin/dll_init.cc
index d742607..3562b3b 100644
--- a/winsup/cygwin/dll_init.cc
+++ b/winsup/cygwin/dll_init.cc
@@ -289,6 +289,19 @@ dll_list::find_by_modname (PCWCHAR modname)
return NULL;
}

+/* Look for a dll based on the ntname used
+ to dynamically reload in forked child. */
+dll *
+dll_list::find_by_forkedntname (PCWCHAR ntname)
+{
+ dll *d = &start;
+ while ((d = d->next) != NULL)
+ if (!wcscasecmp (ntname, d->forkedntname ()))
+ return d;
+
+ return NULL;
+}
+
#define RETRIES 1000

/* Allocate space for a dll struct. */
@@ -308,8 +321,11 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
/* Already loaded? For linked DLLs, only compare the basenames. Linked
DLLs are loaded using just the basename and the default DLL search path.
The Windows loader picks up the first one it finds.
- This also applies to cygwin1.dll and the main-executable (DLL_SELF). */
- dll *d = (type != DLL_LOAD) ? dlls.find_by_modname (modname) : dlls[ntname];
+ This also applies to cygwin1.dll and the main-executable (DLL_SELF).
+ When in_load_after_fork, dynamically loaded dll's are reloaded
+ using their parent's forkable_ntname, if available. */
+ dll *d = (type != DLL_LOAD) ? dlls.find_by_modname (modname) :
+ in_load_after_fork ? dlls.find_by_forkedntname (ntname) : dlls[ntname];
if (d)
{
if (!in_forkee)
@@ -677,14 +693,16 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
fabort ("unable to release protective reservation (%p) for %W, %E",
d->handle, d->ntname);

- HMODULE h = LoadLibraryExW (buffered_shortname (d->ntname),
+ HMODULE h = LoadLibraryExW (buffered_shortname (d->forkedntname ()),
NULL, DONT_RESOLVE_DLL_REFERENCES);
if (!h)
- fabort ("unable to create interim mapping for %W, %E", d->ntname);
+ fabort ("unable to create interim mapping for %W (using %W), %E",
+ d->ntname, buffered_shortname (d->forkedntname ()));
if (h != d->handle)
{
- sigproc_printf ("%W loaded in wrong place: %p != %p",
- d->ntname, h, d->handle);
+ sigproc_printf ("%W (using %W) loaded in wrong place: %p != %p",
+ d->ntname, buffered_shortname (d->forkedntname ()),
+ h, d->handle);
FreeLibrary (h);
PVOID reservation = reserve_at (d->ntname, h,
d->handle, d->image_size);
@@ -695,8 +713,8 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
if (retries < DLL_RETRY_MAX)
load_after_fork_impl (parent, d, retries+1);
else
- fabort ("unable to remap %W to same address as parent (%p) - try running rebaseall",
- d->ntname, d->handle);
+ fabort ("unable to remap %W (using %W) to same address as parent (%p) - try running rebaseall",
+ d->ntname, buffered_shortname (d->forkedntname ()), d->handle);

/* once the above returns all the dlls are mapped; release
the reservation and continue unwinding */
@@ -724,17 +742,18 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
/* Free the library using our parent's handle: it's identical
to ours or we wouldn't have gotten this far */
if (!FreeLibrary (d->handle))
- fabort ("unable to unload interim mapping of %W, %E",
- d->ntname);
+ fabort ("unable to unload interim mapping of %W (using %W), %E",
+ d->ntname, buffered_shortname (d->forkedntname ()));
}
/* cygwin1.dll - as linked dependency - may reuse the shortname
buffer, even in case of failure: don't reuse shortname later */
- HMODULE h = LoadLibraryW (buffered_shortname (d->ntname));
+ HMODULE h = LoadLibraryW (buffered_shortname (d->forkedntname ()));
if (!h)
- fabort ("unable to map %W, %E", d->ntname);
+ fabort ("unable to map %W (using %W), %E",
+ d->ntname, buffered_shortname (d->forkedntname ()));
if (h != d->handle)
- fabort ("unable to map %W to same address as parent: %p != %p",
- d->ntname, d->handle, h);
+ fabort ("unable to map %W (using %W) to same address as parent: %p != %p",
+ d->ntname, buffered_shortname (d->forkedntname ()), d->handle, h);
}
}

diff --git a/winsup/cygwin/dll_init.h b/winsup/cygwin/dll_init.h
index 93f0807..d598618 100644
--- a/winsup/cygwin/dll_init.h
+++ b/winsup/cygwin/dll_init.h
@@ -76,6 +76,10 @@ struct dll
p.run_dtors ();
}
}
+ PWCHAR forkedntname ()
+ {
+ return forkable_ntname && *forkable_ntname ? forkable_ntname : ntname;
+ }
};

#define MAX_DLL_BEFORE_INIT 100
@@ -98,6 +102,7 @@ class dll_list
PWCHAR forkables_mutex_name;
HANDLE forkables_mutex;
void track_self ();
+ dll *find_by_forkedntname (PCWCHAR ntname);
size_t forkable_ntnamesize (dll_type, PCWCHAR fullntname, PCWCHAR modname);
void prepare_forkables_nomination ();
void update_forkables_needs ();
diff --git a/winsup/cygwin/fork.cc b/winsup/cygwin/fork.cc
index 2436b4d..6e38a5a 100644
--- a/winsup/cygwin/fork.cc
+++ b/winsup/cygwin/fork.cc
@@ -350,12 +350,14 @@ frok::parent (volatile char * volatile stack_here)

*with_forkables = dlls.setup_forkables (*with_forkables);

+ ch.silentfail (!*with_forkables); /* fail silently without forkables */
+
while (1)
{
PCWCHAR forking_progname = NULL;
if (dlls.main_executable)
forking_progname = dll_list::buffered_shortname
- (dlls.main_executable->ntname);
+ (dlls.main_executable->forkedntname ());
if (!forking_progname || !*forking_progname)
forking_progname = myself->progname;

@@ -484,7 +486,7 @@ frok::parent (volatile char * volatile stack_here)
impure_beg = _impure_ptr;
impure_end = _impure_ptr + 1;
}
- rc = child_copy (hchild, true,
+ rc = child_copy (hchild, true, !*with_forkables,
"stack", stack_here, ch.stackbase,
impure, impure_beg, impure_end,
NULL);
@@ -502,7 +504,7 @@ frok::parent (volatile char * volatile stack_here)
for (dll *d = dlls.istart (DLL_LINK); d; d = dlls.inext ())
{
debug_printf ("copying data/bss of a linked dll");
- if (!child_copy (hchild, true,
+ if (!child_copy (hchild, true, !*with_forkables,
"linked dll data", d->p.data_start, d->p.data_end,
"linked dll bss", d->p.bss_start, d->p.bss_end,
NULL))
@@ -532,7 +534,7 @@ frok::parent (volatile char * volatile stack_here)
for (dll *d = dlls.istart (DLL_LOAD); d; d = dlls.inext ())
{
debug_printf ("copying data/bss for a loaded dll");
- if (!child_copy (hchild, true,
+ if (!child_copy (hchild, true, !*with_forkables,
"loaded dll data", d->p.data_start, d->p.data_end,
"loaded dll bss", d->p.bss_start, d->p.bss_end,
NULL))
@@ -572,7 +574,13 @@ cleanup:
extern "C" int
fork ()
{
- bool with_forkables = true;
+ bool with_forkables = false; /* do not force hardlinks on first try */
+ int res = dofork (&with_forkables);
+ if (res >= 0)
+ return res;
+ if (with_forkables)
+ return res; /* no need for second try when already enabled */
+ with_forkables = true; /* enable hardlinks for second try */
return dofork (&with_forkables);
}

@@ -652,7 +660,10 @@ dofork (bool *with_forkables)
strcpy (buf, "child %d - ");
strcat (buf, grouped.errmsg);
strcat (buf, ", errno %d");
- system_printf (buf, grouped.child_pid, grouped.this_errno);
+ if (grouped.ch.silentfail ())
+ debug_printf (buf, grouped.child_pid, grouped.this_errno);
+ else
+ system_printf (buf, grouped.child_pid, grouped.this_errno);
}

set_errno (grouped.this_errno);
@@ -678,10 +689,10 @@ vfork ()
/* Copy memory from one process to another. */

bool
-child_copy (HANDLE hp, bool write, ...)
+child_copy (HANDLE hp, bool write, bool silentfail, ...)
{
va_list args;
- va_start (args, write);
+ va_start (args, silentfail);
static const char *huh[] = {"read", "write"};

char *what;
@@ -707,10 +718,14 @@ child_copy (HANDLE hp, bool write, ...)
{
if (!res)
__seterrno ();
- /* If this happens then there is a bug in our fork
- implementation somewhere. */
- system_printf ("%s %s copy failed, %p..%p, done %lu, windows pid %u, %E",
- what, huh[write], low, high, done, myself->dwProcessId);
+ if (silentfail)
+ debug_printf ("%s %s copy failed, %p..%p, done %lu, windows pid %u, %E",
+ what, huh[write], low, high, done, myself->dwProcessId);
+ else
+ /* If this happens then there is a bug in our fork
+ implementation somewhere. */
+ system_printf ("%s %s copy failed, %p..%p, done %lu, windows pid %u, %E",
+ what, huh[write], low, high, done, myself->dwProcessId);
goto err;
}
}
diff --git a/winsup/cygwin/sigproc.cc b/winsup/cygwin/sigproc.cc
index 36fc649..184c15a 100644
--- a/winsup/cygwin/sigproc.cc
+++ b/winsup/cygwin/sigproc.cc
@@ -1088,7 +1088,10 @@ child_info_fork::abort (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
- strace_vprintf (SYSTEM, fmt, ap);
+ if (silentfail ())
+ strace_vprintf (DEBUG, fmt, ap);
+ else
+ strace_vprintf (SYSTEM, fmt, ap);
TerminateProcess (GetCurrentProcess (), EXITCODE_FORK_FAILED);
}
if (retry > 0)
diff --git a/winsup/cygwin/winsup.h b/winsup/cygwin/winsup.h
index 44ea72a..77437b2 100644
--- a/winsup/cygwin/winsup.h
+++ b/winsup/cygwin/winsup.h
@@ -227,7 +227,7 @@ void multiple_cygwin_problem (const char *, uintptr_t, uintptr_t);

extern "C" void vklog (int priority, const char *message, va_list ap);
extern "C" void klog (int priority, const char *message, ...);
-bool child_copy (HANDLE, bool, ...);
+bool child_copy (HANDLE, bool, bool, ...);

class path_conv;
--
2.7.3
Michael Haubenwallner
2016-12-07 10:58:25 UTC
Permalink
Store loaded dll's file name as full NT path.

* dll_init.h (struct dll): Rename member variable name to ntname.
(struct dll_list): Declare private static member variable
nt_max_path_buffer. Declare public static methods form_ntname,
form_shortname. Define public static methods nt_max_path_buf,
buffered_shortname.
(dll_list::operator []): Use PCWCHAR rather than const PWCHAR.
(dll_list::find_by_modname): Ditto.
* dll_init.cc (in_load_after_fork): Define earlier in file.
(struct dll_list): Rename member variable name to ntname.
Define nt_max_path_buffer variable.
Implement static methods form_ntname, form_shortname.
(dll_list::operator []): Use PCWCHAR rather than const PWCHAR.
(dll_list::find_by_modname): Ditto.
(reserve_at): Ditto.
(release_at): Ditto.
(dll_list::alloc): Use nt_max_path_buf method instead of local
buffer. Store module file name as full NT path, convert using
the form_ntname static method.
(dll_list::load_after_fork): Call load_after_fork_impl only when
reload_on_fork is set.
* fork.cc (frok::child): Call dlls.load_after_fork even without
need to dynamically load dlls.
(frok::parent): Move syscall_printf into the retry loop.
---
winsup/cygwin/dll_init.cc | 205 +++++++++++++++++++++++++++++++++++-----------
winsup/cygwin/dll_init.h | 20 ++++-
winsup/cygwin/fork.cc | 28 +++----
3 files changed, 187 insertions(+), 66 deletions(-)

diff --git a/winsup/cygwin/dll_init.cc b/winsup/cygwin/dll_init.cc
index 0fe5714..6bdd437 100644
--- a/winsup/cygwin/dll_init.cc
+++ b/winsup/cygwin/dll_init.cc
@@ -30,10 +30,129 @@ extern void __stdcall check_sanity_and_sync (per_process *);

dll_list dlls;

+WCHAR NO_COPY dll_list::nt_max_path_buffer[NT_MAX_PATH];
+
muto dll_list::protect;

static bool dll_global_dtors_recorded;

+/* We need the in_load_after_fork flag so dll_dllcrt0_1 can decide at fork
+ time if this is a linked DLL or a dynamically loaded DLL. In either case,
+ both, cygwin_finished_initializing and in_forkee are true, so they are not
+ sufficient to discern the situation. */
+static bool NO_COPY in_load_after_fork;
+
+/* Into ntbuf with ntbufsize, prints name prefixed with "\\??\\"
+ or "\\??\\UNC" as necessary to form the native NT path name.
+ Returns the end of the resulting string in ntbuf.
+ Supports using (a substring of) ntbuf as name argument. */
+PWCHAR dll_list::form_ntname (PWCHAR ntbuf, size_t ntbufsize, PCWCHAR name)
+{
+ while (true)
+ {
+ /* avoid using path_conv here: cygheap might not be
+ initialized when started from non-cygwin process,
+ or still might be frozen in_forkee */
+ if (name[0] == L'\0' || ntbufsize < 8)
+ break;
+ if (name[1] == L':') /* short Win32 drive letter path name */
+ {
+ int winlen = min (ntbufsize - 5, wcslen (name));
+ if (ntbuf + 4 != name)
+ memmove (ntbuf + 4, name, sizeof (*ntbuf) * winlen);
+ wcsncpy (ntbuf, L"\\??\\", 4);
+ ntbuf += 4 + winlen;
+ break;
+ }
+ if (!wcsncmp (name, L"\\\\?\\", 4)) /* long Win32 path name */
+ {
+ int winlen = min (ntbufsize - 1, wcslen (name));
+ if (ntbuf != name)
+ memmove (ntbuf, name, sizeof (*ntbuf) * winlen);
+ ntbuf[1] = L'?';
+ ntbuf += winlen;
+ break;
+ }
+ if (!wcsncmp (name, L"\\\\", 2)) /* short Win32 UNC path name */
+ {
+ name += 1; /* skip first backslash */
+ int winlen = min (ntbufsize - 8, wcslen (name));
+ if (ntbuf + 7 != name)
+ memmove (ntbuf + 7, name, sizeof (*ntbuf) * winlen);
+ wcsncpy (ntbuf, L"\\??\\UNC", 7);
+ ntbuf += 7 + winlen;
+ break;
+ }
+ if (!wcsncmp (name, L"\\??\\", 4)) /* already a long NT path name */
+ {
+ int winlen = min (ntbufsize - 1, wcslen (name));
+ if (ntbuf != name)
+ memmove (ntbuf, name, sizeof (*ntbuf) * winlen);
+ ntbuf += winlen;
+ break;
+ }
+ system_printf ("WARNING: invalid path name '%W'", name);
+ break;
+ }
+ if (ntbufsize)
+ *ntbuf = L'\0';
+ return ntbuf;
+}
+
+/* Into shortbuf with shortbufsize, prints name with "\\??\\"
+ or "\\??\\UNC" prefix removed/modified as necessary to form
+ the short Win32 path name.
+ Returns the end of the resulting string in shortbuf.
+ Supports using (a substring of) shortbuf as name argument. */
+PWCHAR
+dll_list::form_shortname (PWCHAR shortbuf, size_t shortbufsize, PCWCHAR name)
+{
+ while (true)
+ {
+ /* avoid using path_conv here: cygheap might not be
+ initialized when started from non-cygwin process,
+ or still might be frozen in_forkee */
+ if (name[0] == L'\0' || shortbufsize < 2)
+ break;
+ if (name[0] == L'\\' &&
+ (name[1] == L'\\' || name[1] == L'?') &&
+ name[2] == L'?' &&
+ name[3] == L'\\') /* long Win32 or NT path name */
+ name += 4;
+ if (name[1] == L':') /* short Win32 drive letter path name */
+ {
+ int ntlen = min (shortbufsize - 1, wcslen (name));
+ if (shortbuf != name)
+ memmove (shortbuf, name, sizeof (*shortbuf) * ntlen);
+ shortbuf += ntlen;
+ break;
+ }
+ if (!wcsncmp (name, L"UNC\\", 4)) /* UNC path name */
+ {
+ name += 3; /* skip "UNC" */
+ int winlen = min (shortbufsize - 2, wcslen (name));
+ if (shortbuf + 1 != name)
+ memmove (shortbuf + 1, name, sizeof (*shortbuf) * winlen);
+ shortbuf[0] = L'\\';
+ shortbuf += 1 + winlen;
+ break;
+ }
+ if (!wcsncmp (name, L"\\\\", 2)) /* already a short Win32 UNC path name */
+ {
+ int winlen = min (shortbufsize - 1, wcslen (name));
+ if (shortbuf != name)
+ memmove (shortbuf, name, sizeof (*shortbuf) * winlen);
+ shortbuf += winlen;
+ break;
+ }
+ system_printf ("WARNING: invalid path name '%W'", name);
+ break;
+ }
+ if (shortbufsize)
+ *shortbuf = L'\0';
+ return shortbuf;
+}
+
/* Run destructors for all DLLs on exit. */
void
dll_global_dtors ()
@@ -148,11 +267,11 @@ dll::init ()
of dll_list::alloc, as well as the comment preceeding the definition of
the in_load_after_fork bool later in the file. */
dll *
-dll_list::operator[] (const PWCHAR name)
+dll_list::operator[] (PCWCHAR ntname)
{
dll *d = &start;
while ((d = d->next) != NULL)
- if (!wcscasecmp (name, d->name))
+ if (!wcscasecmp (ntname, d->ntname))
return d;

return NULL;
@@ -160,7 +279,7 @@ dll_list::operator[] (const PWCHAR name)

/* Look for a dll based on the basename. */
dll *
-dll_list::find_by_modname (const PWCHAR modname)
+dll_list::find_by_modname (PCWCHAR modname)
{
dll *d = &start;
while ((d = d->next) != NULL)
@@ -177,37 +296,29 @@ dll *
dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
{
/* Called under loader lock conditions so this function can't be called
- multiple times in parallel. A static buffer is safe. */
- static WCHAR buf[NT_MAX_PATH];
- GetModuleFileNameW (h, buf, NT_MAX_PATH);
- PWCHAR name = buf;
- if (!wcsncmp (name, L"\\\\?\\", 4))
- {
- name += 4;
- if (!wcsncmp (name, L"UNC\\", 4))
- {
- name += 2;
- *name = L'\\';
- }
- }
- DWORD namelen = wcslen (name);
- PWCHAR modname = wcsrchr (name, L'\\') + 1;
+ multiple times in parallel. The static buffer is safe. */
+ PWCHAR ntname = nt_max_path_buf ();
+ GetModuleFileNameW (h, ntname, NT_MAX_PATH);
+ PWCHAR modname = form_ntname (ntname, NT_MAX_PATH, ntname);
+ DWORD ntnamelen = modname - ntname;
+ while (modname > ntname && *(modname - 1) != L'\\')
+ --modname;

guard (true);
/* Already loaded? For linked DLLs, only compare the basenames. Linked
DLLs are loaded using just the basename and the default DLL search path.
The Windows loader picks up the first one it finds. */
- dll *d = (type == DLL_LINK) ? dlls.find_by_modname (modname) : dlls[name];
+ dll *d = (type == DLL_LINK) ? dlls.find_by_modname (modname) : dlls[ntname];
if (d)
{
if (!in_forkee)
d->count++; /* Yes. Bump the usage count. */
else if (d->handle != h)
fabort ("%W: Loaded to different address: parent(%p) != child(%p)",
- name, d->handle, h);
+ ntname, d->handle, h);
/* If this DLL has been linked against, and the full path differs, try
to sanity check if this is the same DLL, just in another path. */
- else if (type == DLL_LINK && wcscasecmp (name, d->name)
+ else if (type == DLL_LINK && wcscasecmp (ntname, d->ntname)
&& (d->p.data_start != p->data_start
|| d->p.data_start != p->data_start
|| d->p.bss_start != p->bss_start
@@ -219,19 +330,19 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
" child loaded: %W\n"
"The DLLs differ, so it's not safe to run the forked child.\n"
"Make sure to remove the offending DLL before trying again.",
- d->name, name);
+ d->ntname, ntname);
d->p = p;
}
else
{
/* FIXME: Change this to new at some point. */
- d = (dll *) cmalloc (HEAP_2_DLL, sizeof (*d) + (namelen * sizeof (*name)));
+ d = (dll *) cmalloc (HEAP_2_DLL, sizeof (*d) + (ntnamelen * sizeof (*ntname)));

/* Now we've allocated a block of information. Fill it in with the
supplied info about this DLL. */
d->count = 1;
- wcscpy (d->name, name);
- d->modname = d->name + (modname - name);
+ wcscpy (d->ntname, ntname);
+ d->modname = d->ntname + (modname - ntname);
d->handle = h;
d->has_dtors = true;
d->p = p;
@@ -437,7 +548,7 @@ dll_list::init ()
to clobber the dll's target address range because it often overlaps.
*/
static PVOID
-reserve_at (const PWCHAR name, PVOID here, PVOID dll_base, DWORD dll_size)
+reserve_at (PCWCHAR name, PVOID here, PVOID dll_base, DWORD dll_size)
{
DWORD size;
MEMORY_BASIC_INFORMATION mb;
@@ -466,7 +577,7 @@ reserve_at (const PWCHAR name, PVOID here, PVOID dll_base, DWORD dll_size)

/* Release the memory previously allocated by "reserve_at" above. */
static void
-release_at (const PWCHAR name, PVOID here)
+release_at (PCWCHAR name, PVOID here)
{
if (!VirtualFree (here, 0, MEM_RELEASE))
fabort ("couldn't release memory %p for '%W' alignment, %E\n",
@@ -491,12 +602,6 @@ dll_list::reserve_space ()
d->modname, d->handle);
}

-/* We need the in_load_after_fork flag so dll_dllcrt0_1 can decide at fork
- time if this is a linked DLL or a dynamically loaded DLL. In either case,
- both, cygwin_finished_initializing and in_forkee are true, so they are not
- sufficient to discern the situation. */
-static bool NO_COPY in_load_after_fork;
-
/* Reload DLLs after a fork. Iterates over the list of dynamically loaded
DLLs and attempts to load them in the same place as they were loaded in the
parent. */
@@ -507,7 +612,8 @@ dll_list::load_after_fork (HANDLE parent)
// dll_list::reserve_space();

in_load_after_fork = true;
- load_after_fork_impl (parent, dlls.istart (DLL_LOAD), 0);
+ if (reload_on_fork)
+ load_after_fork_impl (parent, dlls.istart (DLL_LOAD), 0);
in_load_after_fork = false;
}

@@ -540,33 +646,34 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
dll's protective reservation from step 1
*/
if (!retries && !VirtualFree (d->handle, 0, MEM_RELEASE))
- fabort ("unable to release protective reservation for %W (%p), %E",
- d->modname, d->handle);
+ fabort ("unable to release protective reservation (%p) for %W, %E",
+ d->handle, d->ntname);

- HMODULE h = LoadLibraryExW (d->name, NULL, DONT_RESOLVE_DLL_REFERENCES);
+ HMODULE h = LoadLibraryExW (buffered_shortname (d->ntname),
+ NULL, DONT_RESOLVE_DLL_REFERENCES);
if (!h)
- fabort ("unable to create interim mapping for %W, %E", d->name);
+ fabort ("unable to create interim mapping for %W, %E", d->ntname);
if (h != d->handle)
{
sigproc_printf ("%W loaded in wrong place: %p != %p",
- d->modname, h, d->handle);
+ d->ntname, h, d->handle);
FreeLibrary (h);
- PVOID reservation = reserve_at (d->modname, h,
+ PVOID reservation = reserve_at (d->ntname, h,
d->handle, d->image_size);
if (!reservation)
fabort ("unable to block off %p to prevent %W from loading there",
- h, d->modname);
+ h, d->ntname);

if (retries < DLL_RETRY_MAX)
load_after_fork_impl (parent, d, retries+1);
else
fabort ("unable to remap %W to same address as parent (%p) - try running rebaseall",
- d->modname, d->handle);
+ d->ntname, d->handle);

/* once the above returns all the dlls are mapped; release
the reservation and continue unwinding */
sigproc_printf ("releasing blocked space at %p", reservation);
- release_at (d->modname, reservation);
+ release_at (d->ntname, reservation);
return;
}
}
@@ -582,7 +689,7 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
{
if (!VirtualFree (d->handle, 0, MEM_RELEASE))
fabort ("unable to release protective reservation for %W (%p), %E",
- d->modname, d->handle);
+ d->ntname, d->handle);
}
else
{
@@ -590,14 +697,16 @@ void dll_list::load_after_fork_impl (HANDLE parent, dll* d, int retries)
to ours or we wouldn't have gotten this far */
if (!FreeLibrary (d->handle))
fabort ("unable to unload interim mapping of %W, %E",
- d->modname);
+ d->ntname);
}
- HMODULE h = LoadLibraryW (d->name);
+ /* cygwin1.dll - as linked dependency - may reuse the shortname
+ buffer, even in case of failure: don't reuse shortname later */
+ HMODULE h = LoadLibraryW (buffered_shortname (d->ntname));
if (!h)
- fabort ("unable to map %W, %E", d->name);
+ fabort ("unable to map %W, %E", d->ntname);
if (h != d->handle)
fabort ("unable to map %W to same address as parent: %p != %p",
- d->modname, d->handle, h);
+ d->ntname, d->handle, h);
}
}

diff --git a/winsup/cygwin/dll_init.h b/winsup/cygwin/dll_init.h
index 8448813..e85fdb1 100644
--- a/winsup/cygwin/dll_init.h
+++ b/winsup/cygwin/dll_init.h
@@ -58,7 +58,7 @@ struct dll
DWORD image_size;
void* preferred_base;
PWCHAR modname;
- WCHAR name[1];
+ WCHAR ntname[1]; /* must be the last data member */
void detach ();
int init ();
void run_dtors ()
@@ -79,11 +79,25 @@ class dll_list
dll *hold;
dll_type hold_type;
static muto protect;
+ /* Use this buffer under loader lock conditions only. */
+ static WCHAR NO_COPY nt_max_path_buffer[NT_MAX_PATH];
public:
+ static PWCHAR form_ntname (PWCHAR ntbuf, size_t bufsize, PCWCHAR name);
+ static PWCHAR form_shortname (PWCHAR shortbuf, size_t bufsize, PCWCHAR name);
+ static PWCHAR nt_max_path_buf ()
+ {
+ return nt_max_path_buffer;
+ }
+ static PCWCHAR buffered_shortname (PCWCHAR name)
+ {
+ form_shortname (nt_max_path_buffer, NT_MAX_PATH, name);
+ return nt_max_path_buffer;
+ }
+
dll start;
int loaded_dlls;
int reload_on_fork;
- dll *operator [] (const PWCHAR name);
+ dll *operator [] (PCWCHAR ntname);
dll *alloc (HINSTANCE, per_process *, dll_type);
dll *find (void *);
void detach (void *);
@@ -91,7 +105,7 @@ public:
void load_after_fork (HANDLE);
void reserve_space ();
void load_after_fork_impl (HANDLE, dll* which, int retries);
- dll *find_by_modname (const PWCHAR name);
+ dll *find_by_modname (PCWCHAR modname);
void populate_deps (dll* d);
void topsort ();
void topsort_visit (dll* d, bool goto_tail);
diff --git a/winsup/cygwin/fork.cc b/winsup/cygwin/fork.cc
index ef5a268..86689b4 100644
--- a/winsup/cygwin/fork.cc
+++ b/winsup/cygwin/fork.cc
@@ -181,21 +181,18 @@ frok::child (volatile char * volatile here)
if (fixup_shms_after_fork ())
api_fatal ("recreate_shm areas after fork failed");

- /* If we haven't dynamically loaded any dlls, just signal
- the parent. Otherwise, load all the dlls, tell the parent
- that we're done, and wait for the parent to fill in the.
- loaded dlls' data/bss. */
+ /* load dynamic dlls, if any */
+ dlls.load_after_fork (hParent);
+
+ cygheap->fdtab.fixup_after_fork (hParent);
+
+ /* If we haven't dynamically loaded any dlls, just signal the parent.
+ Otherwise, tell the parent that we've loaded all the dlls
+ and wait for the parent to fill in the loaded dlls' data/bss. */
if (!load_dlls)
- {
- cygheap->fdtab.fixup_after_fork (hParent);
- sync_with_parent ("performed fork fixup", false);
- }
+ sync_with_parent ("performed fork fixup", false);
else
- {
- dlls.load_after_fork (hParent);
- cygheap->fdtab.fixup_after_fork (hParent);
- sync_with_parent ("loaded dlls", true);
- }
+ sync_with_parent ("loaded dlls", true);

init_console_handler (myself->ctty > 0);
ForceCloseHandle1 (fork_info->forker_finished, forker_finished);
@@ -338,8 +335,6 @@ frok::parent (volatile char * volatile stack_here)
si.lpReserved2 = (LPBYTE) &ch;
si.cbReserved2 = sizeof (ch);

- syscall_printf ("CreateProcessW (%W, %W, 0, 0, 1, %y, 0, 0, %p, %p)",
- myself->progname, myself->progname, c_flags, &si, &pi);
bool locked = __malloc_lock ();

/* Remove impersonation */
@@ -350,6 +345,9 @@ frok::parent (volatile char * volatile stack_here)

while (1)
{
+ syscall_printf ("CreateProcessW (%W, %W, 0, 0, 1, %y, 0, 0, %p, %p)",
+ myself->progname, myself->progname, c_flags, &si, &pi);
+
hchild = NULL;
rc = CreateProcessW (myself->progname, /* image to run */
GetCommandLineW (), /* Take same space for command
--
2.7.3
Michael Haubenwallner
2016-12-07 10:58:27 UTC
Permalink
In preparation to protect fork() against dll- and exe-updates, create
hardlinks to the main executable and each loaded dll in subdirectories
of /var/run/cygfork/, if that one exists on the NTFS file system.

The directory names consist of the user sid, the main executable's NTFS
IndexNumber, and the most recent LastWriteTime of all involved binaries
(dlls and main executable). Next to the main.exe hardlink we create the
empty file main.exe.local to enable dll redirection.

The name of the mutex to synchronize hardlink creation/cleanup also is
assembled from these directory names, to allow for synchronized cleanup
of even orphaned hardlink directories.

The hardlink to each dynamically loaded dll goes into another directory,
named using the NTFS IndexNumber of the dll's original directory.

* Makefile.in (DLL_OFILES): Add forkable.o.
* dll_init.h (struct dll): Declare member variables fbi, fii,
forkable_ntname. Declare methods nominate_forkable,
create_forkable.
(struct dll_list): Declare enum forkables_needs. Declare member
variables forkables_dirx_size, forkables_dirx_ntname,
forkables_mutex_name, forkables_mutex. Declare private methods
forkable_ntnamesize, prepare_forkables_nomination,
update_forkables_needs, update_forkables, create_forkables,
denominate_forkables, close_mutex, try_remove_forkables,
set_forkables_inheritance, request_forkables. Declare public
static methods ntopenfile, read_fii, read_fbi. Declare public
methods release_forkables, cleanup_forkables. Define public
inline method setup_forkables.
* dll_init.cc (dll_list::alloc): Allocate memory to hold the
name of the hardlink in struct dll member forkable_ntname.
Initialize struct dll members fbi, fii.
(dll_list::load_after_fork): Call release_forkables method.
* fork.cc: Rename public fork function to static dofork, add
with_forkables as bool pointer parameter. Add new fork function
calling dofork. (struct frok): Add bool pointer member
with_forkables, add as constructor parameter.
(frok::parent): Call dlls.setup_forkables before CreateProcessW,
dlls.release_forkables afterwards.
* pinfo.cc (pinfo::exit): Call dlls.cleanup_forkables.
* syscalls.cc (_unlink_nt): Rename public unlink_nt function to
static _unlink_nt, with 'shareable' as additional argument.
(unlink_nt): New, wrap _unlink_nt for original behaviour.
(unlink_nt_shareable): New, wrap _unlink_nt to keep a binary
file still loadable while removing one of its hardlinks.
* forkable.cc: New file.
Implement static functions mkdirs, rmdirs, rmdirs_synchronized,
stat_real_file_once, format_IndexNumber, rootname, sidname,
exename, lwtimename. Define static array forkable_nameparts.
(struct dll): Implement nominate_forkable, create_forkable.
(struct dll_list): Implement static methods ntopenfile,
read_fii, read_fbi. Implement forkable_ntnamesize,
---
winsup/cygwin/Makefile.in | 1 +
winsup/cygwin/dll_init.cc | 16 +-
winsup/cygwin/dll_init.h | 50 +++
winsup/cygwin/fork.cc | 21 +-
winsup/cygwin/forkable.cc | 1095 +++++++++++++++++++++++++++++++++++++++++++++
winsup/cygwin/pinfo.cc | 3 +
winsup/cygwin/syscalls.cc | 24 +-
7 files changed, 1204 insertions(+), 6 deletions(-)
create mode 100644 winsup/cygwin/forkable.cc

diff --git a/winsup/cygwin/Makefile.in b/winsup/cygwin/Makefile.in
index 7c01df7..e3a8686 100644
--- a/winsup/cygwin/Makefile.in
+++ b/winsup/cygwin/Makefile.in
@@ -305,6 +305,7 @@ DLL_OFILES:= \
flock.o \
fnmatch.o \
fork.o \
+ forkable.o \
fts.o \
ftw.o \
getopt.o \
diff --git a/winsup/cygwin/dll_init.cc b/winsup/cygwin/dll_init.cc
index 8c208d5..d742607 100644
--- a/winsup/cygwin/dll_init.cc
+++ b/winsup/cygwin/dll_init.cc
@@ -336,8 +336,11 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
}
else
{
+ size_t forkntsize = forkable_ntnamesize (type, ntname, modname);
+
/* FIXME: Change this to new at some point. */
- d = (dll *) cmalloc (HEAP_2_DLL, sizeof (*d) + (ntnamelen * sizeof (*ntname)));
+ d = (dll *) cmalloc (HEAP_2_DLL, sizeof (*d)
+ + ((ntnamelen + forkntsize) * sizeof (*ntname)));

/* Now we've allocated a block of information. Fill it in with the
supplied info about this DLL. */
@@ -353,6 +356,15 @@ dll_list::alloc (HINSTANCE h, per_process *p, dll_type type)
d->image_size = ((pefile*)h)->optional_hdr ()->SizeOfImage;
d->preferred_base = (void*) ((pefile*)h)->optional_hdr()->ImageBase;
d->type = type;
+ d->fbi.FileAttributes = INVALID_FILE_ATTRIBUTES;
+ d->fii.IndexNumber.QuadPart = -1LL;
+ if (!forkntsize)
+ d->forkable_ntname = NULL;
+ else
+ {
+ d->forkable_ntname = d->ntname + ntnamelen + 1;
+ *d->forkable_ntname = L'\0';
+ }
append (d);
if (type == DLL_LOAD)
loaded_dlls++;
@@ -621,6 +633,8 @@ dll_list::reserve_space ()
void
dll_list::load_after_fork (HANDLE parent)
{
+ release_forkables ();
+
// moved to frok::child for performance reasons:
// dll_list::reserve_space();

diff --git a/winsup/cygwin/dll_init.h b/winsup/cygwin/dll_init.h
index a132adb..93f0807 100644
--- a/winsup/cygwin/dll_init.h
+++ b/winsup/cygwin/dll_init.h
@@ -59,9 +59,15 @@ struct dll
DWORD image_size;
void* preferred_base;
PWCHAR modname;
+ FILE_BASIC_INFORMATION fbi;
+ FILE_INTERNAL_INFORMATION fii;
+ PWCHAR forkable_ntname;
WCHAR ntname[1]; /* must be the last data member */
+
void detach ();
int init ();
+ void nominate_forkable (PCWCHAR);
+ bool create_forkable ();
void run_dtors ()
{
if (has_dtors)
@@ -76,7 +82,32 @@ struct dll

class dll_list
{
+ /* forkables */
+ enum
+ {
+ forkables_unknown,
+ forkables_impossible,
+ forkables_disabled,
+ forkables_needless,
+ forkables_needed,
+ forkables_created,
+ }
+ forkables_needs;
+ DWORD forkables_dirx_size;
+ PWCHAR forkables_dirx_ntname;
+ PWCHAR forkables_mutex_name;
+ HANDLE forkables_mutex;
void track_self ();
+ size_t forkable_ntnamesize (dll_type, PCWCHAR fullntname, PCWCHAR modname);
+ void prepare_forkables_nomination ();
+ void update_forkables_needs ();
+ bool update_forkables ();
+ bool create_forkables ();
+ void denominate_forkables ();
+ bool close_mutex ();
+ void try_remove_forkables (PWCHAR dirbuf, size_t dirlen, size_t dirbufsize);
+ void set_forkables_inheritance (bool);
+ void request_forkables ();

dll *end;
dll *hold;
@@ -85,6 +116,11 @@ class dll_list
/* Use this buffer under loader lock conditions only. */
static WCHAR NO_COPY nt_max_path_buffer[NT_MAX_PATH];
public:
+ static HANDLE ntopenfile (PCWCHAR ntname, NTSTATUS *pstatus = NULL,
+ ULONG openopts = 0, ACCESS_MASK access = 0,
+ HANDLE rootDir = NULL);
+ static bool read_fii (HANDLE fh, PFILE_INTERNAL_INFORMATION pfii);
+ static bool read_fbi (HANDLE fh, PFILE_BASIC_INFORMATION pfbi);
static PWCHAR form_ntname (PWCHAR ntbuf, size_t bufsize, PCWCHAR name);
static PWCHAR form_shortname (PWCHAR shortbuf, size_t bufsize, PCWCHAR name);
static PWCHAR nt_max_path_buf ()
@@ -115,6 +151,20 @@ public:
void topsort_visit (dll* d, bool goto_tail);
void append (dll* d);

+ void release_forkables ();
+ void cleanup_forkables ();
+ bool setup_forkables (bool with_forkables)
+ {
+ if (forkables_needs == forkables_impossible)
+ return true; /* short cut to not retry fork */
+ /* Once used, always use forkables in current process chain. */
+ if (forkables_needs != forkables_unknown)
+ with_forkables = true;
+ if (with_forkables)
+ request_forkables ();
+ return with_forkables;
+ }
+
dll *inext ()
{
while ((hold = hold->next))
diff --git a/winsup/cygwin/fork.cc b/winsup/cygwin/fork.cc
index 45c571b..2436b4d 100644
--- a/winsup/cygwin/fork.cc
+++ b/winsup/cygwin/fork.cc
@@ -30,8 +30,13 @@ details. */
/* FIXME: Once things stabilize, bump up to a few minutes. */
#define FORK_WAIT_TIMEOUT (300 * 1000) /* 300 seconds */

+static int dofork (bool *with_forkables);
class frok
{
+ frok (bool *forkables)
+ : with_forkables (forkables)
+ {}
+ bool *with_forkables;
bool load_dlls;
child_info_fork ch;
const char *errmsg;
@@ -41,7 +46,7 @@ class frok
int __stdcall parent (volatile char * volatile here);
int __stdcall child (volatile char * volatile here);
bool error (const char *fmt, ...);
- friend int fork ();
+ friend int dofork (bool *with_forkables);
};

static void
@@ -343,6 +348,8 @@ frok::parent (volatile char * volatile stack_here)
ch.refresh_cygheap ();
ch.prefork (); /* set up process tracking pipes. */

+ *with_forkables = dlls.setup_forkables (*with_forkables);
+
while (1)
{
PCWCHAR forking_progname = NULL;
@@ -379,6 +386,7 @@ frok::parent (volatile char * volatile stack_here)
{
this_errno = geterrno_from_win_error ();
error ("CreateProcessW failed for '%W'", myself->progname);
+ dlls.release_forkables ();
memset (&pi, 0, sizeof (pi));
goto cleanup;
}
@@ -392,6 +400,8 @@ frok::parent (volatile char * volatile stack_here)
CloseHandle (pi.hThread);
hchild = pi.hProcess;

+ dlls.release_forkables ();
+
/* Protect the handle but name it similarly to the way it will
be called in subproc handling. */
ProtectHandle1 (hchild, childhProc);
@@ -562,7 +572,14 @@ cleanup:
extern "C" int
fork ()
{
- frok grouped;
+ bool with_forkables = true;
+ return dofork (&with_forkables);
+}
+
+static int
+dofork (bool *with_forkables)
+{
+ frok grouped (with_forkables);

debug_printf ("entering");
grouped.load_dlls = 0;
diff --git a/winsup/cygwin/forkable.cc b/winsup/cygwin/forkable.cc
new file mode 100644
index 0000000..c92a44f
--- /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)
+{
+ PWCHAR cygroot = cygheap->installation_root;
+ if (!buf)
+ return 6 /* "\??\UN" */ + wcslen (cygroot);
+ return dll_list::form_ntname (buf, bufsize, cygroot) - 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;
+ }
+}
diff --git a/winsup/cygwin/pinfo.cc b/winsup/cygwin/pinfo.cc
index 9b74442..573060f 100644
--- a/winsup/cygwin/pinfo.cc
+++ b/winsup/cygwin/pinfo.cc
@@ -25,6 +25,7 @@ details. */
#include "cygtls.h"
#include "tls_pbuf.h"
#include "child_info.h"
+#include "dll_init.h"

class pinfo_basic: public _pinfo
{
@@ -222,6 +223,8 @@ pinfo::exit (DWORD n)
int exitcode = self->exitcode & 0xffff;
if (!self->cygstarted)
exitcode = ((exitcode & 0xff) << 8) | ((exitcode >> 8) & 0xff);
+ sigproc_printf ("Calling dlls.cleanup_forkables n %y, exitcode %y", n, exitcode);
+ dlls.cleanup_forkables ();
sigproc_printf ("Calling ExitProcess n %y, exitcode %y", n, exitcode);
if (!TerminateProcess (GetCurrentProcess (), exitcode))
system_printf ("TerminateProcess failed, %E");
diff --git a/winsup/cygwin/syscalls.cc b/winsup/cygwin/syscalls.cc
index 13bb309..cf90213 100644
--- a/winsup/cygwin/syscalls.cc
+++ b/winsup/cygwin/syscalls.cc
@@ -636,8 +636,8 @@ check_dir_not_empty (HANDLE dir, path_conv &pc)
return STATUS_SUCCESS;
}

-NTSTATUS
-unlink_nt (path_conv &pc)
+static NTSTATUS
+_unlink_nt (path_conv &pc, bool shareable)
{
NTSTATUS status;
HANDLE fh, fh_ro = NULL;
@@ -717,6 +717,9 @@ retry_open:
bin so that it actually disappears from its directory even though its
in use. Otherwise, if opening doesn't fail, the file is not in use and
we can go straight to setting the delete disposition flag.
+ However, while we have the file open with FILE_SHARE_DELETE, using
+ this file via another hardlink for anything other than DELETE by
+ concurrent processes fails. The 'shareable' argument is to prevent this.

NOTE: The missing sharing modes FILE_SHARE_READ and FILE_SHARE_WRITE do
NOT result in a STATUS_SHARING_VIOLATION, if another handle is
@@ -726,7 +729,10 @@ retry_open:
will succeed. So, apparently there is no reliable way to find out
if a file is already open elsewhere for other purposes than
reading and writing data. */
- status = NtOpenFile (&fh, access, &attr, &io, FILE_SHARE_DELETE, flags);
+ if (shareable)
+ status = STATUS_SHARING_VIOLATION;
+ else
+ status = NtOpenFile (&fh, access, &attr, &io, FILE_SHARE_DELETE, flags);
/* STATUS_SHARING_VIOLATION is what we expect. STATUS_LOCK_NOT_GRANTED can
be generated under not quite clear circumstances when trying to open a
file on NFS with FILE_SHARE_DELETE only. This has been observed with
@@ -972,6 +978,18 @@ out:
return status;
}

+NTSTATUS
+unlink_nt (path_conv &pc)
+{
+ return _unlink_nt (pc, false);
+}
+
+NTSTATUS
+unlink_nt_shareable (path_conv &pc)
+{
+ return _unlink_nt (pc, true);
+}
+
extern "C" int
unlink (const char *ourname)
{
--
2.7.3
Corinna Vinschen
2016-12-08 15:51:05 UTC
Permalink
Post by Michael Haubenwallner
Hi Corinna (et al.),
now with NtQueryVirtualMemory(MemorySectionName) I've been able to drop
the need to early open any file, and any check for modified binaries.
Instead, only the failing fork call is the trigger to create the
hardlinks (if possible) - followed by a second fork call then.
Would you mind to create a brief tl;dr overview with bullet points to
describe what happens at which point?
Post by Michael Haubenwallner
Unlike in child process startup, where tmp_pathbuf is not initialised
yet (have nt_max_path_buffer instead), do you see a reason to not use
tmp_pathbuf during hardlink creation in parent process (patch 3)?
No, that's fine.
Post by Michael Haubenwallner
The idea there is to really catch the right file for when that file is
just being renamed - that needs the second filename buffer to compare.
Other than that: What do you think about these patches now?
I'm still reluctant in terms of the impact of this change. It seems the
latest version has much less overhead in the default case, which is, no
DLLs and executables has been removed, but I'm not yet sure about
including it in master.

For the time being, I created a topic branch called "topic/forkables"
with this patch. I'm a bit over my head at the moment, but we should
definitely start testing this after the christmas break. If you have
changes, feel free to send them to the cygwin-patches list and I'll
apply them to the topic branch(*).


Thanks,
Corinna

(*) Note that the topic branch will see a forced push once in a while
when the branch needs to be rebased to master. That's why the
branch is called "topic/...". Those branches are the only ones
allowing forced push.
--
Corinna Vinschen Please, send mails regarding Cygwin to
Cygwin Maintainer cygwin AT cygwin DOT com
Red Hat
Michael Haubenwallner
2017-01-09 17:45:17 UTC
Permalink
Post by Corinna Vinschen
Would you mind to create a brief tl;dr overview with bullet points to
describe what happens at which point?
No problem, but that needs some iterations to filter the really interesting
information. Here's a first draft (ohw, a brain dump, sorry) that still is
too long (whoa, did I really examine that much corners?):


== Current situation ==

Cygwin's fork() implementation uses CreateProcess and LoadLibrary to load
binary files (the main-executable and the dlls) into the child process.

For a reliable POSIX-ly fork implementation, any binary file loaded into the
child process needs to be the very same file as loaded in the parent process.

Since Cygwin lacks a runtime loader, the dll search path for linked dlls boils
down to something like: <current dir>, <dir of executable>, <PATH env-var>.

Unfortunately, CreateProcess and LoadLibrary may encounter different (or missing)
binary files (compared to what is loaded into the parent process) in cases like:
* The parent process may have changed the <current dir>.
* The parent process may have changed the <PATH env-var>.
* A dll with the same basename may appear earlier in the dll search path.
* A binary file might have been removed (moved to trash actually).
* When moved to trash, the original binary file name may be different file.


== The "forkables" topic ==

As these really are corner cases: Until a different (or missing) binary file is
detected while loading (or creating) the child process, fork is attempted using
both the original executable location and the original dll search path.

But instead of failing, for the corner cases the "forkables" topic tries to
perform beforehand (in the parent process):
* Once for any loaded binary file, query the NTFS-IndexNumber using its _current_
location.
* Create a temporary application directory, containing:
+ (hardlinks to) the "main.exe" and all the linked dlls,
+ an empty "main.exe.local" file to enable "DotLocal Dll Redirection".
* Create a temporary subdirectory for each directory a dll was dynamically
loaded from, containing (hardlinks to) the dlls dynamically loaded from
that original directory.
* Retry fork with binary file names from that temporary directories (while
retaining the original binary file basenames).


== Implementation details ==

With multiprocessing there's a number of combined challenges to take care of:
* Processes fork concurrently.
* Processes use same main-executable files of different name (eg. ash->dash).
* Processes dynamically load additional dlls between fork calls.
* Processes unload dynamically loaded dlls between fork calls.
* Processes use same binary file names, but of different age.
* All of them apply to (forked!) child processes as well.
* Ensure temporary directories are cleaned up.

To solve these challenges, the forkables implementation is based on these ideas:
* Always create directories, hardlinks and the .local file, but never fail or
overwrite an existing item - it really is there for the requested purpose
already, just created (and in-use right now) by another (similar) process.
* Multiple main-executable (and .local) file names are fine within one
temporary application directory - one executable ignores the other.
* The temporary application directories are created per user SID.
* The temporary application directory name is formed using the main-
executable's NTFS-IndexNumber and the most recent time stamp (LastWriteTime)
of binary files currently loaded in the forking process.

<damn-wrong reason="original directory may not exist any more">
* The temporary subdirectory name for a dynamically loaded dll is formed
using the original directory's NTFS-IndexNumber.
</damn-wrong>


-- Synchronization --

To synchronize cleanup, the states of a mutex named along the temporary
application directory name are used to indicate the directory's state:
(Locked): do not use for forking.
(Unlocked): can be used (to create items) for forking.
(Absent): not in use, ready do clean up.

The available mutex' state transitions are used as:


- At process exit

(Locked -> Absent)
* Before closing the current process' mutex handle, it is locked with almost no
timeout. It turned out that closing a _locked_ mutex' handle is promoted more
synchronously to other processes than closing an unlocked mutex' handle when
it was the mutex' last handle causing the mutex to be destroyed.

(Absent -> Locked)
* For temporary application directories found on the file system, the according
mutex name is tried to "Create-With-Lock".
* If successful, this one directory is cleaned up while holding the lock.
* Finally the mutex handle is closed, resulting in either Unlocked or Absent.


- When preparing to fork

(Unlocked -> Locked)
* Upon first forkables creation in a process, the mutex handle is opened and
locked with infinite timeout, to wait for any process that might be cleaning
up the temporary application directory right now.
* Immediately unlocked once locking succeeded.
* Both parent and child process keep the mutex handle (inherited by child)
open until they exit - see above.


/haubi/

Loading...