1
0
mirror of https://github.com/veracrypt/VeraCrypt.git synced 2026-06-17 10:06:06 -05:00

macOS: parse hdiutil -plist output via CoreFoundation instead of string scanning (#1776)

The hdiutil `-plist` output used for mount/dismount device discovery
(MountAuxVolumeImage and UpdateMountedVolumeInfo) was parsed with a
hand-rolled string scanner that assumed the value always follows the
requested key and that <key>/<string> pairs appear in a fixed order.

Replace it with the CoreFoundation property-list API
(CFPropertyListCreateWithData + dictionary/array navigation), which is
correct by construction and robust to hdiutil output ordering/variation.
An RAII helper (CFHolder) ensures CFRelease on every path.

Behavior is preserved: prefer the system-entity that carries a
mount-point, otherwise fall back to the first dev-entry, and match disk
images by normalized image-path. CoreFoundation is already linked on
macOS (via Cocoa), so no build changes are needed.

Verified end-to-end on Apple Silicon: mounting parses `hdiutil attach`
output and dismount parses `hdiutil info` output correctly.

Co-authored-by: Damian Rickard <damian@rickard.us>
This commit is contained in:
damianrickard
2026-06-16 00:54:58 -04:00
committed by GitHub
parent 40f6473c90
commit 37412adf04
+109 -97
View File
@@ -13,6 +13,8 @@
#include <fstream>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <CoreFoundation/CoreFoundation.h>
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>
@@ -30,73 +32,66 @@
namespace VeraCrypt
{
static string DecodePlistXmlString (const string &xmlString)
// RAII wrapper so CFRelease is never forgotten on early returns / exceptions.
class CFHolder
{
string decoded;
public:
explicit CFHolder (CFTypeRef ref = nullptr) : Ref (ref) { }
~CFHolder () { if (Ref) CFRelease (Ref); }
CFTypeRef Get () const { return Ref; }
private:
CFHolder (const CFHolder &);
CFHolder &operator= (const CFHolder &);
CFTypeRef Ref;
};
for (size_t i = 0; i < xmlString.size(); ++i)
{
if (xmlString[i] != '&')
{
decoded += xmlString[i];
continue;
}
static string CFStringToStdString (CFStringRef cfStr)
{
if (!cfStr)
return string();
if (xmlString.compare (i, 5, "&amp;") == 0)
{
decoded += '&';
i += 4;
}
else if (xmlString.compare (i, 4, "&lt;") == 0)
{
decoded += '<';
i += 3;
}
else if (xmlString.compare (i, 4, "&gt;") == 0)
{
decoded += '>';
i += 3;
}
else if (xmlString.compare (i, 6, "&quot;") == 0)
{
decoded += '"';
i += 5;
}
else if (xmlString.compare (i, 6, "&apos;") == 0)
{
decoded += '\'';
i += 5;
}
else
decoded += xmlString[i];
}
CFIndex length = CFStringGetLength (cfStr);
CFIndex maxSize = CFStringGetMaximumSizeForEncoding (length, kCFStringEncodingUTF8) + 1;
if (maxSize <= 0)
return string();
return decoded;
vector <char> buffer ((size_t) maxSize);
if (!CFStringGetCString (cfStr, &buffer[0], maxSize, kCFStringEncodingUTF8))
return string();
return string (&buffer[0]);
}
static bool ExtractPlistString (const string &xml, const string &key, size_t start, size_t limit, string &value, size_t *endPos = nullptr)
// Fetch a string value from a CFDictionary by (C-string) key; empty if absent
// or not a string.
static string CFDictionaryGetStdString (CFDictionaryRef dict, const char *key)
{
// hdiutil currently emits simple <key>name</key><string>value</string> pairs.
// This lightweight parser assumes the value follows the requested key.
string keyTag = "<key>" + key + "</key>";
size_t p = xml.find (keyTag, start);
if (p == string::npos || p >= limit)
return false;
if (!dict)
return string();
p = xml.find ("<string>", p + keyTag.size());
if (p == string::npos || p >= limit)
return false;
p += 8;
CFHolder cfKey (CFStringCreateWithCString (kCFAllocatorDefault, key, kCFStringEncodingUTF8));
if (!cfKey.Get())
return string();
size_t e = xml.find ("</string>", p);
if (e == string::npos || e > limit)
return false;
CFTypeRef value = CFDictionaryGetValue (dict, cfKey.Get()); // borrowed reference
if (!value || CFGetTypeID (value) != CFStringGetTypeID())
return string();
value = DecodePlistXmlString (xml.substr (p, e - p));
if (endPos)
*endPos = e + 9;
return CFStringToStdString ((CFStringRef) value);
}
return true;
// Parse an hdiutil -plist (XML) document into a CFPropertyList. Returns nullptr
// on failure; the caller owns the result and must CFRelease it.
static CFPropertyListRef ParsePropertyList (const string &xml)
{
if (xml.empty())
return nullptr;
CFHolder data (CFDataCreate (kCFAllocatorDefault, (const UInt8 *) xml.data(), (CFIndex) xml.size()));
if (!data.Get())
return nullptr;
return CFPropertyListCreateWithData (kCFAllocatorDefault, (CFDataRef) data.Get(), kCFPropertyListImmutable, nullptr, nullptr);
}
static string NormalizeDiskImagePath (const string &path)
@@ -125,45 +120,40 @@ namespace VeraCrypt
return normalized;
}
static bool ExtractDiskImageDeviceAndMountPoint (const string &xml, size_t start, size_t limit, DevicePath &device, DirectoryPath &mountPoint)
// Walk a "system-entities" array (from hdiutil attach/info). Prefer the entity
// that carries a mount-point; otherwise fall back to the first dev-entry.
static bool ExtractDeviceAndMountPointFromEntities (CFArrayRef entities, DevicePath &device, DirectoryPath &mountPoint)
{
if (!entities || CFGetTypeID (entities) != CFArrayGetTypeID())
return false;
string firstDevice;
string mountedDevice;
string mountedPath;
for (size_t p = start; ; )
CFIndex count = CFArrayGetCount (entities);
for (CFIndex i = 0; i < count; ++i)
{
size_t devKeyPos = xml.find ("<key>dev-entry</key>", p);
if (devKeyPos == string::npos || devKeyPos >= limit)
break;
string devEntry;
size_t devValueEnd = 0;
if (!ExtractPlistString (xml, "dev-entry", devKeyPos, limit, devEntry, &devValueEnd))
{
p = devKeyPos + 1;
CFTypeRef entry = CFArrayGetValueAtIndex (entities, i); // borrowed reference
if (!entry || CFGetTypeID (entry) != CFDictionaryGetTypeID())
continue;
CFDictionaryRef entryDict = (CFDictionaryRef) entry;
string devEntry = StringConverter::Trim (CFDictionaryGetStdString (entryDict, "dev-entry"));
if (devEntry.empty())
continue;
}
devEntry = StringConverter::Trim (devEntry);
if (firstDevice.empty())
firstDevice = devEntry;
size_t nextDevKeyPos = xml.find ("<key>dev-entry</key>", devValueEnd);
if (nextDevKeyPos == string::npos || nextDevKeyPos > limit)
nextDevKeyPos = limit;
string currentMountPoint;
// hdiutil currently emits dev-entry before mount-point inside each entity.
if (ExtractPlistString (xml, "mount-point", devValueEnd, nextDevKeyPos, currentMountPoint)
&& !currentMountPoint.empty())
string currentMountPoint = CFDictionaryGetStdString (entryDict, "mount-point");
if (!currentMountPoint.empty())
{
mountedDevice = devEntry;
mountedPath = currentMountPoint;
break;
}
p = devValueEnd;
}
if (!mountedDevice.empty())
@@ -182,6 +172,20 @@ namespace VeraCrypt
return false;
}
// Parse "hdiutil attach -plist" output (top-level dict with "system-entities").
static bool ExtractDiskImageDeviceAndMountPoint (const string &attachXml, DevicePath &device, DirectoryPath &mountPoint)
{
CFHolder plist (ParsePropertyList (attachXml));
if (!plist.Get() || CFGetTypeID (plist.Get()) != CFDictionaryGetTypeID())
return false;
CFTypeRef entities = CFDictionaryGetValue ((CFDictionaryRef) plist.Get(), CFSTR ("system-entities")); // borrowed
if (!entities || CFGetTypeID (entities) != CFArrayGetTypeID())
return false;
return ExtractDeviceAndMountPointFromEntities ((CFArrayRef) entities, device, mountPoint);
}
static bool FindDiskImageInfoByImagePath (const string &imagePath, DevicePath &device, DirectoryPath &mountPoint)
{
list <string> args;
@@ -191,27 +195,35 @@ namespace VeraCrypt
string xml = Process::Execute ("/usr/bin/hdiutil", args);
string normalizedImagePath = NormalizeDiskImagePath (imagePath);
for (size_t p = 0; ; )
CFHolder plist (ParsePropertyList (xml));
if (!plist.Get() || CFGetTypeID (plist.Get()) != CFDictionaryGetTypeID())
return false;
CFTypeRef images = CFDictionaryGetValue ((CFDictionaryRef) plist.Get(), CFSTR ("images")); // borrowed
if (!images || CFGetTypeID (images) != CFArrayGetTypeID())
return false;
CFArrayRef imageArray = (CFArrayRef) images;
CFIndex count = CFArrayGetCount (imageArray);
for (CFIndex i = 0; i < count; ++i)
{
size_t imageKeyPos = xml.find ("<key>image-path</key>", p);
if (imageKeyPos == string::npos)
break;
string currentImagePath;
size_t imageValueEnd = 0;
if (!ExtractPlistString (xml, "image-path", imageKeyPos, string::npos, currentImagePath, &imageValueEnd))
{
p = imageKeyPos + 1;
CFTypeRef image = CFArrayGetValueAtIndex (imageArray, i); // borrowed
if (!image || CFGetTypeID (image) != CFDictionaryGetTypeID())
continue;
}
size_t nextImageKeyPos = xml.find ("<key>image-path</key>", imageValueEnd);
if (NormalizeDiskImagePath (currentImagePath) == normalizedImagePath)
{
return ExtractDiskImageDeviceAndMountPoint (xml, imageValueEnd, nextImageKeyPos, device, mountPoint);
}
CFDictionaryRef imageDict = (CFDictionaryRef) image;
p = imageValueEnd;
string currentImagePath = CFDictionaryGetStdString (imageDict, "image-path");
if (NormalizeDiskImagePath (currentImagePath) != normalizedImagePath)
continue;
// Matching image found: extract from its system-entities (mirrors the
// previous behavior of returning the result for the first match).
CFTypeRef entities = CFDictionaryGetValue (imageDict, CFSTR ("system-entities")); // borrowed
if (!entities || CFGetTypeID (entities) != CFArrayGetTypeID())
return false;
return ExtractDeviceAndMountPointFromEntities ((CFArrayRef) entities, device, mountPoint);
}
return false;
@@ -473,7 +485,7 @@ namespace VeraCrypt
DevicePath virtualDev;
DirectoryPath mountPoint;
if (!ExtractDiskImageDeviceAndMountPoint (xml, 0, string::npos, virtualDev, mountPoint)
if (!ExtractDiskImageDeviceAndMountPoint (xml, virtualDev, mountPoint)
|| virtualDev.IsEmpty())
throw ParameterIncorrect (SRC_POS);
(void) mountPoint;