/* $Id: HostHardwareLinux.cpp $ */ /** @file * VirtualBox Main - Code for handling hardware detection under Linux, VBoxSVC. */ /* * Copyright (C) 2008-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 * *********************************************************************************************************************************/ #define LOG_GROUP LOG_GROUP_MAIN #include "HostHardwareLinux.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef VBOX_USB_WITH_SYSFS # ifdef VBOX_USB_WITH_INOTIFY # include /* O_CLOEXEC */ # include # include # include # include # endif #endif //#include #include #include #include #include #include #include #include /* * Define NVME constant here to allow building * on several kernel versions even if the * building host doesn't contain certain NVME * includes */ #define NVME_IOCTL_ID _IO('N', 0x40) /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ #ifdef TESTCASE static bool testing() { return true; } static bool fNoProbe = false; static bool noProbe() { return fNoProbe; } static void setNoProbe(bool val) { fNoProbe = val; } #else static bool testing() { return false; } static bool noProbe() { return false; } static void setNoProbe(bool val) { (void)val; } #endif /********************************************************************************************************************************* * Typedefs and Defines * *********************************************************************************************************************************/ typedef enum SysfsWantDevice_T { DVD, Floppy, FixedDisk } SysfsWantDevice_T; /********************************************************************************************************************************* * Internal Functions * *********************************************************************************************************************************/ static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_PROTO; static int getDriveInfoFromSysfs(DriveInfoList *pList, SysfsWantDevice_T wantDevice, bool *pfSuccess) RT_NOTHROW_PROTO; /** * Find the length of a string, ignoring trailing non-ascii or control * characters * * @note Code duplicated in HostHardwareFreeBSD.cpp */ static size_t strLenStripped(const char *pcsz) RT_NOTHROW_DEF { size_t cch = 0; for (size_t i = 0; pcsz[i] != '\0'; ++i) if (pcsz[i] > 32 /*space*/ && pcsz[i] < 127 /*delete*/) cch = i; return cch + 1; } /** * Get the name of a floppy drive according to the Linux floppy driver. * * @returns true on success, false if the name was not available (i.e. the * device was not readable, or the file name wasn't a PC floppy * device) * @param pcszNode the path to the device node for the device * @param Number the Linux floppy driver number for the drive. Required. * @param pszName where to store the name retrieved */ static bool floppyGetName(const char *pcszNode, unsigned Number, floppy_drive_name pszName) RT_NOTHROW_DEF { AssertPtrReturn(pcszNode, false); AssertPtrReturn(pszName, false); AssertReturn(Number <= 7, false); RTFILE File; int vrc = RTFileOpen(&File, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); if (RT_SUCCESS(vrc)) { int iRcIoCtl; vrc = RTFileIoCtl(File, FDGETDRVTYP, pszName, 0, &iRcIoCtl); RTFileClose(File); if (RT_SUCCESS(vrc) && iRcIoCtl >= 0) return true; } return false; } /** * Create a UDI and a description for a floppy drive based on a number and the * driver's name for it. * * We deliberately return an ugly sequence of characters as the description * rather than an English language string to avoid translation issues. * * @param pcszName the floppy driver name for the device (optional) * @param Number the number of the floppy (0 to 3 on FDC 0, 4 to 7 on * FDC 1) * @param pszDesc where to store the device description (optional) * @param cbDesc the size of the buffer in @a pszDesc * @param pszUdi where to store the device UDI (optional) * @param cbUdi the size of the buffer in @a pszUdi */ static void floppyCreateDeviceStrings(const floppy_drive_name pcszName, unsigned Number, char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOTHROW_DEF { AssertPtrNullReturnVoid(pcszName); AssertPtrNullReturnVoid(pszDesc); AssertReturnVoid(!pszDesc || cbDesc > 0); AssertPtrNullReturnVoid(pszUdi); AssertReturnVoid(!pszUdi || cbUdi > 0); AssertReturnVoid(Number <= 7); if (pcszName) { const char *pcszSize; switch(pcszName[0]) { case 'd': case 'q': case 'h': pcszSize = "5.25\""; break; case 'D': case 'H': case 'E': case 'u': pcszSize = "3.5\""; break; default: pcszSize = "(unknown)"; } if (pszDesc) RTStrPrintf(pszDesc, cbDesc, "%s %s K%s", pcszSize, &pcszName[1], Number > 3 ? ", FDC 2" : ""); } else { if (pszDesc) RTStrPrintf(pszDesc, cbDesc, "FDD %d%s", (Number & 4) + 1, Number > 3 ? ", FDC 2" : ""); } if (pszUdi) RTStrPrintf(pszUdi, cbUdi, "/org/freedesktop/Hal/devices/platform_floppy_%u_storage", Number); } /** * Check whether a device number might correspond to a CD-ROM device according * to Documentation/devices.txt in the Linux kernel source. * * @returns true if it might, false otherwise * @param Number the device number (major and minor combination) */ static bool isCdromDevNum(dev_t Number) RT_NOTHROW_DEF { int major = major(Number); int minor = minor(Number); if (major == IDE0_MAJOR && !(minor & 0x3f)) return true; if (major == SCSI_CDROM_MAJOR) return true; if (major == CDU31A_CDROM_MAJOR) return true; if (major == GOLDSTAR_CDROM_MAJOR) return true; if (major == OPTICS_CDROM_MAJOR) return true; if (major == SANYO_CDROM_MAJOR) return true; if (major == MITSUMI_X_CDROM_MAJOR) return true; if (major == IDE1_MAJOR && !(minor & 0x3f)) return true; if (major == MITSUMI_CDROM_MAJOR) return true; if (major == CDU535_CDROM_MAJOR) return true; if (major == MATSUSHITA_CDROM_MAJOR) return true; if (major == MATSUSHITA_CDROM2_MAJOR) return true; if (major == MATSUSHITA_CDROM3_MAJOR) return true; if (major == MATSUSHITA_CDROM4_MAJOR) return true; if (major == AZTECH_CDROM_MAJOR) return true; if (major == 30 /* CM205_CDROM_MAJOR */) /* no #define for some reason */ return true; if (major == CM206_CDROM_MAJOR) return true; if (major == IDE3_MAJOR && !(minor & 0x3f)) return true; if (major == 46 /* Parallel port ATAPI CD-ROM */) /* no #define */ return true; if (major == IDE4_MAJOR && !(minor & 0x3f)) return true; if (major == IDE5_MAJOR && !(minor & 0x3f)) return true; if (major == IDE6_MAJOR && !(minor & 0x3f)) return true; if (major == IDE7_MAJOR && !(minor & 0x3f)) return true; if (major == IDE8_MAJOR && !(minor & 0x3f)) return true; if (major == IDE9_MAJOR && !(minor & 0x3f)) return true; if (major == 113 /* VIOCD_MAJOR */) return true; return false; } /** * Send an SCSI INQUIRY command to a device and return selected information. * * @returns iprt status code * @retval VERR_TRY_AGAIN if the query failed but might succeed next time * @param pcszNode the full path to the device node * @param pbType where to store the SCSI device type on success (optional) * @param pszVendor where to store the vendor id string on success (optional) * @param cbVendor the size of the @a pszVendor buffer * @param pszModel where to store the product id string on success (optional) * @param cbModel the size of the @a pszModel buffer * @note check documentation on the SCSI INQUIRY command and the Linux kernel * SCSI headers included above if you want to understand what is going * on in this method. */ static int cdromDoInquiry(const char *pcszNode, uint8_t *pbType, char *pszVendor, size_t cbVendor, char *pszModel, size_t cbModel) RT_NOTHROW_DEF { LogRelFlowFunc(("pcszNode=%s, pbType=%p, pszVendor=%p, cbVendor=%zu, pszModel=%p, cbModel=%zu\n", pcszNode, pbType, pszVendor, cbVendor, pszModel, cbModel)); AssertPtrReturn(pcszNode, VERR_INVALID_POINTER); AssertPtrNullReturn(pbType, VERR_INVALID_POINTER); AssertPtrNullReturn(pszVendor, VERR_INVALID_POINTER); AssertPtrNullReturn(pszModel, VERR_INVALID_POINTER); RTFILE hFile = NIL_RTFILE; int vrc = RTFileOpen(&hFile, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); if (RT_SUCCESS(vrc)) { int iRcIoCtl = 0; unsigned char auchResponse[96] = { 0 }; struct cdrom_generic_command CdromCommandReq; RT_ZERO(CdromCommandReq); CdromCommandReq.cmd[0] = INQUIRY; CdromCommandReq.cmd[4] = sizeof(auchResponse); CdromCommandReq.buffer = auchResponse; CdromCommandReq.buflen = sizeof(auchResponse); CdromCommandReq.data_direction = CGC_DATA_READ; CdromCommandReq.timeout = 5000; /* ms */ vrc = RTFileIoCtl(hFile, CDROM_SEND_PACKET, &CdromCommandReq, 0, &iRcIoCtl); if (RT_SUCCESS(vrc) && iRcIoCtl < 0) vrc = RTErrConvertFromErrno(-CdromCommandReq.stat); RTFileClose(hFile); if (RT_SUCCESS(vrc)) { if (pbType) *pbType = auchResponse[0] & 0x1f; if (pszVendor) { RTStrPrintf(pszVendor, cbVendor, "%.8s", &auchResponse[8] /* vendor id string */); RTStrPurgeEncoding(pszVendor); } if (pszModel) { RTStrPrintf(pszModel, cbModel, "%.16s", &auchResponse[16] /* product id string */); RTStrPurgeEncoding(pszModel); } LogRelFlowFunc(("returning success: type=%u, vendor=%.8s, product=%.16s\n", auchResponse[0] & 0x1f, &auchResponse[8], &auchResponse[16])); return VINF_SUCCESS; } } LogRelFlowFunc(("returning %Rrc\n", vrc)); return vrc; } /** * Initialise the device strings (description and UDI) for a DVD drive based on * vendor and model name strings. * * @param pcszVendor the vendor ID string * @param pcszModel the product ID string * @param pszDesc where to store the description string (optional) * @param cbDesc the size of the buffer in @a pszDesc * @param pszUdi where to store the UDI string (optional) * @param cbUdi the size of the buffer in @a pszUdi * * @note Used for more than DVDs these days. */ static void dvdCreateDeviceStrings(const char *pcszVendor, const char *pcszModel, char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOEXCEPT { AssertPtrReturnVoid(pcszVendor); AssertPtrReturnVoid(pcszModel); AssertPtrNullReturnVoid(pszDesc); AssertReturnVoid(!pszDesc || cbDesc > 0); AssertPtrNullReturnVoid(pszUdi); AssertReturnVoid(!pszUdi || cbUdi > 0); size_t cchModel = strLenStripped(pcszModel); /* * Vendor and Model strings can contain trailing spaces. * Create trimmed copy of them because we should not modify * original strings. */ char* pszStartTrimmed = RTStrStripL(pcszVendor); char* pszVendor = RTStrDup(pszStartTrimmed); RTStrStripR(pszVendor); pszStartTrimmed = RTStrStripL(pcszModel); char* pszModel = RTStrDup(pszStartTrimmed); RTStrStripR(pszModel); size_t cbVendor = strlen(pszVendor); /* Create a cleaned version of the model string for the UDI string. */ char szCleaned[128]; for (unsigned i = 0; i < sizeof(szCleaned) && pcszModel[i] != '\0'; ++i) if ( (pcszModel[i] >= '0' && pcszModel[i] <= '9') || (pcszModel[i] >= 'A' && pcszModel[i] <= 'z')) szCleaned[i] = pcszModel[i]; else szCleaned[i] = '_'; szCleaned[RT_MIN(cchModel, sizeof(szCleaned) - 1)] = '\0'; /* Construct the description string as "Vendor Product" */ if (pszDesc) { if (cbVendor > 0) { RTStrPrintf(pszDesc, cbDesc, "%.*s %s", cbVendor, pszVendor, strlen(pszModel) > 0 ? pszModel : "(unknown drive model)"); RTStrPurgeEncoding(pszDesc); } else RTStrCopy(pszDesc, cbDesc, pszModel); } /* Construct the UDI string */ if (pszUdi) { if (cchModel > 0) RTStrPrintf(pszUdi, cbUdi, "/org/freedesktop/Hal/devices/storage_model_%s", szCleaned); else pszUdi[0] = '\0'; } } /** * Check whether the device is the NVME device. * @returns true on success, false if the name was not available (i.e. the * device was not readable, or the file name wasn't a NVME device) * @param pcszNode the path to the device node for the device */ static bool probeNVME(const char *pcszNode) RT_NOTHROW_DEF { AssertPtrReturn(pcszNode, false); RTFILE File; int vrc = RTFileOpen(&File, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); if (RT_SUCCESS(vrc)) { int iRcIoCtl; vrc = RTFileIoCtl(File, NVME_IOCTL_ID, NULL, 0, &iRcIoCtl); RTFileClose(File); if (RT_SUCCESS(vrc) && iRcIoCtl >= 0) return true; } return false; } /** * Check whether a device node points to a valid device and create a UDI and * a description for it, and store the device number, if it does. * * @returns true if the device is valid, false otherwise * @param pcszNode the path to the device node * @param isDVD are we looking for a DVD device (or a floppy device)? * @param pDevice where to store the device node (optional) * @param pszDesc where to store the device description (optional) * @param cbDesc the size of the buffer in @a pszDesc * @param pszUdi where to store the device UDI (optional) * @param cbUdi the size of the buffer in @a pszUdi */ static bool devValidateDevice(const char *pcszNode, bool isDVD, dev_t *pDevice, char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOTHROW_DEF { AssertPtrReturn(pcszNode, false); AssertPtrNullReturn(pDevice, false); AssertPtrNullReturn(pszDesc, false); AssertReturn(!pszDesc || cbDesc > 0, false); AssertPtrNullReturn(pszUdi, false); AssertReturn(!pszUdi || cbUdi > 0, false); RTFSOBJINFO ObjInfo; if (RT_FAILURE(RTPathQueryInfo(pcszNode, &ObjInfo, RTFSOBJATTRADD_UNIX))) return false; if (!RTFS_IS_DEV_BLOCK(ObjInfo.Attr.fMode)) return false; if (pDevice) *pDevice = ObjInfo.Attr.u.Unix.Device; if (isDVD) { char szVendor[128], szModel[128]; uint8_t u8Type; if (!isCdromDevNum(ObjInfo.Attr.u.Unix.Device)) return false; if (RT_FAILURE(cdromDoInquiry(pcszNode, &u8Type, szVendor, sizeof(szVendor), szModel, sizeof(szModel)))) return false; if (u8Type != TYPE_ROM) return false; dvdCreateDeviceStrings(szVendor, szModel, pszDesc, cbDesc, pszUdi, cbUdi); } else { /* Floppies on Linux are legacy devices with hardcoded majors and minors */ if (major(ObjInfo.Attr.u.Unix.Device) != FLOPPY_MAJOR) return false; unsigned Number; switch (minor(ObjInfo.Attr.u.Unix.Device)) { case 0: case 1: case 2: case 3: Number = minor(ObjInfo.Attr.u.Unix.Device); break; case 128: case 129: case 130: case 131: Number = minor(ObjInfo.Attr.u.Unix.Device) - 128 + 4; break; default: return false; } floppy_drive_name szName; if (!floppyGetName(pcszNode, Number, szName)) return false; floppyCreateDeviceStrings(szName, Number, pszDesc, cbDesc, pszUdi, cbUdi); } return true; } int VBoxMainDriveInfo::updateDVDs() RT_NOEXCEPT { LogFlowThisFunc(("entered\n")); int vrc; try { mDVDList.clear(); /* Always allow the user to override our auto-detection using an * environment variable. */ bool fSuccess = false; /* Have we succeeded in finding anything yet? */ vrc = getDriveInfoFromEnv("VBOX_CDROM", &mDVDList, true /* isDVD */, &fSuccess); setNoProbe(false); if (RT_SUCCESS(vrc) && (!fSuccess || testing())) vrc = getDriveInfoFromSysfs(&mDVDList, DVD, &fSuccess); if (RT_SUCCESS(vrc) && testing()) { setNoProbe(true); vrc = getDriveInfoFromSysfs(&mDVDList, DVD, &fSuccess); } } catch (std::bad_alloc &e) { vrc = VERR_NO_MEMORY; } LogFlowThisFunc(("vrc=%Rrc\n", vrc)); return vrc; } int VBoxMainDriveInfo::updateFloppies() RT_NOEXCEPT { LogFlowThisFunc(("entered\n")); int vrc; try { mFloppyList.clear(); bool fSuccess = false; /* Have we succeeded in finding anything yet? */ vrc = getDriveInfoFromEnv("VBOX_FLOPPY", &mFloppyList, false /* isDVD */, &fSuccess); setNoProbe(false); if (RT_SUCCESS(vrc) && (!fSuccess || testing())) vrc = getDriveInfoFromSysfs(&mFloppyList, Floppy, &fSuccess); if (RT_SUCCESS(vrc) && testing()) { setNoProbe(true); vrc = getDriveInfoFromSysfs(&mFloppyList, Floppy, &fSuccess); } } catch (std::bad_alloc &) { vrc = VERR_NO_MEMORY; } LogFlowThisFunc(("vrc=%Rrc\n", vrc)); return vrc; } int VBoxMainDriveInfo::updateFixedDrives() RT_NOEXCEPT { LogFlowThisFunc(("entered\n")); int vrc; try { mFixedDriveList.clear(); setNoProbe(false); bool fSuccess = false; /* Have we succeeded in finding anything yet? */ vrc = getDriveInfoFromSysfs(&mFixedDriveList, FixedDisk, &fSuccess); if (RT_SUCCESS(vrc) && testing()) { setNoProbe(true); vrc = getDriveInfoFromSysfs(&mFixedDriveList, FixedDisk, &fSuccess); } } catch (std::bad_alloc &) { vrc = VERR_NO_MEMORY; } LogFlowThisFunc(("vrc=%Rrc\n", vrc)); return vrc; } /** * Extract the names of drives from an environment variable and add them to a * list if they are valid. * * @returns iprt status code * @param pcszVar the name of the environment variable. The variable * value should be a list of device node names, separated * by ':' characters. * @param pList the list to append the drives found to * @param isDVD are we looking for DVD drives or for floppies? * @param pfSuccess this will be set to true if we found at least one drive * and to false otherwise. Optional. * * @note This is duplicated in HostHardwareFreeBSD.cpp. */ static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_DEF { AssertPtrReturn(pcszVar, VERR_INVALID_POINTER); AssertPtrReturn(pList, VERR_INVALID_POINTER); AssertPtrNullReturn(pfSuccess, VERR_INVALID_POINTER); LogFlowFunc(("pcszVar=%s, pList=%p, isDVD=%d, pfSuccess=%p\n", pcszVar, pList, isDVD, pfSuccess)); int vrc = VINF_SUCCESS; bool success = false; char *pszFreeMe = RTEnvDupEx(RTENV_DEFAULT, pcszVar); try { char *pszCurrent = pszFreeMe; while (pszCurrent && *pszCurrent != '\0') { char *pszNext = strchr(pszCurrent, ':'); if (pszNext) *pszNext++ = '\0'; char szReal[RTPATH_MAX]; char szDesc[256], szUdi[256]; if ( RT_SUCCESS(RTPathReal(pszCurrent, szReal, sizeof(szReal))) && devValidateDevice(szReal, isDVD, NULL, szDesc, sizeof(szDesc), szUdi, sizeof(szUdi))) { pList->push_back(DriveInfo(szReal, szUdi, szDesc)); success = true; } pszCurrent = pszNext; } if (pfSuccess != NULL) *pfSuccess = success; } catch (std::bad_alloc &) { vrc = VERR_NO_MEMORY; } RTStrFree(pszFreeMe); LogFlowFunc(("vrc=%Rrc, success=%d\n", vrc, success)); return vrc; } class SysfsBlockDev { public: SysfsBlockDev(const char *pcszName, SysfsWantDevice_T wantDevice) RT_NOEXCEPT : mpcszName(pcszName), mWantDevice(wantDevice), misConsistent(true), misValid(false) { if (findDeviceNode()) { switch (mWantDevice) { case DVD: validateAndInitForDVD(); break; case Floppy: validateAndInitForFloppy(); break; default: validateAndInitForFixedDisk(); break; } } } private: /** The name of the subdirectory of /sys/block for this device */ const char *mpcszName; /** Are we looking for a floppy, a DVD or a fixed disk device? */ SysfsWantDevice_T mWantDevice; /** The device node for the device */ char mszNode[RTPATH_MAX]; /** Does the sysfs entry look like we expect it too? This is a canary * for future sysfs ABI changes. */ bool misConsistent; /** Is this entry a valid specimen of what we are looking for? */ bool misValid; /** Human readable drive description string */ char mszDesc[256]; /** Unique identifier for the drive. Should be identical to hal's UDI for * the device. May not be unique for two identical drives. */ char mszUdi[256]; private: /* Private methods */ /** * Fill in the device node member based on the /sys/block subdirectory. * @returns boolean success value */ bool findDeviceNode() RT_NOEXCEPT { dev_t dev = 0; int vrc = RTLinuxSysFsReadDevNumFile(&dev, "block/%s/dev", mpcszName); if (RT_FAILURE(vrc) || dev == 0) { misConsistent = false; return false; } vrc = RTLinuxCheckDevicePath(dev, RTFS_TYPE_DEV_BLOCK, mszNode, sizeof(mszNode), "%s", mpcszName); return RT_SUCCESS(vrc); } /** Check whether the sysfs block entry is valid for a DVD device and * initialise the string data members for the object. We try to get all * the information we need from sysfs if possible, to avoid unnecessarily * poking the device, and if that fails we fall back to an SCSI INQUIRY * command. */ void validateAndInitForDVD() RT_NOEXCEPT { int64_t type = 0; int vrc = RTLinuxSysFsReadIntFile(10, &type, "block/%s/device/type", mpcszName); if (RT_SUCCESS(vrc) && type != TYPE_ROM) return; if (type == TYPE_ROM) { char szVendor[128]; vrc = RTLinuxSysFsReadStrFile(szVendor, sizeof(szVendor), NULL, "block/%s/device/vendor", mpcszName); if (RT_SUCCESS(vrc)) { char szModel[128]; vrc = RTLinuxSysFsReadStrFile(szModel, sizeof(szModel), NULL, "block/%s/device/model", mpcszName); if (RT_SUCCESS(vrc)) { misValid = true; dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); return; } } } if (!noProbe()) probeAndInitForDVD(); } /** Try to find out whether a device is a DVD drive by sending it an * SCSI INQUIRY command. If it is, initialise the string and validity * data members for the object based on the returned data. */ void probeAndInitForDVD() RT_NOEXCEPT { AssertReturnVoid(mszNode[0] != '\0'); uint8_t bType = 0; char szVendor[128] = ""; char szModel[128] = ""; int vrc = cdromDoInquiry(mszNode, &bType, szVendor, sizeof(szVendor), szModel, sizeof(szModel)); if (RT_SUCCESS(vrc) && bType == TYPE_ROM) { misValid = true; dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); } } /** Check whether the sysfs block entry is valid for a floppy device and * initialise the string data members for the object. Since we only * support floppies using the basic "floppy" driver, we check the driver * using the entry name and a driver-specific ioctl. */ void validateAndInitForFloppy() RT_NOEXCEPT { floppy_drive_name szName; char szDriver[8]; if ( mpcszName[0] != 'f' || mpcszName[1] != 'd' || mpcszName[2] < '0' || mpcszName[2] > '7' || mpcszName[3] != '\0') return; bool fHaveName = false; if (!noProbe()) fHaveName = floppyGetName(mszNode, mpcszName[2] - '0', szName); int vrc = RTLinuxSysFsGetLinkDest(szDriver, sizeof(szDriver), NULL, "block/%s/%s", mpcszName, "device/driver"); if (RT_SUCCESS(vrc)) { if (RTStrCmp(szDriver, "floppy")) return; } else if (!fHaveName) return; floppyCreateDeviceStrings(fHaveName ? szName : NULL, mpcszName[2] - '0', mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); misValid = true; } void validateAndInitForFixedDisk() RT_NOEXCEPT { /* * For current task only device path is needed. Therefore, device probing * is skipped and other fields are empty if there aren't files in the * device entry. */ int64_t type = 0; int vrc = RTLinuxSysFsReadIntFile(10, &type, "block/%s/device/type", mpcszName); if (!RT_SUCCESS(vrc) || type != TYPE_DISK) { if (noProbe() || !probeNVME(mszNode)) { char szDriver[16]; vrc = RTLinuxSysFsGetLinkDest(szDriver, sizeof(szDriver), NULL, "block/%s/%s", mpcszName, "device/device/driver"); if (RT_FAILURE(vrc) || RTStrCmp(szDriver, "nvme")) return; } } char szVendor[128]; char szModel[128]; size_t cbRead = 0; vrc = RTLinuxSysFsReadStrFile(szVendor, sizeof(szVendor), &cbRead, "block/%s/device/vendor", mpcszName); szVendor[cbRead] = '\0'; /* Assume the model is always present. Vendor is not present for NVME disks */ cbRead = 0; vrc = RTLinuxSysFsReadStrFile(szModel, sizeof(szModel), &cbRead, "block/%s/device/model", mpcszName); szModel[cbRead] = '\0'; if (RT_SUCCESS(vrc)) { misValid = true; dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); } } public: bool isConsistent() const RT_NOEXCEPT { return misConsistent; } bool isValid() const RT_NOEXCEPT { return misValid; } const char *getDesc() const RT_NOEXCEPT { return mszDesc; } const char *getUdi() const RT_NOEXCEPT { return mszUdi; } const char *getNode() const RT_NOEXCEPT { return mszNode; } }; /** * Helper function to query the sysfs subsystem for information about DVD * drives attached to the system. * @returns iprt status code * @param pList where to add information about the drives detected * @param wantDevice The kind of devices we're looking for. * @param pfSuccess Did we find anything? * * @returns IPRT status code * @throws Nothing. */ static int getDriveInfoFromSysfs(DriveInfoList *pList, SysfsWantDevice_T wantDevice, bool *pfSuccess) RT_NOTHROW_DEF { AssertPtrReturn(pList, VERR_INVALID_POINTER); AssertPtrNullReturn(pfSuccess, VERR_INVALID_POINTER); /* Valid or Null */ LogFlowFunc (("pList=%p, wantDevice=%u, pfSuccess=%p\n", pList, (unsigned)wantDevice, pfSuccess)); if (!RTPathExists("/sys")) return VINF_SUCCESS; bool fSuccess = true; unsigned cFound = 0; RTDIR hDir = NIL_RTDIR; int vrc = RTDirOpen(&hDir, "/sys/block"); /* This might mean that sysfs semantics have changed */ AssertReturn(vrc != VERR_FILE_NOT_FOUND, VINF_SUCCESS); if (RT_SUCCESS(vrc)) { for (;;) { RTDIRENTRY entry; vrc = RTDirRead(hDir, &entry, NULL); Assert(vrc != VERR_BUFFER_OVERFLOW); /* Should never happen... */ if (RT_FAILURE(vrc)) /* Including overflow and no more files */ break; if (entry.szName[0] == '.') continue; SysfsBlockDev dev(entry.szName, wantDevice); /* This might mean that sysfs semantics have changed */ AssertBreakStmt(dev.isConsistent(), fSuccess = false); if (!dev.isValid()) continue; try { pList->push_back(DriveInfo(dev.getNode(), dev.getUdi(), dev.getDesc())); } catch (std::bad_alloc &e) { vrc = VERR_NO_MEMORY; break; } ++cFound; } RTDirClose(hDir); } if (vrc == VERR_NO_MORE_FILES) vrc = VINF_SUCCESS; else if (RT_FAILURE(vrc)) /* Clean up again */ while (cFound-- > 0) pList->pop_back(); if (pfSuccess) *pfSuccess = fSuccess; LogFlow (("vrc=%Rrc, fSuccess=%u\n", vrc, (unsigned)fSuccess)); return vrc; } /** Helper for readFilePathsFromDir(). Adds a path to the vector if it is not * NULL and not a dotfile (".", "..", ".*"). */ static int maybeAddPathToVector(const char *pcszPath, const char *pcszEntry, VECTOR_PTR(char *) *pvecpchDevs) RT_NOTHROW_DEF { if (!pcszPath) return 0; if (pcszEntry[0] == '.') return 0; char *pszPath = RTStrDup(pcszPath); if (pszPath) { int vrc = VEC_PUSH_BACK_PTR(pvecpchDevs, char *, pszPath); if (RT_SUCCESS(vrc)) return 0; } return ENOMEM; } /** * Helper for readFilePaths(). * * Adds the entries from the open directory @a pDir to the vector @a pvecpchDevs * using either the full path or the realpath() and skipping hidden files * and files on which realpath() fails. */ static int readFilePathsFromDir(const char *pcszPath, DIR *pDir, VECTOR_PTR(char *) *pvecpchDevs, int withRealPath) RT_NOTHROW_DEF { struct dirent entry, *pResult; int err; #if RT_GNUC_PREREQ(4, 6) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif for (err = readdir_r(pDir, &entry, &pResult); pResult != NULL && err == 0; err = readdir_r(pDir, &entry, &pResult)) #if RT_GNUC_PREREQ(4, 6) # pragma GCC diagnostic pop #endif { /* We (implicitly) require that PATH_MAX be defined */ char szPath[PATH_MAX + 1], szRealPath[PATH_MAX + 1], *pszPath; if (snprintf(szPath, sizeof(szPath), "%s/%s", pcszPath, entry.d_name) < 0) return errno; if (withRealPath) pszPath = realpath(szPath, szRealPath); else pszPath = szPath; if ((err = maybeAddPathToVector(pszPath, entry.d_name, pvecpchDevs))) return err; } return err; } /** * Helper for walkDirectory to dump the names of a directory's entries into a * vector of char pointers. * * @returns zero on success or (positive) posix error value. * @param pcszPath the path to dump. * @param pvecpchDevs an empty vector of char pointers - must be cleaned up * by the caller even on failure. * @param withRealPath whether to canonicalise the filename with realpath */ static int readFilePaths(const char *pcszPath, VECTOR_PTR(char *) *pvecpchDevs, int withRealPath) RT_NOTHROW_DEF { AssertPtrReturn(pvecpchDevs, EINVAL); AssertReturn(VEC_SIZE_PTR(pvecpchDevs) == 0, EINVAL); AssertPtrReturn(pcszPath, EINVAL); DIR *pDir = opendir(pcszPath); if (!pDir) return RTErrConvertFromErrno(errno); int err = readFilePathsFromDir(pcszPath, pDir, pvecpchDevs, withRealPath); if (closedir(pDir) < 0 && !err) err = errno; return RTErrConvertFromErrno(err); } class hotplugNullImpl : public VBoxMainHotplugWaiterImpl { public: hotplugNullImpl(const char *) {} virtual ~hotplugNullImpl (void) {} /** @copydoc VBoxMainHotplugWaiter::Wait */ virtual int Wait (RTMSINTERVAL cMillies) { NOREF(cMillies); return VERR_NOT_SUPPORTED; } /** @copydoc VBoxMainHotplugWaiter::Interrupt */ virtual void Interrupt (void) {} virtual int getStatus(void) { return VERR_NOT_SUPPORTED; } }; #ifdef VBOX_USB_WITH_SYSFS # ifdef VBOX_USB_WITH_INOTIFY /** Class wrapper around an inotify watch (or a group of them to be precise). */ typedef struct inotifyWatch { /** The native handle of the inotify fd. */ int mhInotify; } inotifyWatch; /** The flags we pass to inotify - modify, create, delete, change permissions */ #define MY_IN_FLAGS (IN_CREATE | IN_DELETE | IN_MODIFY | IN_ATTRIB) AssertCompile(MY_IN_FLAGS == 0x306); static int iwAddWatch(inotifyWatch *pSelf, const char *pcszPath) { errno = 0; if ( inotify_add_watch(pSelf->mhInotify, pcszPath, MY_IN_FLAGS) >= 0 || errno == EACCES) return VINF_SUCCESS; /* Other errors listed in the manpage can be treated as fatal */ return RTErrConvertFromErrno(errno); } /** Object initialisation */ static int iwInit(inotifyWatch *pSelf) { AssertPtr(pSelf); pSelf->mhInotify = -1; int fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (fd >= 0) { pSelf->mhInotify = fd; return VINF_SUCCESS; } Assert(errno > 0); return RTErrConvertFromErrno(errno); } static void iwTerm(inotifyWatch *pSelf) { AssertPtrReturnVoid(pSelf); if (pSelf->mhInotify != -1) { close(pSelf->mhInotify); pSelf->mhInotify = -1; } } static int iwGetFD(inotifyWatch *pSelf) { AssertPtrReturn(pSelf, -1); return pSelf->mhInotify; } # define SYSFS_WAKEUP_STRING "Wake up!" class hotplugInotifyImpl : public VBoxMainHotplugWaiterImpl { /** Pipe used to interrupt wait(), the read end. */ int mhWakeupPipeR; /** Pipe used to interrupt wait(), the write end. */ int mhWakeupPipeW; /** The inotify watch set */ inotifyWatch mWatches; /** Flag to mark that the Wait() method is currently being called, and to * ensure that it isn't called multiple times in parallel. */ volatile uint32_t mfWaiting; /** The root of the USB devices tree. */ const char *mpcszDevicesRoot; /** iprt result code from object initialisation. Should be AssertReturn-ed * on at the start of all methods. I went this way because I didn't want * to deal with exceptions. */ int mStatus; /** ID values associates with the wakeup pipe and the FAM socket for polling */ enum { RPIPE_ID = 0, INOTIFY_ID, MAX_POLLID }; /** Clean up any resources in use, gracefully skipping over any which have * not yet been allocated or already cleaned up. Intended to be called * from the destructor or after a failed initialisation. */ void term(void); int drainInotify(); /** Read the wakeup string from the wakeup pipe */ int drainWakeupPipe(void); public: hotplugInotifyImpl(const char *pcszDevicesRoot); virtual ~hotplugInotifyImpl(void) { term(); #ifdef DEBUG /** The first call to term should mark all resources as freed, so this * should be a semantic no-op. */ term(); #endif } /** Is inotify available and working on this system? If so we expect that * this implementation will be usable. */ static bool Available(void) { int const fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (fd >= 0) close(fd); return fd >= 0; } virtual int getStatus(void) { return mStatus; } /** @copydoc VBoxMainHotplugWaiter::Wait */ virtual int Wait(RTMSINTERVAL); /** @copydoc VBoxMainHotplugWaiter::Interrupt */ virtual void Interrupt(void); }; /** Simplified version of RTPipeCreate */ static int pipeCreateSimple(int *phPipeRead, int *phPipeWrite) { AssertPtrReturn(phPipeRead, VERR_INVALID_POINTER); AssertPtrReturn(phPipeWrite, VERR_INVALID_POINTER); /* * Create the pipe and set the close-on-exec flag. * ASSUMES we're building and running on Linux 2.6.27 or later (pipe2). */ int aFds[2] = {-1, -1}; if (pipe2(aFds, O_CLOEXEC)) return RTErrConvertFromErrno(errno); *phPipeRead = aFds[0]; *phPipeWrite = aFds[1]; /* * Before we leave, make sure to shut up SIGPIPE. */ signal(SIGPIPE, SIG_IGN); return VINF_SUCCESS; } hotplugInotifyImpl::hotplugInotifyImpl(const char *pcszDevicesRoot) : mhWakeupPipeR(-1), mhWakeupPipeW(-1), mfWaiting(0) , mpcszDevicesRoot(pcszDevicesRoot), mStatus(VERR_WRONG_ORDER) { # ifdef DEBUG /* Excercise the code path (term() on a not-fully-initialised object) as * well as we can. On an uninitialised object this method is a semantic * no-op. */ mWatches.mhInotify = -1; /* term will access this variable */ term(); /* For now this probing method should only be used if nothing else is * available */ # endif int vrc = iwInit(&mWatches); if (RT_SUCCESS(vrc)) { vrc = iwAddWatch(&mWatches, mpcszDevicesRoot); if (RT_SUCCESS(vrc)) vrc = pipeCreateSimple(&mhWakeupPipeR, &mhWakeupPipeW); } mStatus = vrc; if (RT_FAILURE(vrc)) term(); } void hotplugInotifyImpl::term(void) { /** This would probably be a pending segfault, so die cleanly */ AssertRelease(!mfWaiting); if (mhWakeupPipeR != -1) { close(mhWakeupPipeR); mhWakeupPipeR = -1; } if (mhWakeupPipeW != -1) { close(mhWakeupPipeW); mhWakeupPipeW = -1; } iwTerm(&mWatches); } int hotplugInotifyImpl::drainInotify() { char chBuf[RTPATH_MAX + 256]; /* Should always be big enough */ ssize_t cchRead; AssertRCReturn(mStatus, VERR_WRONG_ORDER); errno = 0; do cchRead = read(iwGetFD(&mWatches), chBuf, sizeof(chBuf)); while (cchRead > 0); if (cchRead == 0) return VINF_SUCCESS; if ( cchRead < 0 && ( errno == EAGAIN #if EAGAIN != EWOULDBLOCK || errno == EWOULDBLOCK #endif )) return VINF_SUCCESS; Assert(errno > 0); return RTErrConvertFromErrno(errno); } int hotplugInotifyImpl::drainWakeupPipe(void) { char szBuf[sizeof(SYSFS_WAKEUP_STRING)]; ssize_t cbRead; AssertRCReturn(mStatus, VERR_WRONG_ORDER); cbRead = read(mhWakeupPipeR, szBuf, sizeof(szBuf)); Assert(cbRead > 0); NOREF(cbRead); return VINF_SUCCESS; } int hotplugInotifyImpl::Wait(RTMSINTERVAL aMillies) { AssertRCReturn(mStatus, VERR_WRONG_ORDER); bool fEntered = ASMAtomicCmpXchgU32(&mfWaiting, 1, 0); AssertReturn(fEntered, VERR_WRONG_ORDER); VECTOR_PTR(char *) vecpchDevs; VEC_INIT_PTR(&vecpchDevs, char *, RTStrFree); int vrc = readFilePaths(mpcszDevicesRoot, &vecpchDevs, false); if (RT_SUCCESS(vrc)) { char **ppszEntry; VEC_FOR_EACH(&vecpchDevs, char *, ppszEntry) if (RT_FAILURE(vrc = iwAddWatch(&mWatches, *ppszEntry))) break; if (RT_SUCCESS(vrc)) { struct pollfd pollFD[MAX_POLLID]; pollFD[RPIPE_ID].fd = mhWakeupPipeR; pollFD[RPIPE_ID].events = POLLIN; pollFD[INOTIFY_ID].fd = iwGetFD(&mWatches); pollFD[INOTIFY_ID].events = POLLIN | POLLERR | POLLHUP; errno = 0; int cPolled = poll(pollFD, RT_ELEMENTS(pollFD), aMillies); if (cPolled < 0) { Assert(errno > 0); vrc = RTErrConvertFromErrno(errno); } else if (pollFD[RPIPE_ID].revents) { vrc = drainWakeupPipe(); if (RT_SUCCESS(vrc)) vrc = VERR_INTERRUPTED; } else if ((pollFD[INOTIFY_ID].revents)) { if (cPolled == 1) vrc = drainInotify(); else AssertFailedStmt(vrc = VERR_INTERNAL_ERROR); } else { if (errno == 0 && cPolled == 0) vrc = VERR_TIMEOUT; else AssertFailedStmt(vrc = VERR_INTERNAL_ERROR); } } } mfWaiting = 0; VEC_CLEANUP_PTR(&vecpchDevs); return vrc; } void hotplugInotifyImpl::Interrupt(void) { AssertRCReturnVoid(mStatus); ssize_t cbWritten = write(mhWakeupPipeW, SYSFS_WAKEUP_STRING, sizeof(SYSFS_WAKEUP_STRING)); if (cbWritten > 0) fsync(mhWakeupPipeW); } # endif /* VBOX_USB_WITH_INOTIFY */ #endif /* VBOX_USB_WTH_SYSFS */ VBoxMainHotplugWaiter::VBoxMainHotplugWaiter(const char *pcszDevicesRoot) { try { #ifdef VBOX_USB_WITH_SYSFS # ifdef VBOX_USB_WITH_INOTIFY if (hotplugInotifyImpl::Available()) { mImpl = new hotplugInotifyImpl(pcszDevicesRoot); return; } # endif /* VBOX_USB_WITH_INOTIFY */ #endif /* VBOX_USB_WITH_SYSFS */ mImpl = new hotplugNullImpl(pcszDevicesRoot); } catch (std::bad_alloc &e) { } }