/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Maintenance service file system monitoring. * * The Initial Developer of the Original Code is * Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Brian R. Bondy * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include #include #include #include #include #pragma comment(lib, "wtsapi32.lib") #pragma comment(lib, "userenv.lib") #pragma comment(lib, "shlwapi.lib") #include "nsWindowsHelpers.h" #include "nsAutoPtr.h" #include "workmonitor.h" #include "serviceinstall.h" #include "servicebase.h" #include "registrycertificates.h" #include "uachelper.h" #include "updatehelper.h" extern BOOL gServiceStopping; // Wait 15 minutes for an update operation to run at most. // Updates usually take less than a minute so this seems like a // significantly large and safe amount of time to wait. static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000; PRUnichar* MakeCommandLine(int argc, PRUnichar **argv); BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, LPCWSTR newFileName); // The error codes start from 16000 since Windows system error // codes only go up to 15999 const int SERVICE_UPDATER_COULD_NOT_BE_STARTED = 16000; const int SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 16001; const int SERVICE_UPDATER_SIGN_ERROR = 16002; /** * Runs an update process as the service using the SYSTEM account. * * @param updaterPath The path to the update process to start. * @param workingDir The working directory to execute the update process in * @param cmdLine The command line parameters to pass to updater.exe * @param processStarted Set to TRUE if the process was started. * @return TRUE if the update process was run had a return code of 0. */ BOOL StartUpdateProcess(LPCWSTR updaterPath, LPCWSTR workingDir, int argcTmp, LPWSTR *argvTmp, BOOL &processStarted) { STARTUPINFO si = {0}; si.cb = sizeof(STARTUPINFO); si.lpDesktop = L"winsta0\\Default"; PROCESS_INFORMATION pi = {0}; LOG(("Starting update process as the service in session 0.")); // The updater command line is of the form: // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] // So update callback-dir is the 4th, callback-path is the 5th and its args // are the 6th index. LPWSTR cmdLine = MakeCommandLine(argcTmp, argvTmp); // If we're about to start the update process from session 0, // then we should not show a GUI. This only really needs to be done // on Vista and higher, but it's better to keep everything consistent // across all OS if it's of no harm. if (argcTmp >= 2 ) { // Setting the desktop to blank will ensure no GUI is displayed si.lpDesktop = L""; si.dwFlags |= STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } // We move the updater.ini file out of the way because we will handle // executing PostUpdate through the service. We handle PostUpdate from // the service because there are some per user things that happen that // can't run in session 0 which we run updater.exe in. // Once we are done running updater.exe we rename updater.ini back so // that if there were any errors the next updater.exe will run correctly. WCHAR updaterINI[MAX_PATH + 1]; WCHAR updaterINITemp[MAX_PATH + 1]; BOOL selfHandlePostUpdate = FALSE; // We use the updater.ini from the same directory as the updater.exe // because of background updates. if (PathGetSiblingFilePath(updaterINI, argvTmp[0], L"updater.ini") && PathGetSiblingFilePath(updaterINITemp, argvTmp[0], L"updater.tmp")) { selfHandlePostUpdate = MoveFileEx(updaterINI, updaterINITemp, MOVEFILE_REPLACE_EXISTING); } // Create an environment block for the updater.exe process we're about to // start. Indicate that MOZ_USING_SERVICE is set so the updater.exe can // do anything special that it needs to do for service updates. // Search in updater.cpp for more info on MOZ_USING_SERVICE. WCHAR envVarString[32]; wsprintf(envVarString, L"MOZ_USING_SERVICE=1"); _wputenv(envVarString); LPVOID environmentBlock = NULL; if (!CreateEnvironmentBlock(&environmentBlock, NULL, TRUE)) { LOG(("Could not create an environment block, setting it to NULL.\n")); environmentBlock = NULL; } // Empty value on _wputenv is how you remove an env variable in Windows _wputenv(L"MOZ_USING_SERVICE="); processStarted = CreateProcessW(updaterPath, cmdLine, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE | CREATE_UNICODE_ENVIRONMENT, environmentBlock, workingDir, &si, &pi); if (environmentBlock) { DestroyEnvironmentBlock(environmentBlock); } BOOL updateWasSuccessful = FALSE; if (processStarted) { // Wait for the updater process to finish LOG(("Process was started... waiting on result.\n")); DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER); if (WAIT_TIMEOUT == waitRes) { // We waited a long period of time for updater.exe and it never finished // so kill it. TerminateProcess(pi.hProcess, 1); } else { // Check the return code of updater.exe to make sure we get 0 DWORD returnCode; if (GetExitCodeProcess(pi.hProcess, &returnCode)) { LOG(("Process finished with return code %d.\n", returnCode)); // updater returns 0 if successful. updateWasSuccessful = (returnCode == 0); } else { LOG(("Process finished but could not obtain return code.\n")); } } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { DWORD lastError = GetLastError(); LOG(("Could not create process as current user, " "updaterPath: %ls; cmdLine: %l. (%d)\n", updaterPath, cmdLine, lastError)); } // Now that we're done with the update, restore back the updater.ini file // We use it ourselves, and also we want it back in case we had any type // of error so that the normal update process can use it. if (selfHandlePostUpdate) { MoveFileEx(updaterINITemp, updaterINI, MOVEFILE_REPLACE_EXISTING); // Only run the PostUpdate if the update was successful and if we have // a callback application. This is the same thing updater.exe does. if (updateWasSuccessful && argcTmp > 5) { LPCWSTR callbackApplication = argvTmp[5]; LPCWSTR updateInfoDir = argvTmp[1]; // Launch the PostProcess with admin access in session 0. This is // actually launching the post update process but it takes in the // callback app path to figure out where to apply to. // The PostUpdate process with user only access will be done inside // the unelevated updater.exe after the update process is complete // from the service. We don't know here which session to start // the user PostUpdate process from. LOG(("Launching post update process as the service in session 0.")); LaunchWinPostProcess(callbackApplication, updateInfoDir, true, NULL); } } free(cmdLine); return updateWasSuccessful; } /** * Processes a work item (file by the name of .mz) and executes * the command within. * The only supported command at this time is running an update. * * @param monitoringBasePath The base path that is being monitored. * @param notifyInfo the notifyInfo struct for the changes. * @return TRUE if we want the service to stop. */ BOOL ProcessWorkItem(LPCWSTR monitoringBasePath, FILE_NOTIFY_INFORMATION ¬ifyInfo) { int filenameLength = notifyInfo.FileNameLength / sizeof(notifyInfo.FileName[0]); notifyInfo.FileName[filenameLength] = L'\0'; // When the file is ready for processing it will be renamed // to have a .mz extension BOOL haveWorkItem = notifyInfo.Action == FILE_ACTION_RENAMED_NEW_NAME && notifyInfo.FileNameLength > 3 && notifyInfo.FileName[filenameLength - 3] == L'.' && notifyInfo.FileName[filenameLength - 2] == L'm' && notifyInfo.FileName[filenameLength - 1] == L'z'; if (!haveWorkItem) { // We don't have a work item, keep looking return FALSE; } // Indicate that the service is busy and shouldn't be used by anyone else // by opening or creating a named event. Programs should check if this // event exists before writing a work item out. It should already be // created by updater.exe so CreateEventW will lead to an open named event. nsAutoHandle serviceRunningEvent(CreateEventW(NULL, TRUE, FALSE, SERVICE_EVENT_NAME)); LOG(("Processing new command meta file: %ls\n", notifyInfo.FileName)); WCHAR fullMetaUpdateFilePath[MAX_PATH + 1]; wcscpy(fullMetaUpdateFilePath, monitoringBasePath); // We only support file paths in monitoring directories that are MAX_PATH // chars or less. if (!PathAppendSafe(fullMetaUpdateFilePath, notifyInfo.FileName)) { SetEvent(serviceRunningEvent); return TRUE; } nsAutoHandle metaUpdateFile(CreateFile(fullMetaUpdateFilePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL)); if (metaUpdateFile == INVALID_HANDLE_VALUE) { LOG(("Could not open command meta file: %ls\n", notifyInfo.FileName)); SetEvent(serviceRunningEvent); return TRUE; } DWORD fileSize = GetFileSize(metaUpdateFile, NULL); DWORD commandID = 0; // The file should be in wide characters so if it's of odd size it's // an invalid file. const int kSanityCheckFileSize = 1024 * 64; if (fileSize == INVALID_FILE_SIZE || (fileSize %2) != 0 || fileSize > kSanityCheckFileSize || fileSize < sizeof(DWORD)) { LOG(("Could not obtain file size or an improper file size was encountered " "for command meta file: %ls\n", notifyInfo.FileName)); SetEvent(serviceRunningEvent); return TRUE; } // The first 4 bytes are for the command ID. // Currently only command ID 1 which is for updates is supported. DWORD commandIDCount; BOOL result = ReadFile(metaUpdateFile, &commandID, sizeof(DWORD), &commandIDCount, NULL); fileSize -= sizeof(DWORD); // The next MAX_PATH wchar's are for the app to start WCHAR updaterPath[MAX_PATH + 1]; ZeroMemory(updaterPath, sizeof(updaterPath)); DWORD updaterPathCount; result |= ReadFile(metaUpdateFile, updaterPath, MAX_PATH * sizeof(WCHAR), &updaterPathCount, NULL); fileSize -= updaterPathCount; // The next MAX_PATH wchar's are for the app to start WCHAR workingDirectory[MAX_PATH + 1]; ZeroMemory(workingDirectory, sizeof(workingDirectory)); DWORD workingDirectoryCount; result |= ReadFile(metaUpdateFile, workingDirectory, MAX_PATH * sizeof(WCHAR), &workingDirectoryCount, NULL); fileSize -= workingDirectoryCount; // + 2 for wide char termination nsAutoArrayPtr cmdlineBuffer = new char[fileSize + 2]; DWORD cmdLineBufferRead; result |= ReadFile(metaUpdateFile, cmdlineBuffer, fileSize, &cmdLineBufferRead, NULL); fileSize -= cmdLineBufferRead; // We have all the info we need from the work item file, get rid of it. metaUpdateFile.reset(); if (!DeleteFileW(fullMetaUpdateFilePath)) { LOG(("Could not delete work item file: %ls. (%d)\n", fullMetaUpdateFilePath, GetLastError())); } if (!result || commandID != 1 || commandIDCount != sizeof(DWORD) || updaterPathCount != MAX_PATH * sizeof(WCHAR) || workingDirectoryCount != MAX_PATH * sizeof(WCHAR) || fileSize != 0) { LOG(("Could not read command data for command meta file: %ls\n", notifyInfo.FileName)); SetEvent(serviceRunningEvent); return TRUE; } cmdlineBuffer[cmdLineBufferRead] = '\0'; cmdlineBuffer[cmdLineBufferRead + 1] = '\0'; WCHAR *cmdlineBufferWide = reinterpret_cast(cmdlineBuffer.get()); LOG(("An update command was detected and is being processed for command meta " "file: %ls\n", notifyInfo.FileName)); int argcTmp = 0; LPWSTR* argvTmp = CommandLineToArgvW(cmdlineBufferWide, &argcTmp); // Check for updater.exe sign problems BOOL updaterSignProblem = FALSE; #ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK if (argcTmp > 2) { updaterSignProblem = !DoesBinaryMatchAllowedCertificates(argvTmp[2], updaterPath); } #endif // In order to proceed with the update we need at least 3 command line // parameters and no sign problems. if (argcTmp > 2 && !updaterSignProblem) { BOOL updateProcessWasStarted = FALSE; if (StartUpdateProcess(updaterPath, workingDirectory, argcTmp, argvTmp, updateProcessWasStarted)) { LOG(("updater.exe was launched and run successfully!\n")); StartServiceUpdate(argcTmp, argvTmp); } else { LOG(("Error running update process. Updating update.status" " Last error: %d\n", GetLastError())); // If the update process was started, then updater.exe is responsible for // setting the failure code. If it could not be started then we do the // work. We set an error instead of directly setting status pending // so that the app.update.service.errors pref can be updated when // the callback app restarts. if (!updateProcessWasStarted) { if (!WriteStatusFailure(argvTmp[1], SERVICE_UPDATER_COULD_NOT_BE_STARTED)) { LOG(("Could not write update.status service update failure." "Last error: %d\n", GetLastError())); } } } } else if (argcTmp <= 2) { LOG(("Not enough command line parameters specified. " "Updating update.status.\n")); // We can only update update.status if argvTmp[1] exists. argvTmp[1] is // the directory where the update.status file exists. if (argcTmp != 2 || !WriteStatusFailure(argvTmp[1], SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS)) { LOG(("Could not write update.status service update failure." "Last error: %d\n", GetLastError())); } } else { LOG(("Could not start process due to certificate check error on " "updater.exe. Updating update.status. Last error: %d\n", GetLastError())); // When there is a certificate check error on the updater.exe application, // we want to write out the error. if (!WriteStatusFailure(argvTmp[1], SERVICE_UPDATER_SIGN_ERROR)) { LOG(("Could not write pending state to update.status. (%d)\n", GetLastError())); } } LocalFree(argvTmp); SetEvent(serviceRunningEvent); // We processed a work item, whether or not it was successful. return TRUE; } /** * Starts monitoring the update directory for work items. * * @return FALSE if there was an error starting directory monitoring. */ BOOL StartDirectoryChangeMonitor() { LOG(("Starting directory change monitor...\n")); // Init the update directory path and ensure it exists. // Example: C:\programData\Mozilla\Firefox\updates\[channel] // The channel is not included here as we want to monitor the base directory WCHAR updateData[MAX_PATH + 1]; if (!GetUpdateDirectoryPath(updateData)) { LOG(("Could not obtain update directory path\n")); return FALSE; } // Obtain a directory handle, FILE_FLAG_BACKUP_SEMANTICS is needed for this. nsAutoHandle directoryHandle(CreateFile(updateData, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL)); if (directoryHandle == INVALID_HANDLE_VALUE) { LOG(("Could not obtain directory handle to monitor\n")); return FALSE; } // Allocate a buffer that is big enough to hold 128 changes // Note that FILE_NOTIFY_INFORMATION is a variable length struct // so that's why we don't create a simple array directly. char fileNotifyInfoBuffer[(sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH) * 128]; ZeroMemory(&fileNotifyInfoBuffer, sizeof(fileNotifyInfoBuffer)); DWORD bytesReturned; // Listen for directory changes to the update directory while (ReadDirectoryChangesW(directoryHandle, fileNotifyInfoBuffer, sizeof(fileNotifyInfoBuffer), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME, &bytesReturned, NULL, NULL)) { char *fileNotifyInfoBufferLocation = fileNotifyInfoBuffer; // Make sure we have at least one entry to process while (bytesReturned/sizeof(FILE_NOTIFY_INFORMATION) > 0) { // Point to the current directory info which is changed FILE_NOTIFY_INFORMATION ¬ifyInfo = *((FILE_NOTIFY_INFORMATION*)fileNotifyInfoBufferLocation); fileNotifyInfoBufferLocation += notifyInfo .NextEntryOffset; bytesReturned -= notifyInfo .NextEntryOffset; if (!wcscmp(notifyInfo.FileName, L"stop") && gServiceStopping) { LOG(("A stop command was issued.\n")); return TRUE; } BOOL processedWorkItem = ProcessWorkItem(updateData, notifyInfo); if (processedWorkItem) { // We don't return here because during stop we will write out a stop // file which will stop monitoring. StopService(); break; } // NextEntryOffset will be 0 if there are no more items to monitor, // and we're ready to make another call to ReadDirectoryChangesW. if (0 == notifyInfo.NextEntryOffset) { break; } } } return TRUE; }