/* $Id: VBoxExtPackHelperApp.cpp 98281 2023-01-24 12:15:49Z vboxsync $ */ /** @file * VirtualBox Main - Extension Pack Helper Application, usually set-uid-to-root. */ /* * Copyright (C) 2010-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-only */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include "../include/ExtPackUtil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef RT_OS_WINDOWS # define _WIN32_WINNT 0x0501 # include /* ShellExecuteEx, ++ */ # include /* CoInitializeEx */ # ifdef DEBUG # include # endif #endif #ifdef RT_OS_DARWIN # include # include # include #endif #if !defined(RT_OS_OS2) # include # include # if !defined(RT_OS_WINDOWS) # include # include /* geteuid */ # endif #endif /********************************************************************************************************************************* * Defined Constants And Macros * *********************************************************************************************************************************/ /** Enable elevation on Windows and Darwin. */ #if !defined(RT_OS_OS2) || defined(DOXYGEN_RUNNING) # define WITH_ELEVATION #endif /** @name Command and option names * @{ */ #define CMD_INSTALL 1000 #define CMD_UNINSTALL 1001 #define CMD_CLEANUP 1002 #ifdef WITH_ELEVATION # define OPT_ELEVATED 1090 # define OPT_STDOUT 1091 # define OPT_STDERR 1092 #endif #define OPT_DISP_INFO_HACK 1093 /** @} */ /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ #ifdef RT_OS_WINDOWS static HINSTANCE g_hInstance; #endif #ifdef IN_RT_R3 /* Override RTAssertShouldPanic to prevent gdb process creation. */ RTDECL(bool) RTAssertShouldPanic(void) { return true; } #endif /** * Handle the special standard options when these are specified after the * command. * * @param ch The option character. */ static RTEXITCODE DoStandardOption(int ch) { switch (ch) { case 'h': { RTMsgInfo(VBOX_PRODUCT " Extension Pack Helper App\n" "Copyright (C) " VBOX_C_YEAR " " VBOX_VENDOR "\n" "\n" "This NOT intended for general use, please use VBoxManage instead\n" "or call the IExtPackManager API directly.\n" "\n" "Usage: %s [options]\n" "Commands:\n" " install --base-dir --cert-dir --name \\\n" " --tarball --tarball-fd \n" " uninstall --base-dir --name \n" " cleanup --base-dir \n" , RTProcShortName()); return RTEXITCODE_SUCCESS; } case 'V': RTPrintf("%sr%d\n", VBOX_VERSION_STRING, RTBldCfgRevision()); return RTEXITCODE_SUCCESS; default: AssertFailedReturn(RTEXITCODE_FAILURE); } } /** * Checks if the cerficiate directory is valid. * * @returns true if it is valid, false if it isn't. * @param pszCertDir The certificate directory to validate. */ static bool IsValidCertificateDir(const char *pszCertDir) { /* * Just be darn strict for now. */ char szCorrect[RTPATH_MAX]; int vrc = RTPathAppPrivateNoArch(szCorrect, sizeof(szCorrect)); if (RT_FAILURE(vrc)) return false; vrc = RTPathAppend(szCorrect, sizeof(szCorrect), VBOX_EXTPACK_CERT_DIR); if (RT_FAILURE(vrc)) return false; return RTPathCompare(szCorrect, pszCertDir) == 0; } /** * Checks if the base directory is valid. * * @returns true if it is valid, false if it isn't. * @param pszBaesDir The base directory to validate. */ static bool IsValidBaseDir(const char *pszBaseDir) { /* * Just be darn strict for now. */ char szCorrect[RTPATH_MAX]; int vrc = RTPathAppPrivateArchTop(szCorrect, sizeof(szCorrect)); if (RT_FAILURE(vrc)) return false; vrc = RTPathAppend(szCorrect, sizeof(szCorrect), VBOX_EXTPACK_INSTALL_DIR); if (RT_FAILURE(vrc)) return false; return RTPathCompare(szCorrect, pszBaseDir) == 0; } /** * Cleans up a temporary extension pack directory. * * This is used by 'uninstall', 'cleanup' and in the failure path of 'install'. * * @returns The program exit code. * @param pszDir The directory to clean up. The caller is * responsible for making sure this is valid. * @param fTemporary Whether this is a temporary install directory or * not. */ static RTEXITCODE RemoveExtPackDir(const char *pszDir, bool fTemporary) { /** @todo May have to undo 555 modes here later. */ int vrc = RTDirRemoveRecursive(pszDir, RTDIRRMREC_F_CONTENT_AND_DIR); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to delete the %sextension pack directory: %Rrc ('%s')", fTemporary ? "temporary " : "", vrc, pszDir); return RTEXITCODE_SUCCESS; } /** * Wrapper around RTDirRename that may retry the operation for up to 15 seconds * on windows to deal with AV software. */ static int CommonDirRenameWrapper(const char *pszSrc, const char *pszDst, uint32_t fFlags) { #ifdef RT_OS_WINDOWS uint64_t nsNow = RTTimeNanoTS(); for (;;) { int vrc = RTDirRename(pszSrc, pszDst, fFlags); if ( ( vrc != VERR_ACCESS_DENIED && vrc != VERR_SHARING_VIOLATION) || RTTimeNanoTS() - nsNow > RT_NS_15SEC) return vrc; RTThreadSleep(128); } #else return RTDirRename(pszSrc, pszDst, fFlags); #endif } /** * Common uninstall worker used by both uninstall and install --replace. * * @returns success or failure, message displayed on failure. * @param pszExtPackDir The extension pack directory name. */ static RTEXITCODE CommonUninstallWorker(const char *pszExtPackDir) { /* Rename the extension pack directory before deleting it to prevent new VM processes from picking it up. */ char szExtPackUnInstDir[RTPATH_MAX]; int vrc = RTStrCopy(szExtPackUnInstDir, sizeof(szExtPackUnInstDir), pszExtPackDir); if (RT_SUCCESS(vrc)) vrc = RTStrCat(szExtPackUnInstDir, sizeof(szExtPackUnInstDir), "-_-uninst"); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to construct temporary extension pack path: %Rrc", vrc); vrc = CommonDirRenameWrapper(pszExtPackDir, szExtPackUnInstDir, RTPATHRENAME_FLAGS_NO_REPLACE); if (vrc == VERR_ALREADY_EXISTS) { /* Automatic cleanup and try again. It's in theory possible that we're racing another cleanup operation here, so just ignore errors and try again. (There is no installation race due to the exclusive temporary installation directory.) */ RemoveExtPackDir(szExtPackUnInstDir, false /*fTemporary*/); vrc = CommonDirRenameWrapper(pszExtPackDir, szExtPackUnInstDir, RTPATHRENAME_FLAGS_NO_REPLACE); } if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to rename the extension pack directory: %Rrc\n" "If the problem persists, try running the command: VBoxManage extpack cleanup", vrc); /* Recursively delete the directory content. */ return RemoveExtPackDir(szExtPackUnInstDir, false /*fTemporary*/); } /** * Wrapper around VBoxExtPackOpenTarFss. * * @returns success or failure, message displayed on failure. * @param hTarballFile The handle to the tarball file. * @param phTarFss Where to return the filesystem stream handle. */ static RTEXITCODE OpenTarFss(RTFILE hTarballFile, PRTVFSFSSTREAM phTarFss) { char szError[8192]; int vrc = VBoxExtPackOpenTarFss(hTarballFile, szError, sizeof(szError), phTarFss, NULL); if (RT_FAILURE(vrc)) { Assert(szError[0]); return RTMsgErrorExit(RTEXITCODE_FAILURE, "%s", szError); } Assert(!szError[0]); return RTEXITCODE_SUCCESS; } /** * Sets the permissions of the temporary extension pack directory just before * renaming it. * * By default the temporary directory is only accessible by root, this function * will make it world readable and browseable. * * @returns The program exit code. * @param pszDir The temporary extension pack directory. */ static RTEXITCODE SetExtPackPermissions(const char *pszDir) { RTMsgInfo("Setting permissions..."); #if !defined(RT_OS_WINDOWS) int vrc = RTPathSetMode(pszDir, 0755); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to set directory permissions: %Rrc ('%s')", vrc, pszDir); #else /** @todo TrustedInstaller? */ RT_NOREF1(pszDir); #endif return RTEXITCODE_SUCCESS; } /** * Wrapper around VBoxExtPackValidateMember. * * @returns Program exit code, failure with message. * @param pszName The name of the directory. * @param enmType The object type. * @param hVfsObj The VFS object. */ static RTEXITCODE ValidateMemberOfExtPack(const char *pszName, RTVFSOBJTYPE enmType, RTVFSOBJ hVfsObj) { char szError[8192]; int vrc = VBoxExtPackValidateMember(pszName, enmType, hVfsObj, szError, sizeof(szError)); if (RT_FAILURE(vrc)) { Assert(szError[0]); return RTMsgErrorExit(RTEXITCODE_FAILURE, "%s", szError); } Assert(!szError[0]); return RTEXITCODE_SUCCESS; } /** * Validates the extension pack tarball prior to unpacking. * * Operations performed: * - Hardening checks. * * @returns The program exit code. * @param pszDir The directory where the extension pack has been * unpacked. * @param pszExtPackName The expected extension pack name. * @param pszTarball The name of the tarball in case we have to * complain about something. */ static RTEXITCODE ValidateUnpackedExtPack(const char *pszDir, const char *pszTarball, const char *pszExtPackName) { RT_NOREF2(pszTarball, pszExtPackName); RTMsgInfo("Validating unpacked extension pack..."); RTERRINFOSTATIC ErrInfo; RTErrInfoInitStatic(&ErrInfo); int vrc = SUPR3HardenedVerifyDir(pszDir, true /*fRecursive*/, true /*fCheckFiles*/, &ErrInfo.Core); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Hardening check failed with %Rrc: %s", vrc, ErrInfo.Core.pszMsg); return RTEXITCODE_SUCCESS; } /** * Unpacks a directory from an extension pack tarball. * * @returns Program exit code, failure with message. * @param pszDstDirName The name of the unpacked directory. * @param hVfsObj The source object for the directory. */ static RTEXITCODE UnpackExtPackDir(const char *pszDstDirName, RTVFSOBJ hVfsObj) { /* * Get the mode mask before creating the directory. */ RTFSOBJINFO ObjInfo; int vrc = RTVfsObjQueryInfo(hVfsObj, &ObjInfo, RTFSOBJATTRADD_NOTHING); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTVfsObjQueryInfo failed on '%s': %Rrc", pszDstDirName, vrc); ObjInfo.Attr.fMode &= ~(RTFS_UNIX_IWOTH | RTFS_UNIX_IWGRP); vrc = RTDirCreate(pszDstDirName, ObjInfo.Attr.fMode, 0); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to create directory '%s': %Rrc", pszDstDirName, vrc); #ifndef RT_OS_WINDOWS /* * Because of umask, we have to apply the mode again. */ vrc = RTPathSetMode(pszDstDirName, ObjInfo.Attr.fMode); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to set directory permissions on '%s': %Rrc", pszDstDirName, vrc); #else /** @todo Ownership tricks on windows? */ #endif return RTEXITCODE_SUCCESS; } /** * Unpacks a file from an extension pack tarball. * * @returns Program exit code, failure with message. * @param pszName The name in the tarball. * @param pszDstFilename The name of the unpacked file. * @param hVfsIosSrc The source stream for the file. * @param hUnpackManifest The manifest to add the file digest to. */ static RTEXITCODE UnpackExtPackFile(const char *pszName, const char *pszDstFilename, RTVFSIOSTREAM hVfsIosSrc, RTMANIFEST hUnpackManifest) { /* * Query the object info, we'll need it for buffer sizing as well as * setting the file mode. */ RTFSOBJINFO ObjInfo; int vrc = RTVfsIoStrmQueryInfo(hVfsIosSrc, &ObjInfo, RTFSOBJATTRADD_NOTHING); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTVfsIoStrmQueryInfo failed with %Rrc on '%s'", vrc, pszDstFilename); /* * Create the file. */ uint32_t fFlags = RTFILE_O_WRITE | RTFILE_O_DENY_ALL | RTFILE_O_CREATE | (0600 << RTFILE_O_CREATE_MODE_SHIFT); RTFILE hFile; vrc = RTFileOpen(&hFile, pszDstFilename, fFlags); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to create '%s': %Rrc", pszDstFilename, vrc); /* * Create a I/O stream for the destination file, stack a manifest entry * creator on top of it. */ RTVFSIOSTREAM hVfsIosDst2; vrc = RTVfsIoStrmFromRTFile(hFile, fFlags, true /*fLeaveOpen*/, &hVfsIosDst2); if (RT_SUCCESS(vrc)) { RTVFSIOSTREAM hVfsIosDst; vrc = RTManifestEntryAddPassthruIoStream(hUnpackManifest, hVfsIosDst2, pszName, RTMANIFEST_ATTR_SIZE | RTMANIFEST_ATTR_SHA256, false /*fReadOrWrite*/, &hVfsIosDst); RTVfsIoStrmRelease(hVfsIosDst2); if (RT_SUCCESS(vrc)) { /* * Pump the data thru. */ vrc = RTVfsUtilPumpIoStreams(hVfsIosSrc, hVfsIosDst, (uint32_t)RT_MIN(ObjInfo.cbObject, _1G)); if (RT_SUCCESS(vrc)) { vrc = RTManifestPtIosAddEntryNow(hVfsIosDst); if (RT_SUCCESS(vrc)) { RTVfsIoStrmRelease(hVfsIosDst); hVfsIosDst = NIL_RTVFSIOSTREAM; /* * Set the mode mask. */ ObjInfo.Attr.fMode &= ~(RTFS_UNIX_IWOTH | RTFS_UNIX_IWGRP); vrc = RTFileSetMode(hFile, ObjInfo.Attr.fMode); /** @todo Windows needs to do more here, I think. */ if (RT_SUCCESS(vrc)) { RTFileClose(hFile); return RTEXITCODE_SUCCESS; } RTMsgError("Failed to set the mode of '%s' to %RTfmode: %Rrc", pszDstFilename, ObjInfo.Attr.fMode, vrc); } else RTMsgError("RTManifestPtIosAddEntryNow failed for '%s': %Rrc", pszDstFilename, vrc); } else RTMsgError("RTVfsUtilPumpIoStreams failed for '%s': %Rrc", pszDstFilename, vrc); RTVfsIoStrmRelease(hVfsIosDst); } else RTMsgError("RTManifestEntryAddPassthruIoStream failed: %Rrc", vrc); } else RTMsgError("RTVfsIoStrmFromRTFile failed: %Rrc", vrc); RTFileClose(hFile); return RTEXITCODE_FAILURE; } /** * Unpacks the extension pack into the specified directory. * * This will apply ownership and permission changes to all the content, the * exception is @a pszDirDst which will be handled by SetExtPackPermissions. * * @returns The program exit code. * @param hTarballFile The tarball to unpack. * @param pszDirDst Where to unpack it. * @param hValidManifest The manifest we've validated. * @param pszTarball The name of the tarball in case we have to * complain about something. */ static RTEXITCODE UnpackExtPack(RTFILE hTarballFile, const char *pszDirDst, RTMANIFEST hValidManifest, const char *pszTarball) { RT_NOREF1(pszTarball); RTMsgInfo("Unpacking extension pack into '%s'...", pszDirDst); /* * Set up the destination path. */ char szDstPath[RTPATH_MAX]; int vrc = RTPathAbs(pszDirDst, szDstPath, sizeof(szDstPath) - VBOX_EXTPACK_MAX_MEMBER_NAME_LENGTH - 2); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTPathAbs('%s',,) failed: %Rrc", pszDirDst, vrc); size_t offDstPath = RTPathStripTrailingSlash(szDstPath); szDstPath[offDstPath++] = '/'; szDstPath[offDstPath] = '\0'; /* * Open the tar.gz filesystem stream and set up an manifest in-memory file. */ RTVFSFSSTREAM hTarFss; RTEXITCODE rcExit = OpenTarFss(hTarballFile, &hTarFss); if (rcExit != RTEXITCODE_SUCCESS) return rcExit; RTMANIFEST hUnpackManifest; vrc = RTManifestCreate(0 /*fFlags*/, &hUnpackManifest); if (RT_SUCCESS(vrc)) { /* * Process the tarball (would be nice to move this to a function). */ for (;;) { /* * Get the next stream object. */ char *pszName; RTVFSOBJ hVfsObj; RTVFSOBJTYPE enmType; vrc = RTVfsFsStrmNext(hTarFss, &pszName, &enmType, &hVfsObj); if (RT_FAILURE(vrc)) { if (vrc != VERR_EOF) rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTVfsFsStrmNext failed: %Rrc", vrc); break; } const char *pszAdjName = pszName[0] == '.' && pszName[1] == '/' ? &pszName[2] : pszName; /* * Check the type & name validity then unpack it. */ rcExit = ValidateMemberOfExtPack(pszName, enmType, hVfsObj); if (rcExit == RTEXITCODE_SUCCESS) { szDstPath[offDstPath] = '\0'; vrc = RTStrCopy(&szDstPath[offDstPath], sizeof(szDstPath) - offDstPath, pszAdjName); if (RT_SUCCESS(vrc)) { if ( enmType == RTVFSOBJTYPE_FILE || enmType == RTVFSOBJTYPE_IO_STREAM) { RTVFSIOSTREAM hVfsIos = RTVfsObjToIoStream(hVfsObj); rcExit = UnpackExtPackFile(pszAdjName, szDstPath, hVfsIos, hUnpackManifest); RTVfsIoStrmRelease(hVfsIos); } else if (*pszAdjName && strcmp(pszAdjName, ".")) rcExit = UnpackExtPackDir(szDstPath, hVfsObj); } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Name is too long: '%s' (%Rrc)", pszAdjName, vrc); } /* * Clean up and break out on failure. */ RTVfsObjRelease(hVfsObj); RTStrFree(pszName); if (rcExit != RTEXITCODE_SUCCESS) break; } /* * Check that what we just extracted matches the already verified * manifest. */ if (rcExit == RTEXITCODE_SUCCESS) { char szError[RTPATH_MAX]; vrc = RTManifestEqualsEx(hUnpackManifest, hValidManifest, NULL /*papszIgnoreEntries*/, NULL /*papszIgnoreAttr*/, 0 /*fFlags*/, szError, sizeof(szError)); if (RT_SUCCESS(vrc)) rcExit = RTEXITCODE_SUCCESS; else if (vrc == VERR_NOT_EQUAL && szError[0]) rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Manifest mismatch: %s", szError); else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTManifestEqualsEx failed: %Rrc", vrc); } #if 0 RTVFSIOSTREAM hVfsIosStdOut = NIL_RTVFSIOSTREAM; RTVfsIoStrmFromStdHandle(RTHANDLESTD_OUTPUT, RTFILE_O_WRITE, true, &hVfsIosStdOut); RTVfsIoStrmWrite(hVfsIosStdOut, "Unpack:\n", sizeof("Unpack:\n") - 1, true, NULL); RTManifestWriteStandard(hUnpackManifest, hVfsIosStdOut); RTVfsIoStrmWrite(hVfsIosStdOut, "Valid:\n", sizeof("Valid:\n") - 1, true, NULL); RTManifestWriteStandard(hValidManifest, hVfsIosStdOut); #endif RTManifestRelease(hUnpackManifest); } RTVfsFsStrmRelease(hTarFss); return rcExit; } /** * Wrapper around VBoxExtPackValidateTarball. * * @returns The program exit code. * @param hTarballFile The handle to open the @a pszTarball file. * @param pszExtPackName The name of the extension pack name. * @param pszTarball The name of the tarball in case we have to * complain about something. * @param pszTarballDigest The SHA-256 digest of the tarball. * @param phValidManifest Where to return the handle to fully validated * the manifest for the extension pack. This * includes all files. */ static RTEXITCODE ValidateExtPackTarball(RTFILE hTarballFile, const char *pszExtPackName, const char *pszTarball, const char *pszTarballDigest, PRTMANIFEST phValidManifest) { *phValidManifest = NIL_RTMANIFEST; RTMsgInfo("Validating extension pack '%s' ('%s')...", pszTarball, pszExtPackName); Assert(pszTarballDigest && *pszTarballDigest); char szError[8192]; int vrc = VBoxExtPackValidateTarball(hTarballFile, pszExtPackName, pszTarball, pszTarballDigest, szError, sizeof(szError), phValidManifest, NULL /*phXmlFile*/, NULL /*pStrDigest*/); if (RT_FAILURE(vrc)) { Assert(szError[0]); return RTMsgErrorExit(RTEXITCODE_FAILURE, "%s", szError); } Assert(!szError[0]); return RTEXITCODE_SUCCESS; } /** * The 2nd part of the installation process. * * @returns The program exit code. * @param pszBaseDir The base directory. * @param pszCertDir The certificat directory. * @param pszTarball The tarball name. * @param pszTarballDigest The SHA-256 digest of the tarball. Empty string * if no digest available. * @param hTarballFile The handle to open the @a pszTarball file. * @param hTarballFileOpt The tarball file handle (optional). * @param pszName The extension pack name. * @param pszMangledName The mangled extension pack name. * @param fReplace Whether to replace any existing ext pack. */ static RTEXITCODE DoInstall2(const char *pszBaseDir, const char *pszCertDir, const char *pszTarball, const char *pszTarballDigest, RTFILE hTarballFile, RTFILE hTarballFileOpt, const char *pszName, const char *pszMangledName, bool fReplace) { RT_NOREF1(pszCertDir); /* * Do some basic validation of the tarball file. */ RTFSOBJINFO ObjInfo; int vrc = RTFileQueryInfo(hTarballFile, &ObjInfo, RTFSOBJATTRADD_UNIX); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTFileQueryInfo failed with %Rrc on '%s'", vrc, pszTarball); if (!RTFS_IS_FILE(ObjInfo.Attr.fMode)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Not a regular file: %s", pszTarball); if (hTarballFileOpt != NIL_RTFILE) { RTFSOBJINFO ObjInfo2; vrc = RTFileQueryInfo(hTarballFileOpt, &ObjInfo2, RTFSOBJATTRADD_UNIX); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTFileQueryInfo failed with %Rrc on --tarball-fd", vrc); if ( ObjInfo.Attr.u.Unix.INodeIdDevice != ObjInfo2.Attr.u.Unix.INodeIdDevice || ObjInfo.Attr.u.Unix.INodeId != ObjInfo2.Attr.u.Unix.INodeId) return RTMsgErrorExit(RTEXITCODE_FAILURE, "--tarball and --tarball-fd does not match"); } /* * Construct the paths to the two directories we'll be using. */ char szFinalPath[RTPATH_MAX]; vrc = RTPathJoin(szFinalPath, sizeof(szFinalPath), pszBaseDir, pszMangledName); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to construct the path to the final extension pack directory: %Rrc", vrc); char szTmpPath[RTPATH_MAX]; vrc = RTPathJoin(szTmpPath, sizeof(szTmpPath) - 64, pszBaseDir, pszMangledName); if (RT_SUCCESS(vrc)) { size_t cchTmpPath = strlen(szTmpPath); RTStrPrintf(&szTmpPath[cchTmpPath], sizeof(szTmpPath) - cchTmpPath, "-_-inst-%u", (uint32_t)RTProcSelf()); } if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to construct the path to the temporary extension pack directory: %Rrc", vrc); /* * Check that they don't exist at this point in time, unless fReplace=true. */ vrc = RTPathQueryInfoEx(szFinalPath, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); if (RT_SUCCESS(vrc) && RTFS_IS_DIRECTORY(ObjInfo.Attr.fMode)) { if (!fReplace) return RTMsgErrorExit(RTEXITCODE_FAILURE, "The extension pack is already installed. You must uninstall the old one first."); } else if (RT_SUCCESS(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Found non-directory file system object where the extension pack would be installed ('%s')", szFinalPath); else if (vrc != VERR_FILE_NOT_FOUND && vrc != VERR_PATH_NOT_FOUND) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Unexpected RTPathQueryInfoEx status code %Rrc for '%s'", vrc, szFinalPath); vrc = RTPathQueryInfoEx(szTmpPath, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); if (vrc != VERR_FILE_NOT_FOUND && vrc != VERR_PATH_NOT_FOUND) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Unexpected RTPathQueryInfoEx status code %Rrc for '%s'", vrc, szFinalPath); /* * Create the temporary directory and prepare the extension pack within it. * If all checks out correctly, rename it to the final directory. */ RTDirCreate(pszBaseDir, 0755, 0); #ifndef RT_OS_WINDOWS /* * Because of umask, we have to apply the mode again. */ vrc = RTPathSetMode(pszBaseDir, 0755); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to set directory permissions on '%s': %Rrc", pszBaseDir, vrc); #else /** @todo Ownership tricks on windows? */ #endif vrc = RTDirCreate(szTmpPath, 0700, 0); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to create temporary directory: %Rrc ('%s')", vrc, szTmpPath); RTMANIFEST hValidManifest = NIL_RTMANIFEST; RTEXITCODE rcExit = ValidateExtPackTarball(hTarballFile, pszName, pszTarball, pszTarballDigest, &hValidManifest); if (rcExit == RTEXITCODE_SUCCESS) rcExit = UnpackExtPack(hTarballFile, szTmpPath, hValidManifest, pszTarball); if (rcExit == RTEXITCODE_SUCCESS) rcExit = ValidateUnpackedExtPack(szTmpPath, pszTarball, pszName); if (rcExit == RTEXITCODE_SUCCESS) rcExit = SetExtPackPermissions(szTmpPath); RTManifestRelease(hValidManifest); if (rcExit == RTEXITCODE_SUCCESS) { vrc = CommonDirRenameWrapper(szTmpPath, szFinalPath, RTPATHRENAME_FLAGS_NO_REPLACE); if ( RT_FAILURE(vrc) && fReplace && RTDirExists(szFinalPath)) { /* Automatic uninstall if --replace was given. */ rcExit = CommonUninstallWorker(szFinalPath); if (rcExit == RTEXITCODE_SUCCESS) vrc = CommonDirRenameWrapper(szTmpPath, szFinalPath, RTPATHRENAME_FLAGS_NO_REPLACE); } if (RT_SUCCESS(vrc)) RTMsgInfo("Successfully installed '%s' (%s)", pszName, pszTarball); else if (rcExit == RTEXITCODE_SUCCESS) rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to rename the temporary directory to the final one: %Rrc ('%s' -> '%s')", vrc, szTmpPath, szFinalPath); } /* * Clean up the temporary directory on failure. */ if (rcExit != RTEXITCODE_SUCCESS) RemoveExtPackDir(szTmpPath, true /*fTemporary*/); return rcExit; } /** * Implements the 'install' command. * * @returns The program exit code. * @param argc The number of program arguments. * @param argv The program arguments. */ static RTEXITCODE DoInstall(int argc, char **argv) { /* * Parse the parameters. * * Note! The --base-dir and --cert-dir are only for checking that the * caller and this help applications have the same idea of where * things are. Likewise, the --name is for verifying assumptions * the caller made about the name. The optional --tarball-fd option * is just for easing the paranoia on the user side. */ static const RTGETOPTDEF s_aOptions[] = { { "--base-dir", 'b', RTGETOPT_REQ_STRING }, { "--cert-dir", 'c', RTGETOPT_REQ_STRING }, { "--name", 'n', RTGETOPT_REQ_STRING }, { "--tarball", 't', RTGETOPT_REQ_STRING }, { "--tarball-fd", 'd', RTGETOPT_REQ_UINT64 }, { "--replace", 'r', RTGETOPT_REQ_NOTHING }, { "--sha-256", 's', RTGETOPT_REQ_STRING } }; RTGETOPTSTATE GetState; int vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 0, 0 /*fFlags*/); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptInit failed: %Rrc\n", vrc); const char *pszBaseDir = NULL; const char *pszCertDir = NULL; const char *pszName = NULL; const char *pszTarball = NULL; const char *pszTarballDigest = NULL; RTFILE hTarballFileOpt = NIL_RTFILE; bool fReplace = false; RTGETOPTUNION ValueUnion; int ch; while ((ch = RTGetOpt(&GetState, &ValueUnion))) { switch (ch) { case 'b': if (pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --base-dir options"); pszBaseDir = ValueUnion.psz; if (!IsValidBaseDir(pszBaseDir)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid base directory: '%s'", pszBaseDir); break; case 'c': if (pszCertDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --cert-dir options"); pszCertDir = ValueUnion.psz; if (!IsValidCertificateDir(pszCertDir)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid certificate directory: '%s'", pszCertDir); break; case 'n': if (pszName) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --name options"); pszName = ValueUnion.psz; if (!VBoxExtPackIsValidName(pszName)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid extension pack name: '%s'", pszName); break; case 't': if (pszTarball) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --tarball options"); pszTarball = ValueUnion.psz; break; case 'd': { if (hTarballFileOpt != NIL_RTFILE) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --tarball-fd options"); RTHCUINTPTR hNative = (RTHCUINTPTR)ValueUnion.u64; if (hNative != ValueUnion.u64) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "The --tarball-fd value is out of range: %#RX64", ValueUnion.u64); vrc = RTFileFromNative(&hTarballFileOpt, hNative); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "RTFileFromNative failed on --target-fd value: %Rrc", vrc); break; } case 'r': fReplace = true; break; case 's': { if (pszTarballDigest) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --sha-256 options"); pszTarballDigest = ValueUnion.psz; uint8_t abDigest[RTSHA256_HASH_SIZE]; vrc = RTSha256FromString(pszTarballDigest, abDigest); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Bad SHA-256 string: %Rrc", vrc); break; } case 'h': case 'V': return DoStandardOption(ch); default: return RTGetOptPrintError(ch, &ValueUnion); } } if (!pszName) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --name option"); if (!pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --base-dir option"); if (!pszCertDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --cert-dir option"); if (!pszTarball) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --tarball option"); if (!pszTarballDigest) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --sha-256 option"); /* * Ok, down to business. */ RTCString *pstrMangledName = VBoxExtPackMangleName(pszName); if (!pstrMangledName) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to mangle name ('%s)", pszName); RTEXITCODE rcExit; RTFILE hTarballFile; vrc = RTFileOpen(&hTarballFile, pszTarball, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE); if (RT_SUCCESS(vrc)) { rcExit = DoInstall2(pszBaseDir, pszCertDir, pszTarball, pszTarballDigest, hTarballFile, hTarballFileOpt, pszName, pstrMangledName->c_str(), fReplace); RTFileClose(hTarballFile); } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to open the extension pack tarball: %Rrc ('%s')", vrc, pszTarball); delete pstrMangledName; return rcExit; } /** * Implements the 'uninstall' command. * * @returns The program exit code. * @param argc The number of program arguments. * @param argv The program arguments. */ static RTEXITCODE DoUninstall(int argc, char **argv) { /* * Parse the parameters. * * Note! The --base-dir is only for checking that the caller and this help * applications have the same idea of where things are. */ static const RTGETOPTDEF s_aOptions[] = { { "--base-dir", 'b', RTGETOPT_REQ_STRING }, { "--name", 'n', RTGETOPT_REQ_STRING }, { "--forced", 'f', RTGETOPT_REQ_NOTHING }, }; RTGETOPTSTATE GetState; int vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 0, 0 /*fFlags*/); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptInit failed: %Rrc\n", vrc); const char *pszBaseDir = NULL; const char *pszName = NULL; RTGETOPTUNION ValueUnion; int ch; while ((ch = RTGetOpt(&GetState, &ValueUnion))) { switch (ch) { case 'b': if (pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --base-dir options"); pszBaseDir = ValueUnion.psz; if (!IsValidBaseDir(pszBaseDir)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid base directory: '%s'", pszBaseDir); break; case 'n': if (pszName) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --name options"); pszName = ValueUnion.psz; if (!VBoxExtPackIsValidName(pszName)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid extension pack name: '%s'", pszName); break; case 'f': /* ignored */ break; case 'h': case 'V': return DoStandardOption(ch); default: return RTGetOptPrintError(ch, &ValueUnion); } } if (!pszName) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --name option"); if (!pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --base-dir option"); /* * Mangle the name so we can construct the directory names. */ RTCString *pstrMangledName = VBoxExtPackMangleName(pszName); if (!pstrMangledName) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to mangle name ('%s)", pszName); RTCString strMangledName(*pstrMangledName); delete pstrMangledName; /* * Ok, down to business. */ /* Check that it exists. */ char szExtPackDir[RTPATH_MAX]; vrc = RTPathJoin(szExtPackDir, sizeof(szExtPackDir), pszBaseDir, strMangledName.c_str()); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to construct extension pack path: %Rrc", vrc); if (!RTDirExists(szExtPackDir)) { RTMsgInfo("Extension pack not installed. Nothing to do."); return RTEXITCODE_SUCCESS; } RTEXITCODE rcExit = CommonUninstallWorker(szExtPackDir); if (rcExit == RTEXITCODE_SUCCESS) RTMsgInfo("Successfully removed extension pack '%s'\n", pszName); return rcExit; } /** * Implements the 'cleanup' command. * * @returns The program exit code. * @param argc The number of program arguments. * @param argv The program arguments. */ static RTEXITCODE DoCleanup(int argc, char **argv) { /* * Parse the parameters. * * Note! The --base-dir is only for checking that the caller and this help * applications have the same idea of where things are. */ static const RTGETOPTDEF s_aOptions[] = { { "--base-dir", 'b', RTGETOPT_REQ_STRING }, }; RTGETOPTSTATE GetState; int vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 0, 0 /*fFlags*/); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptInit failed: %Rrc\n", vrc); const char *pszBaseDir = NULL; RTGETOPTUNION ValueUnion; int ch; while ((ch = RTGetOpt(&GetState, &ValueUnion))) { switch (ch) { case 'b': if (pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Too many --base-dir options"); pszBaseDir = ValueUnion.psz; if (!IsValidBaseDir(pszBaseDir)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Invalid base directory: '%s'", pszBaseDir); break; case 'h': case 'V': return DoStandardOption(ch); default: return RTGetOptPrintError(ch, &ValueUnion); } } if (!pszBaseDir) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Missing --base-dir option"); /* * Ok, down to business. */ RTDIR hDir; vrc = RTDirOpen(&hDir, pszBaseDir); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed open the base directory: %Rrc ('%s')", vrc, pszBaseDir); uint32_t cCleaned = 0; RTEXITCODE rcExit = RTEXITCODE_SUCCESS; for (;;) { RTDIRENTRYEX Entry; vrc = RTDirReadEx(hDir, &Entry, NULL /*pcbDirEntry*/, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); if (RT_FAILURE(vrc)) { if (vrc != VERR_NO_MORE_FILES) rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDirReadEx returns %Rrc", vrc); break; } /* * Only directories which conform with our temporary install/uninstall * naming scheme are candidates for cleaning. */ if ( RTFS_IS_DIRECTORY(Entry.Info.Attr.fMode) && strcmp(Entry.szName, ".") != 0 && strcmp(Entry.szName, "..") != 0) { bool fCandidate = false; char *pszMarker = strstr(Entry.szName, "-_-"); if ( pszMarker && ( !strcmp(pszMarker, "-_-uninst") || !strncmp(pszMarker, RT_STR_TUPLE("-_-inst")))) fCandidate = VBoxExtPackIsValidMangledName(Entry.szName, pszMarker - &Entry.szName[0]); if (fCandidate) { /* * Recursive delete, safe. */ char szPath[RTPATH_MAX]; vrc = RTPathJoin(szPath, sizeof(szPath), pszBaseDir, Entry.szName); if (RT_SUCCESS(vrc)) { RTEXITCODE rcExit2 = RemoveExtPackDir(szPath, true /*fTemporary*/); if (rcExit2 == RTEXITCODE_SUCCESS) RTMsgInfo("Successfully removed '%s'.", Entry.szName); else if (rcExit == RTEXITCODE_SUCCESS) rcExit = rcExit2; } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTPathJoin failed with %Rrc for '%s'", vrc, Entry.szName); cCleaned++; } } } RTDirClose(hDir); if (!cCleaned) RTMsgInfo("Nothing to clean."); return rcExit; } #ifdef WITH_ELEVATION #if !defined(RT_OS_WINDOWS) && !defined(RT_OS_DARWIN) /** * Looks in standard locations for a suitable exec tool. * * @returns true if found, false if not. * @param pszPath Where to store the path to the tool on * successs. * @param cbPath The size of the buffer @a pszPath points to. * @param pszName The name of the tool we're looking for. */ static bool FindExecTool(char *pszPath, size_t cbPath, const char *pszName) { static const char * const s_apszPaths[] = { "/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin", "/usr/local/sbin", #ifdef RT_OS_SOLARIS "/usr/sfw/bin", "/usr/gnu/bin", "/usr/xpg4/bin", "/usr/xpg6/bin", "/usr/openwin/bin", "/usr/ucb" #endif }; for (unsigned i = 0; i < RT_ELEMENTS(s_apszPaths); i++) { int vrc = RTPathJoin(pszPath, cbPath, s_apszPaths[i], pszName); if (RT_SUCCESS(vrc)) { RTFSOBJINFO ObjInfo; vrc = RTPathQueryInfoEx(pszPath, &ObjInfo, RTFSOBJATTRADD_UNIX, RTPATH_F_FOLLOW_LINK); if (RT_SUCCESS(vrc)) { if (!(ObjInfo.Attr.fMode & RTFS_UNIX_IWOTH)) return true; } } } return false; } #endif /** * Copies the content of a file to a stream. * * @param hSrc The source file. * @param pDst The destination stream. * @param fComplain Whether to complain about errors (i.e. is this * stderr, if not keep the trap shut because it * may be missing when running under VBoxSVC.) */ static void CopyFileToStdXxx(RTFILE hSrc, PRTSTREAM pDst, bool fComplain) { int vrc; for (;;) { char abBuf[0x1000]; size_t cbRead; vrc = RTFileRead(hSrc, abBuf, sizeof(abBuf), &cbRead); if (RT_FAILURE(vrc)) { RTMsgError("RTFileRead failed: %Rrc", vrc); break; } if (!cbRead) break; vrc = RTStrmWrite(pDst, abBuf, cbRead); if (RT_FAILURE(vrc)) { if (fComplain) RTMsgError("RTStrmWrite failed: %Rrc", vrc); break; } } vrc = RTStrmFlush(pDst); if (RT_FAILURE(vrc) && fComplain) RTMsgError("RTStrmFlush failed: %Rrc", vrc); } /** * Relaunches ourselves as a elevated process using platform specific facilities. * * @returns Program exit code. * @param pszExecPath The executable path. * @param papszArgs The arguments. * @param cSuArgs The number of argument entries reserved for the * 'su' like programs at the start of papszArgs. * @param cMyArgs The number of arguments following @a cSuArgs. * @param iCmd The command that is being executed. (For * selecting messages.) * @param pszDisplayInfoHack Display information hack. Platform specific++. */ static RTEXITCODE RelaunchElevatedNative(const char *pszExecPath, const char **papszArgs, int cSuArgs, int cMyArgs, int iCmd, const char *pszDisplayInfoHack) { RT_NOREF1(cMyArgs); RTEXITCODE rcExit = RTEXITCODE_FAILURE; #ifdef RT_OS_WINDOWS NOREF(iCmd); MSG Msg; PeekMessage(&Msg, NULL, 0, 0, PM_NOREMOVE); CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); SHELLEXECUTEINFOW Info; Info.cbSize = sizeof(Info); Info.fMask = SEE_MASK_NOCLOSEPROCESS; Info.hwnd = NULL; Info.lpVerb = L"runas"; int vrc = RTStrToUtf16(pszExecPath, (PRTUTF16 *)&Info.lpFile); if (RT_SUCCESS(vrc)) { char *pszCmdLine; vrc = RTGetOptArgvToString(&pszCmdLine, &papszArgs[cSuArgs + 1], RTGETOPTARGV_CNV_QUOTE_MS_CRT); if (RT_SUCCESS(vrc)) { vrc = RTStrToUtf16(pszCmdLine, (PRTUTF16 *)&Info.lpParameters); if (RT_SUCCESS(vrc)) { Info.lpDirectory = NULL; Info.nShow = SW_SHOWMAXIMIZED; Info.hInstApp = NULL; Info.lpIDList = NULL; Info.lpClass = NULL; Info.hkeyClass = NULL; Info.dwHotKey = 0; Info.hMonitor = NULL; Info.hProcess = INVALID_HANDLE_VALUE; /* Apply display hacks. */ if (pszDisplayInfoHack) { const char *pszArg = strstr(pszDisplayInfoHack, "hwnd="); if (pszArg) { uint64_t u64Hwnd; vrc = RTStrToUInt64Ex(pszArg + sizeof("hwnd=") - 1, NULL, 0, &u64Hwnd); if (RT_SUCCESS(vrc)) { HWND hwnd = (HWND)(uintptr_t)u64Hwnd; Info.hwnd = hwnd; Info.hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); } } } if (Info.hMonitor == NULL) { POINT Pt = {0,0}; Info.hMonitor = MonitorFromPoint(Pt, MONITOR_DEFAULTTOPRIMARY); } if (Info.hMonitor != NULL) Info.fMask |= SEE_MASK_HMONITOR; if (ShellExecuteExW(&Info)) { if (Info.hProcess != INVALID_HANDLE_VALUE) { /* * Wait for the process, make sure the deal with messages. */ for (;;) { DWORD dwRc = MsgWaitForMultipleObjects(1, &Info.hProcess, FALSE, 5000/*ms*/, QS_ALLEVENTS); if (dwRc == WAIT_OBJECT_0) break; if ( dwRc != WAIT_TIMEOUT && dwRc != WAIT_OBJECT_0 + 1) { RTMsgError("MsgWaitForMultipleObjects returned: %#x (%d), err=%u", dwRc, dwRc, GetLastError()); break; } while (PeekMessageW(&Msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&Msg); DispatchMessageW(&Msg); } } DWORD dwExitCode; if (GetExitCodeProcess(Info.hProcess, &dwExitCode)) { if (dwExitCode < 128) rcExit = (RTEXITCODE)dwExitCode; else rcExit = RTEXITCODE_FAILURE; } CloseHandle(Info.hProcess); } else RTMsgError("ShellExecuteExW return INVALID_HANDLE_VALUE as Info.hProcess"); } else RTMsgError("ShellExecuteExW failed: %u (%#x)", GetLastError(), GetLastError()); RTUtf16Free((PRTUTF16)Info.lpParameters); } RTStrFree(pszCmdLine); } RTUtf16Free((PRTUTF16)Info.lpFile); } else RTMsgError("RTStrToUtf16 failed: %Rc", vrc); #elif defined(RT_OS_DARWIN) RT_NOREF(pszDisplayInfoHack); char szIconName[RTPATH_MAX]; int vrc = RTPathAppPrivateArch(szIconName, sizeof(szIconName)); if (RT_SUCCESS(vrc)) vrc = RTPathAppend(szIconName, sizeof(szIconName), "../Resources/virtualbox.png"); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to construct icon path: %Rrc", vrc); AuthorizationRef AuthRef; OSStatus orc = AuthorizationCreate(NULL, 0, kAuthorizationFlagDefaults, &AuthRef); if (orc == errAuthorizationSuccess) { /* * Preautorize the privileged execution of ourselves. */ AuthorizationItem AuthItem = { kAuthorizationRightExecute, 0, NULL, 0 }; AuthorizationRights AuthRights = { 1, &AuthItem }; NOREF(iCmd); static char s_szPrompt[] = "VirtualBox needs further rights to make changes to your installation.\n\n"; AuthorizationItem aAuthEnvItems[] = { { kAuthorizationEnvironmentPrompt, strlen(s_szPrompt), s_szPrompt, 0 }, { kAuthorizationEnvironmentIcon, strlen(szIconName), szIconName, 0 } }; AuthorizationEnvironment AuthEnv = { RT_ELEMENTS(aAuthEnvItems), aAuthEnvItems }; orc = AuthorizationCopyRights(AuthRef, &AuthRights, &AuthEnv, kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, NULL); if (orc == errAuthorizationSuccess) { /* * Execute with extra permissions */ FILE *pSocketStrm; #if defined(__clang__) || RT_GNUC_PREREQ(4, 4) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif orc = AuthorizationExecuteWithPrivileges(AuthRef, pszExecPath, kAuthorizationFlagDefaults, (char * const *)&papszArgs[cSuArgs + 3], &pSocketStrm); #if defined(__clang__) || RT_GNUC_PREREQ(4, 4) # pragma GCC diagnostic pop #endif if (orc == errAuthorizationSuccess) { /* * Read the output of the tool, the read will fail when it quits. */ for (;;) { char achBuf[1024]; size_t cbRead = fread(achBuf, 1, sizeof(achBuf), pSocketStrm); if (!cbRead) break; fwrite(achBuf, 1, cbRead, stdout); } rcExit = RTEXITCODE_SUCCESS; fclose(pSocketStrm); } else RTMsgError("AuthorizationExecuteWithPrivileges failed: %d", orc); } else if (orc == errAuthorizationCanceled) RTMsgError("Authorization canceled by the user"); else RTMsgError("AuthorizationCopyRights failed: %d", orc); AuthorizationFree(AuthRef, kAuthorizationFlagDefaults); } else RTMsgError("AuthorizationCreate failed: %d", orc); #else RT_NOREF2(pszExecPath, pszDisplayInfoHack); /* * Several of the alternatives below will require a command line. */ char *pszCmdLine; int vrc = RTGetOptArgvToString(&pszCmdLine, &papszArgs[cSuArgs], RTGETOPTARGV_CNV_QUOTE_BOURNE_SH); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptArgvToString failed: %Rrc", vrc); /* * Look for various standard stuff for executing a program as root. * * N.B. When adding new arguments, please make 100% sure RelaunchElevated * allocates enough array entries. * * TODO: Feel free to contribute code for using PolicyKit directly. */ bool fHaveDisplayVar = RTEnvExist("DISPLAY"); int iSuArg = cSuArgs; char szExecTool[260]; char szXterm[260]; /* * kdesudo is available on KDE3/KDE4 */ if (fHaveDisplayVar && FindExecTool(szExecTool, sizeof(szExecTool), "kdesudo")) { iSuArg = cSuArgs - 4; papszArgs[cSuArgs - 4] = szExecTool; papszArgs[cSuArgs - 3] = "--comment"; papszArgs[cSuArgs - 2] = iCmd == CMD_INSTALL ? "VirtualBox extension pack installer" : iCmd == CMD_UNINSTALL ? "VirtualBox extension pack uninstaller" : "VirtualBox extension pack maintainer"; papszArgs[cSuArgs - 1] = "--"; } /* * gksu is our favorite as it is very well integrated. */ else if (fHaveDisplayVar && FindExecTool(szExecTool, sizeof(szExecTool), "gksu")) { #if 0 /* older gksu does not grok --description nor '--' and multiple args. */ iSuArg = cSuArgs - 4; papszArgs[cSuArgs - 4] = szExecTool; papszArgs[cSuArgs - 3] = "--description"; papszArgs[cSuArgs - 2] = iCmd == CMD_INSTALL ? "VirtualBox extension pack installer" : iCmd == CMD_UNINSTALL ? "VirtualBox extension pack uninstaller" : "VirtualBox extension pack maintainer"; papszArgs[cSuArgs - 1] = "--"; #elif defined(RT_OS_SOLARIS) /* Force it not to use pfexec as it won't wait then. */ iSuArg = cSuArgs - 4; papszArgs[cSuArgs - 4] = szExecTool; papszArgs[cSuArgs - 3] = "-au"; papszArgs[cSuArgs - 2] = "root"; papszArgs[cSuArgs - 1] = pszCmdLine; papszArgs[cSuArgs] = NULL; #else iSuArg = cSuArgs - 2; papszArgs[cSuArgs - 2] = szExecTool; papszArgs[cSuArgs - 1] = pszCmdLine; papszArgs[cSuArgs] = NULL; #endif } /* * pkexec may work for ssh console sessions as well if the right agents * are installed. However it is very generic and does not allow for any * custom messages. Thus it comes after gksu. */ else if (FindExecTool(szExecTool, sizeof(szExecTool), "pkexec")) { iSuArg = cSuArgs - 1; papszArgs[cSuArgs - 1] = szExecTool; } /* * The ultimate fallback is running 'su -' within an xterm. We use the * title of the xterm to tell what is going on. */ else if ( fHaveDisplayVar && FindExecTool(szExecTool, sizeof(szExecTool), "su") && FindExecTool(szXterm, sizeof(szXterm), "xterm")) { iSuArg = cSuArgs - 9; papszArgs[cSuArgs - 9] = szXterm; papszArgs[cSuArgs - 8] = "-T"; papszArgs[cSuArgs - 7] = iCmd == CMD_INSTALL ? "VirtualBox extension pack installer - su" : iCmd == CMD_UNINSTALL ? "VirtualBox extension pack uninstaller - su" : "VirtualBox extension pack maintainer - su"; papszArgs[cSuArgs - 6] = "-e"; papszArgs[cSuArgs - 5] = szExecTool; papszArgs[cSuArgs - 4] = "-"; papszArgs[cSuArgs - 3] = "root"; papszArgs[cSuArgs - 2] = "-c"; papszArgs[cSuArgs - 1] = pszCmdLine; papszArgs[cSuArgs] = NULL; } else if (fHaveDisplayVar) RTMsgError("Unable to locate 'pkexec', 'gksu' or 'su+xterm'. Try perform the operation using VBoxManage running as root"); else RTMsgError("Unable to locate 'pkexec'. Try perform the operation using VBoxManage running as root"); if (iSuArg != cSuArgs) { AssertRelease(iSuArg >= 0); /* * Argument list constructed, execute it and wait for the exec * program to complete. */ RTPROCESS hProcess; vrc = RTProcCreateEx(papszArgs[iSuArg], &papszArgs[iSuArg], RTENV_DEFAULT, 0 /*fFlags*/, NULL /*phStdIn*/, NULL /*phStdOut*/, NULL /*phStdErr*/, NULL /*pszAsUser*/, NULL /*pszPassword*/, NULL /* pvExtraData*/, &hProcess); if (RT_SUCCESS(vrc)) { RTPROCSTATUS Status; vrc = RTProcWait(hProcess, RTPROCWAIT_FLAGS_BLOCK, &Status); if (RT_SUCCESS(vrc)) { if (Status.enmReason == RTPROCEXITREASON_NORMAL) rcExit = (RTEXITCODE)Status.iStatus; else rcExit = RTEXITCODE_FAILURE; } else RTMsgError("Error while waiting for '%s': %Rrc", papszArgs[iSuArg], vrc); } else RTMsgError("Failed to execute '%s': %Rrc", papszArgs[iSuArg], vrc); } RTStrFree(pszCmdLine); #endif return rcExit; } /** * Relaunches ourselves as a elevated process using platform specific facilities. * * @returns Program exit code. * @param argc The number of arguments. * @param argv The arguments. * @param iCmd The command that is being executed. * @param pszDisplayInfoHack Display information hack. Platform specific++. */ static RTEXITCODE RelaunchElevated(int argc, char **argv, int iCmd, const char *pszDisplayInfoHack) { /* * We need the executable name later, so get it now when it's easy to quit. */ char szExecPath[RTPATH_MAX]; if (!RTProcGetExecutablePath(szExecPath,sizeof(szExecPath))) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcGetExecutablePath failed"); /* * Create a couple of temporary files for stderr and stdout. */ char szTempDir[RTPATH_MAX - sizeof("/stderr")]; int vrc = RTPathTemp(szTempDir, sizeof(szTempDir)); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTPathTemp failed: %Rrc", vrc); vrc = RTPathAppend(szTempDir, sizeof(szTempDir), "VBoxExtPackHelper-XXXXXX"); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTPathAppend failed: %Rrc", vrc); vrc = RTDirCreateTemp(szTempDir, 0700); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDirCreateTemp failed: %Rrc", vrc); RTEXITCODE rcExit = RTEXITCODE_FAILURE; char szStdOut[RTPATH_MAX]; char szStdErr[RTPATH_MAX]; vrc = RTPathJoin(szStdOut, sizeof(szStdOut), szTempDir, "stdout"); if (RT_SUCCESS(vrc)) vrc = RTPathJoin(szStdErr, sizeof(szStdErr), szTempDir, "stderr"); if (RT_SUCCESS(vrc)) { RTFILE hStdOut; vrc = RTFileOpen(&hStdOut, szStdOut, RTFILE_O_READWRITE | RTFILE_O_CREATE | RTFILE_O_DENY_NONE | (0600 << RTFILE_O_CREATE_MODE_SHIFT)); if (RT_SUCCESS(vrc)) { RTFILE hStdErr; vrc = RTFileOpen(&hStdErr, szStdErr, RTFILE_O_READWRITE | RTFILE_O_CREATE | RTFILE_O_DENY_NONE | (0600 << RTFILE_O_CREATE_MODE_SHIFT)); if (RT_SUCCESS(vrc)) { /* * Insert the --elevated and stdout/err names into the argument * list. Note that darwin skips the --stdout bit, so don't * change the order here. */ int const cSuArgs = 12; int cArgs = argc + 5 + 1; char const **papszArgs = (char const **)RTMemTmpAllocZ((cSuArgs + cArgs + 1) * sizeof(const char *)); if (papszArgs) { int iDst = cSuArgs; papszArgs[iDst++] = argv[0]; papszArgs[iDst++] = "--stdout"; papszArgs[iDst++] = szStdOut; papszArgs[iDst++] = "--stderr"; papszArgs[iDst++] = szStdErr; papszArgs[iDst++] = "--elevated"; for (int iSrc = 1; iSrc <= argc; iSrc++) papszArgs[iDst++] = argv[iSrc]; /* * Do the platform specific process execution (waiting included). */ rcExit = RelaunchElevatedNative(szExecPath, papszArgs, cSuArgs, cArgs, iCmd, pszDisplayInfoHack); /* * Copy the standard files to our standard handles. */ CopyFileToStdXxx(hStdErr, g_pStdErr, true /*fComplain*/); CopyFileToStdXxx(hStdOut, g_pStdOut, false); RTMemTmpFree(papszArgs); } RTFileClose(hStdErr); RTFileDelete(szStdErr); } RTFileClose(hStdOut); RTFileDelete(szStdOut); } } RTDirRemove(szTempDir); return rcExit; } /** * Checks if the process is elevated or not. * * @returns RTEXITCODE_SUCCESS if preconditions are fine, * otherwise error message + RTEXITCODE_FAILURE. * @param pfElevated Where to store the elevation indicator. */ static RTEXITCODE ElevationCheck(bool *pfElevated) { *pfElevated = false; # if defined(RT_OS_WINDOWS) /** @todo This should probably check if UAC is diabled and if we are * Administrator first. Also needs to check for Vista+ first, probably. */ DWORD cb; RTEXITCODE rcExit = RTEXITCODE_SUCCESS; HANDLE hToken; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "OpenProcessToken failed: %u (%#x)", GetLastError(), GetLastError()); /* * Check if we're member of the Administrators group. If we aren't, there * is no way to elevate ourselves to system admin. * N.B. CheckTokenMembership does not do the job here (due to attributes?). */ BOOL fIsAdmin = FALSE; SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; PSID pAdminGrpSid; if (AllocateAndInitializeSid(&NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdminGrpSid)) { # ifdef DEBUG char *pszAdminGrpSid = NULL; ConvertSidToStringSid(pAdminGrpSid, &pszAdminGrpSid); # endif if ( !GetTokenInformation(hToken, TokenGroups, NULL, 0, &cb) && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { PTOKEN_GROUPS pTokenGroups = (PTOKEN_GROUPS)RTMemAllocZ(cb); if (GetTokenInformation(hToken, TokenGroups, pTokenGroups, cb, &cb)) { for (DWORD iGrp = 0; iGrp < pTokenGroups->GroupCount; iGrp++) { # ifdef DEBUG char *pszGrpSid = NULL; ConvertSidToStringSid(pTokenGroups->Groups[iGrp].Sid, &pszGrpSid); # endif if (EqualSid(pAdminGrpSid, pTokenGroups->Groups[iGrp].Sid)) { /* That it's listed is enough I think, ignore attributes. */ fIsAdmin = TRUE; break; } } } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "GetTokenInformation(TokenGroups,cb) failed: %u (%#x)", GetLastError(), GetLastError()); RTMemFree(pTokenGroups); } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "GetTokenInformation(TokenGroups,0) failed: %u (%#x)", GetLastError(), GetLastError()); FreeSid(pAdminGrpSid); } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "AllocateAndInitializeSid failed: %u (%#x)", GetLastError(), GetLastError()); if (fIsAdmin) { /* * Check the integrity level (Vista / UAC). */ # define MY_SECURITY_MANDATORY_HIGH_RID 0x00003000L # define MY_TokenIntegrityLevel ((TOKEN_INFORMATION_CLASS)25) if ( !GetTokenInformation(hToken, MY_TokenIntegrityLevel, NULL, 0, &cb) && GetLastError() == ERROR_INSUFFICIENT_BUFFER) { PSID_AND_ATTRIBUTES pSidAndAttr = (PSID_AND_ATTRIBUTES)RTMemAlloc(cb); if (GetTokenInformation(hToken, MY_TokenIntegrityLevel, pSidAndAttr, cb, &cb)) { DWORD dwIntegrityLevel = *GetSidSubAuthority(pSidAndAttr->Sid, *GetSidSubAuthorityCount(pSidAndAttr->Sid) - 1U); if (dwIntegrityLevel >= MY_SECURITY_MANDATORY_HIGH_RID) *pfElevated = true; } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "GetTokenInformation failed: %u (%#x)", GetLastError(), GetLastError()); RTMemFree(pSidAndAttr); } else if ( GetLastError() == ERROR_INVALID_PARAMETER || GetLastError() == ERROR_NOT_SUPPORTED) *pfElevated = true; /* Older Windows version. */ else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "GetTokenInformation failed: %u (%#x)", GetLastError(), GetLastError()); } else rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Membership in the Administrators group is required to perform this action"); CloseHandle(hToken); return rcExit; # else /* * On Unixy systems, we check if the executable and the current user is * the same. This heuristic works fine for both hardened and development * builds. */ char szExecPath[RTPATH_MAX]; if (RTProcGetExecutablePath(szExecPath, sizeof(szExecPath)) == NULL) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcGetExecutablePath failed"); RTFSOBJINFO ObjInfo; int vrc = RTPathQueryInfoEx(szExecPath, &ObjInfo, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTPathQueryInfoEx failed"); *pfElevated = ObjInfo.Attr.u.Unix.uid == geteuid() || ObjInfo.Attr.u.Unix.uid == getuid(); return RTEXITCODE_SUCCESS; # endif } #endif /* WITH_ELEVATION */ int main(int argc, char **argv) { /* * Initialize IPRT and check that we're correctly installed. */ #ifdef RT_OS_WINDOWS int vrc = RTR3InitExe(argc, &argv, RTR3INIT_FLAGS_UTF8_ARGV); /* WinMain gives us UTF-8, see below. */ #else int vrc = RTR3InitExe(argc, &argv, 0); #endif if (RT_FAILURE(vrc)) return RTMsgInitFailure(vrc); SUPR3HardenedVerifyInit(); RTERRINFOSTATIC ErrInfo; RTErrInfoInitStatic(&ErrInfo); vrc = SUPR3HardenedVerifySelf(argv[0], true /*fInternal*/, &ErrInfo.Core); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "%s", ErrInfo.Core.pszMsg); /* * Elevation check. */ const char *pszDisplayInfoHack = NULL; RTEXITCODE rcExit; #ifdef WITH_ELEVATION bool fElevated; rcExit = ElevationCheck(&fElevated); if (rcExit != RTEXITCODE_SUCCESS) return rcExit; #endif /* * Parse the top level arguments until we find a command. */ static const RTGETOPTDEF s_aOptions[] = { { "install", CMD_INSTALL, RTGETOPT_REQ_NOTHING }, { "uninstall", CMD_UNINSTALL, RTGETOPT_REQ_NOTHING }, { "cleanup", CMD_CLEANUP, RTGETOPT_REQ_NOTHING }, #ifdef WITH_ELEVATION { "--elevated", OPT_ELEVATED, RTGETOPT_REQ_NOTHING }, { "--stdout", OPT_STDOUT, RTGETOPT_REQ_STRING }, { "--stderr", OPT_STDERR, RTGETOPT_REQ_STRING }, #endif { "--display-info-hack", OPT_DISP_INFO_HACK, RTGETOPT_REQ_STRING }, }; RTGETOPTSTATE GetState; vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0 /*fFlags*/); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptInit failed: %Rrc\n", vrc); for (;;) { RTGETOPTUNION ValueUnion; int ch = RTGetOpt(&GetState, &ValueUnion); switch (ch) { case 0: return RTMsgErrorExit(RTEXITCODE_SYNTAX, "No command specified"); case CMD_INSTALL: case CMD_UNINSTALL: case CMD_CLEANUP: { #ifdef WITH_ELEVATION if (!fElevated) return RelaunchElevated(argc, argv, ch, pszDisplayInfoHack); #endif int cCmdargs = argc - GetState.iNext; char **papszCmdArgs = argv + GetState.iNext; switch (ch) { case CMD_INSTALL: rcExit = DoInstall( cCmdargs, papszCmdArgs); break; case CMD_UNINSTALL: rcExit = DoUninstall(cCmdargs, papszCmdArgs); break; case CMD_CLEANUP: rcExit = DoCleanup( cCmdargs, papszCmdArgs); break; default: AssertReleaseFailedReturn(RTEXITCODE_FAILURE); } /* * Standard error should end with rcExit=RTEXITCODE_SUCCESS on * success since the exit code may otherwise get lost in the * process elevation fun. */ RTStrmFlush(g_pStdOut); RTStrmFlush(g_pStdErr); switch (rcExit) { case RTEXITCODE_SUCCESS: RTStrmPrintf(g_pStdErr, "rcExit=RTEXITCODE_SUCCESS\n"); break; default: RTStrmPrintf(g_pStdErr, "rcExit=%d\n", rcExit); break; } RTStrmFlush(g_pStdErr); RTStrmFlush(g_pStdOut); return rcExit; } #ifdef WITH_ELEVATION case OPT_ELEVATED: fElevated = true; break; case OPT_STDERR: case OPT_STDOUT: { # ifdef RT_OS_WINDOWS PRTUTF16 pwszName = NULL; vrc = RTStrToUtf16(ValueUnion.psz, &pwszName); if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Error converting '%s' to UTF-16: %Rrc\n", ValueUnion.psz, vrc); FILE *pFile = _wfreopen(pwszName, L"r+", ch == OPT_STDOUT ? stdout : stderr); RTUtf16Free(pwszName); # else FILE *pFile = freopen(ValueUnion.psz, "r+", ch == OPT_STDOUT ? stdout : stderr); # endif if (!pFile) { vrc = RTErrConvertFromErrno(errno); return RTMsgErrorExit(RTEXITCODE_FAILURE, "freopen on '%s': %Rrc", ValueUnion.psz, vrc); } break; } #endif case OPT_DISP_INFO_HACK: if (pszDisplayInfoHack) return RTMsgErrorExit(RTEXITCODE_SYNTAX, "--display-info-hack shall only occur once"); pszDisplayInfoHack = ValueUnion.psz; break; case 'h': case 'V': return DoStandardOption(ch); default: return RTGetOptPrintError(ch, &ValueUnion); } /* not currently reached */ } /* not reached */ } #ifdef RT_OS_WINDOWS extern "C" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { g_hInstance = hInstance; NOREF(hPrevInstance); NOREF(nShowCmd); NOREF(lpCmdLine); int vrc = RTR3InitExeNoArguments(0); if (RT_FAILURE(vrc)) return RTMsgInitFailure(vrc); LPWSTR pwszCmdLine = GetCommandLineW(); if (!pwszCmdLine) return RTMsgErrorExit(RTEXITCODE_FAILURE, "GetCommandLineW failed"); char *pszCmdLine; vrc = RTUtf16ToUtf8(pwszCmdLine, &pszCmdLine); /* leaked */ if (RT_FAILURE(vrc)) return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to convert the command line: %Rrc", vrc); int cArgs; char **papszArgs; vrc = RTGetOptArgvFromString(&papszArgs, &cArgs, pszCmdLine, RTGETOPTARGV_CNV_QUOTE_MS_CRT, NULL); if (RT_SUCCESS(vrc)) { vrc = main(cArgs, papszArgs); RTGetOptArgvFree(papszArgs); } else vrc = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTGetOptArgvFromString failed: %Rrc", vrc); RTStrFree(pszCmdLine); return vrc; } #endif