1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include "loader.hxx"
21 #include <cassert>
22 #include <systools/win32/uwinapi.h>
23 #include <stdlib.h>
24 #include <string>
25 #include <vector>
26 #include <desktop/exithelper.h>
27 #include <tools/pathutils.hxx>
28 
29 #include <fstream>
30 #include <boost/property_tree/ptree.hpp>
31 #include <boost/property_tree/ini_parser.hpp>
32 
33 namespace {
34 
fail()35 void fail()
36 {
37     LPWSTR buf = nullptr;
38     FormatMessageW(
39         FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
40         GetLastError(), 0, reinterpret_cast< LPWSTR >(&buf), 0, nullptr);
41     MessageBoxW(nullptr, buf, nullptr, MB_OK | MB_ICONERROR);
42     HeapFree(GetProcessHeap(), 0, buf);
43     TerminateProcess(GetCurrentProcess(), 255);
44 }
45 
GetCommandArgs(int * pArgc)46 LPWSTR* GetCommandArgs(int* pArgc) { return CommandLineToArgvW(GetCommandLineW(), pArgc); }
47 
48 // tdf#120249: quotes in arguments need to be escaped; backslashes before quotes need doubling. See
49 // https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw
EscapeArg(LPCWSTR sArg)50 std::wstring EscapeArg(LPCWSTR sArg)
51 {
52     const size_t nOrigSize = wcslen(sArg);
53     LPCWSTR const end = sArg + nOrigSize;
54     std::wstring sResult(L"\"");
55 
56     LPCWSTR lastPosQuote = sArg;
57     LPCWSTR posQuote;
58     while ((posQuote = std::find(lastPosQuote, end, L'"')) != end)
59     {
60         LPCWSTR posBackslash = posQuote;
61         while (posBackslash != lastPosQuote && *(posBackslash - 1) == L'\\')
62             --posBackslash;
63 
64         sResult.append(lastPosQuote, posBackslash);
65         sResult.append((posQuote - posBackslash) * 2 + 1, L'\\'); // 2n+1 '\' to escape the '"'
66         sResult.append(1, L'"');
67         lastPosQuote = posQuote + 1;
68     }
69 
70     LPCWSTR posTrailingBackslashSeq = end;
71     while (posTrailingBackslashSeq != lastPosQuote && *(posTrailingBackslashSeq - 1) == L'\\')
72         --posTrailingBackslashSeq;
73     sResult.append(lastPosQuote, posTrailingBackslashSeq);
74     sResult.append((end - posTrailingBackslashSeq) * 2, L'\\'); // 2n '\' before closing '"'
75     sResult.append(1, L'"');
76 
77     return sResult;
78 }
79 
AddEscapedArg(LPCWSTR sArg,std::vector<std::wstring> & aEscapedArgs,std::size_t & iLengthAccumulator)80 void AddEscapedArg(LPCWSTR sArg, std::vector<std::wstring>& aEscapedArgs,
81                    std::size_t& iLengthAccumulator)
82 {
83     std::wstring sEscapedArg = EscapeArg(sArg);
84     aEscapedArgs.push_back(sEscapedArg);
85     iLengthAccumulator += sEscapedArg.length() + 1; // a space between args
86 }
87 
HasWildCard(LPCWSTR sArg)88 bool HasWildCard(LPCWSTR sArg)
89 {
90     while (*sArg != L'\0')
91     {
92         if (*sArg == L'*' || *sArg == L'?')
93             return true;
94         sArg++;
95     }
96     return false;
97 }
98 
99 }
100 
101 namespace desktop_win32 {
102 
extendLoaderEnvironment(WCHAR * binPath,WCHAR * iniDirectory)103 void extendLoaderEnvironment(WCHAR * binPath, WCHAR * iniDirectory) {
104     if (!GetModuleFileNameW(nullptr, iniDirectory, MAX_PATH)) {
105         fail();
106     }
107     WCHAR * iniDirEnd = tools::filename(iniDirectory);
108     WCHAR name[MAX_PATH + MY_LENGTH(L".bin")];
109         // hopefully std::size_t is large enough to not overflow
110     WCHAR * nameEnd = name;
111     for (WCHAR * p = iniDirEnd; *p != L'\0'; ++p) {
112         *nameEnd++ = *p;
113     }
114     if (!(nameEnd - name >= 4 && nameEnd[-4] == L'.' &&
115          (((nameEnd[-3] == L'E' || nameEnd[-3] == L'e') &&
116            (nameEnd[-2] == L'X' || nameEnd[-2] == L'x') &&
117            (nameEnd[-1] == L'E' || nameEnd[-1] == L'e')) ||
118           ((nameEnd[-3] == L'C' || nameEnd[-3] == L'c') &&
119            (nameEnd[-2] == L'O' || nameEnd[-2] == L'o') &&
120            (nameEnd[-1] == L'M' || nameEnd[-1] == L'm')))))
121     {
122         *nameEnd = L'.';
123         nameEnd += 4;
124     }
125     nameEnd[-3] = 'b';
126     nameEnd[-2] = 'i';
127     nameEnd[-1] = 'n';
128     tools::buildPath(binPath, iniDirectory, iniDirEnd, name, nameEnd - name);
129     *iniDirEnd = L'\0';
130     std::size_t const maxEnv = 32767;
131     WCHAR env[maxEnv];
132     DWORD n = GetEnvironmentVariableW(L"PATH", env, maxEnv);
133     if ((n >= maxEnv || n == 0) && GetLastError() != ERROR_ENVVAR_NOT_FOUND) {
134         fail();
135     }
136     // must be first in PATH to override other entries
137     assert(*(iniDirEnd - 1) == L'\\'); // hence -1 below
138     if (wcsncmp(env, iniDirectory, iniDirEnd - iniDirectory - 1) != 0
139         || env[iniDirEnd - iniDirectory - 1] != L';')
140     {
141         WCHAR pad[MAX_PATH + maxEnv];
142             // hopefully std::size_t is large enough to not overflow
143         WCHAR * p = commandLineAppend(pad, iniDirectory, iniDirEnd - iniDirectory - 1);
144         if (n != 0) {
145             *p++ = L';';
146             for (DWORD i = 0; i <= n; ++i) {
147                 *p++ = env[i];
148             }
149         } else {
150             *p++ = L'\0';
151         }
152         if (!SetEnvironmentVariableW(L"PATH", pad)) {
153             fail();
154         }
155     }
156 }
157 
officeloader_impl(bool bAllowConsole)158 int officeloader_impl(bool bAllowConsole)
159 {
160     WCHAR szTargetFileName[MAX_PATH] = {};
161     WCHAR szIniDirectory[MAX_PATH];
162     STARTUPINFOW aStartupInfo;
163 
164     desktop_win32::extendLoaderEnvironment(szTargetFileName, szIniDirectory);
165 
166     ZeroMemory(&aStartupInfo, sizeof(aStartupInfo));
167     aStartupInfo.cb = sizeof(aStartupInfo);
168 
169     // Create process with same command line, environment and stdio handles which
170     // are directed to the created pipes
171     GetStartupInfoW(&aStartupInfo);
172 
173     DWORD dwExitCode = DWORD(-1);
174 
175     bool fSuccess = false;
176     LPWSTR lpCommandLine = nullptr;
177     bool bFirst = true;
178     WCHAR cwd[MAX_PATH];
179     DWORD cwdLen = GetCurrentDirectoryW(MAX_PATH, cwd);
180     if (cwdLen >= MAX_PATH)
181     {
182         cwdLen = 0;
183     }
184     std::vector<std::wstring> aEscapedArgs;
185 
186     // read limit values from bootstrap.ini
187     unsigned int nMaxMemoryInMB = 0;
188     bool bExcludeChildProcesses = true;
189 
190     const WCHAR* szIniFile = L"\\bootstrap.ini";
191     const size_t nDirLen = wcslen(szIniDirectory);
192     if (wcslen(szIniFile) + nDirLen < MAX_PATH)
193     {
194         WCHAR szBootstrapIni[MAX_PATH];
195         wcscpy(szBootstrapIni, szIniDirectory);
196         wcscpy(&szBootstrapIni[nDirLen], szIniFile);
197 
198         try
199         {
200             boost::property_tree::ptree pt;
201             std::ifstream aFile(szBootstrapIni);
202             boost::property_tree::ini_parser::read_ini(aFile, pt);
203             nMaxMemoryInMB = pt.get("Win32.LimitMaximumMemoryInMB", nMaxMemoryInMB);
204             bExcludeChildProcesses = pt.get("Win32.ExcludeChildProcessesFromLimit", bExcludeChildProcesses);
205         }
206         catch (...)
207         {
208             nMaxMemoryInMB = 0;
209         }
210     }
211 
212     // create a Windows JobObject with a memory limit
213     HANDLE hJobObject = nullptr;
214     if (nMaxMemoryInMB > 0)
215     {
216         JOBOBJECT_EXTENDED_LIMIT_INFORMATION aJobLimit;
217         aJobLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY;
218         if (bExcludeChildProcesses)
219             aJobLimit.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
220         aJobLimit.JobMemoryLimit = nMaxMemoryInMB * 1024 * 1024;
221         hJobObject = CreateJobObjectW(nullptr, nullptr);
222         if (hJobObject != nullptr)
223             SetInformationJobObject(hJobObject, JobObjectExtendedLimitInformation, &aJobLimit,
224                                     sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
225     }
226 
227     do
228     {
229         if (bFirst)
230         {
231             int argc = 0;
232             LPWSTR* argv = GetCommandArgs(&argc);
233             std::size_t n = 0;
234             for (int i = 0; i < argc; ++i)
235             {
236                 // check for wildCards in arguments- windows does not expand automatically
237                 if (HasWildCard(argv[i]))
238                 {
239                     WIN32_FIND_DATAW aFindData;
240                     HANDLE h = FindFirstFileW(argv[i], &aFindData);
241                     if (h == INVALID_HANDLE_VALUE)
242                     {
243                         AddEscapedArg(argv[i], aEscapedArgs, n);
244                     }
245                     else
246                     {
247                         const int nPathSize = 32 * 1024;
248                         wchar_t drive[nPathSize];
249                         wchar_t dir[nPathSize];
250                         wchar_t path[nPathSize];
251                         _wsplitpath_s(argv[i], drive, nPathSize, dir, nPathSize, nullptr, 0,
252                                       nullptr, 0);
253                         _wmakepath_s(path, nPathSize, drive, dir, aFindData.cFileName, nullptr);
254                         AddEscapedArg(path, aEscapedArgs, n);
255 
256                         while (FindNextFileW(h, &aFindData))
257                         {
258                             _wmakepath_s(path, nPathSize, drive, dir, aFindData.cFileName, nullptr);
259                             AddEscapedArg(path, aEscapedArgs, n);
260                         }
261                         FindClose(h);
262                     }
263                 }
264                 else
265                 {
266                     AddEscapedArg(argv[i], aEscapedArgs, n);
267                 }
268             }
269             LocalFree(argv);
270             n += MY_LENGTH(L" \"-env:OOO_CWD=2") + 4 * cwdLen + MY_LENGTH(L"\"") + 1;
271             // 4 * cwdLen: each char preceded by backslash, each trailing
272             // backslash doubled
273             lpCommandLine = new WCHAR[n];
274         }
275         WCHAR* p = desktop_win32::commandLineAppend(lpCommandLine, aEscapedArgs[0].c_str(),
276                                                     aEscapedArgs[0].length());
277         for (size_t i = 1; i < aEscapedArgs.size(); ++i)
278         {
279             const std::wstring& rArg = aEscapedArgs[i];
280             if (bFirst || EXITHELPER_NORMAL_RESTART == dwExitCode
281                 || wcsncmp(rArg.c_str(), MY_STRING(L"\"-env:")) == 0)
282             {
283                 p = desktop_win32::commandLineAppend(p, MY_STRING(L" "));
284                 p = desktop_win32::commandLineAppend(p, rArg.c_str(), rArg.length());
285             }
286         }
287 
288         p = desktop_win32::commandLineAppend(p, MY_STRING(L" \"-env:OOO_CWD="));
289         if (cwdLen == 0)
290         {
291             p = desktop_win32::commandLineAppend(p, MY_STRING(L"0"));
292         }
293         else
294         {
295             p = desktop_win32::commandLineAppend(p, MY_STRING(L"2"));
296             p = desktop_win32::commandLineAppendEncoded(p, cwd);
297         }
298         desktop_win32::commandLineAppend(p, MY_STRING(L"\""));
299         bFirst = false;
300 
301         WCHAR szParentProcessId[64]; // This is more than large enough for a 128 bit decimal value
302         bool bHeadlessMode(false);
303 
304         {
305             // Check command line arguments for "--headless" parameter. We only
306             // set the environment variable "ATTACHED_PARENT_PROCESSID" for the headless
307             // mode as self-destruction of the soffice.bin process can lead to
308             // certain side-effects (log-off can result in data-loss, ".lock" is not deleted.
309             // See 138244 for more information.
310             int argc2;
311             LPWSTR* argv2 = GetCommandArgs(&argc2);
312 
313             if (argc2 > 1)
314             {
315                 int n;
316 
317                 for (n = 1; n < argc2; n++)
318                 {
319                     if (0 == wcsnicmp(argv2[n], L"-headless", 9)
320                         || 0 == wcsnicmp(argv2[n], L"--headless", 10))
321                     {
322                         bHeadlessMode = true;
323                     }
324                 }
325             }
326 
327             LocalFree(argv2);
328         }
329 
330         if (_ltow(static_cast<long>(GetCurrentProcessId()), szParentProcessId, 10) && bHeadlessMode)
331             SetEnvironmentVariableW(L"ATTACHED_PARENT_PROCESSID", szParentProcessId);
332 
333         PROCESS_INFORMATION aProcessInfo;
334 
335         fSuccess = CreateProcessW(szTargetFileName, lpCommandLine, nullptr, nullptr, TRUE,
336                                   bAllowConsole ? 0 : DETACHED_PROCESS, nullptr, szIniDirectory,
337                                   &aStartupInfo, &aProcessInfo);
338 
339         if (fSuccess)
340         {
341             DWORD dwWaitResult;
342 
343             if (hJobObject)
344                 AssignProcessToJobObject(hJobObject, aProcessInfo.hProcess);
345 
346             do
347             {
348                 // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWith" so
349                 // we have to do so as if we were processing any messages
350 
351                 dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE,
352                                                          QS_ALLEVENTS);
353 
354                 if (WAIT_OBJECT_0 + 1 == dwWaitResult)
355                 {
356                     MSG msg;
357 
358                     PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE);
359                 }
360             } while (WAIT_OBJECT_0 + 1 == dwWaitResult);
361 
362             dwExitCode = 0;
363             GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode);
364 
365             CloseHandle(aProcessInfo.hProcess);
366             CloseHandle(aProcessInfo.hThread);
367         }
368     } while (fSuccess
369              && (EXITHELPER_CRASH_WITH_RESTART == dwExitCode
370                  || EXITHELPER_NORMAL_RESTART == dwExitCode));
371 
372     if (hJobObject)
373         CloseHandle(hJobObject);
374 
375     delete[] lpCommandLine;
376 
377     return fSuccess ? dwExitCode : -1;
378 }
379 
unopkgloader_impl(bool bAllowConsole)380 int unopkgloader_impl(bool bAllowConsole)
381 {
382     WCHAR        szTargetFileName[MAX_PATH];
383     WCHAR        szIniDirectory[MAX_PATH];
384     desktop_win32::extendLoaderEnvironment(szTargetFileName, szIniDirectory);
385 
386     STARTUPINFOW aStartupInfo{};
387     aStartupInfo.cb = sizeof(aStartupInfo);
388     GetStartupInfoW(&aStartupInfo);
389 
390     DWORD   dwExitCode = DWORD(-1);
391 
392     size_t iniDirLen = wcslen(szIniDirectory);
393     WCHAR cwd[MAX_PATH];
394     DWORD cwdLen = GetCurrentDirectoryW(MAX_PATH, cwd);
395     if (cwdLen >= MAX_PATH) {
396         cwdLen = 0;
397     }
398     WCHAR redirect[MAX_PATH];
399     DWORD dummy;
400     bool hasRedirect =
401         tools::buildPath(
402             redirect, szIniDirectory, szIniDirectory + iniDirLen,
403             MY_STRING(L"redirect.ini")) != nullptr &&
404             (GetBinaryTypeW(redirect, &dummy) || // cheaper check for file existence?
405                 GetLastError() != ERROR_FILE_NOT_FOUND);
406     LPWSTR cl1 = GetCommandLineW();
407     WCHAR* cl2 = new WCHAR[
408         wcslen(cl1) +
409             (hasRedirect
410                 ? (MY_LENGTH(L" \"-env:INIFILENAME=vnd.sun.star.pathname:") +
411                     iniDirLen + MY_LENGTH(L"redirect.ini\""))
412                 : 0) +
413             MY_LENGTH(L" \"-env:OOO_CWD=2") + 4 * cwdLen + MY_LENGTH(L"\"") + 1];
414     // 4 * cwdLen: each char preceded by backslash, each trailing backslash
415     // doubled
416     WCHAR* p = desktop_win32::commandLineAppend(cl2, cl1);
417     if (hasRedirect) {
418         p = desktop_win32::commandLineAppend(
419             p, MY_STRING(L" \"-env:INIFILENAME=vnd.sun.star.pathname:"));
420         p = desktop_win32::commandLineAppend(p, szIniDirectory);
421         p = desktop_win32::commandLineAppend(p, MY_STRING(L"redirect.ini\""));
422     }
423     p = desktop_win32::commandLineAppend(p, MY_STRING(L" \"-env:OOO_CWD="));
424     if (cwdLen == 0) {
425         p = desktop_win32::commandLineAppend(p, MY_STRING(L"0"));
426     }
427     else {
428         p = desktop_win32::commandLineAppend(p, MY_STRING(L"2"));
429         p = desktop_win32::commandLineAppendEncoded(p, cwd);
430     }
431     desktop_win32::commandLineAppend(p, MY_STRING(L"\""));
432 
433     PROCESS_INFORMATION aProcessInfo;
434 
435     bool fSuccess = CreateProcessW(
436         szTargetFileName,
437         cl2,
438         nullptr,
439         nullptr,
440         TRUE,
441         bAllowConsole ? 0 : DETACHED_PROCESS,
442         nullptr,
443         szIniDirectory,
444         &aStartupInfo,
445         &aProcessInfo);
446 
447     delete[] cl2;
448 
449     if (fSuccess)
450     {
451         DWORD   dwWaitResult;
452 
453         do
454         {
455             // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWidth" so we have to do so
456             // as if we were processing any messages
457 
458             dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE, QS_ALLEVENTS);
459 
460             if (WAIT_OBJECT_0 + 1 == dwWaitResult)
461             {
462                 MSG msg;
463 
464                 PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE);
465             }
466         } while (WAIT_OBJECT_0 + 1 == dwWaitResult);
467 
468         dwExitCode = 0;
469         GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode);
470 
471         CloseHandle(aProcessInfo.hProcess);
472         CloseHandle(aProcessInfo.hThread);
473     }
474 
475     return dwExitCode;
476 }
477 
478 }
479 
480 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
481