-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathdrivelist_linux.cpp
More file actions
368 lines (307 loc) · 13 KB
/
drivelist_linux.cpp
File metadata and controls
368 lines (307 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (C) 2020-2025 Raspberry Pi Ltd
*
* Linux drive enumeration using lsblk.
*
* Design notes:
* - Uses lsblk with JSON output for reliable parsing
* - Parsing logic is separated from command execution for testability
* - Handles various edge cases: loop devices, NVMe, SD cards, internal readers
*/
#include "drivelist.h"
#include "embedded_config.h"
#include <optional>
#include <unistd.h>
#include <QProcess>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
namespace Drivelist {
namespace {
// Maximum recursion depth for walking device children
// Prevents stack overflow from malformed or malicious lsblk output
constexpr int MAX_CHILD_RECURSION_DEPTH = 10;
/**
* @brief Walk device children to collect mountpoints and labels
* @param depth Current recursion depth (starts at 0)
*/
void walkStorageChildren(DeviceDescriptor& device, QStringList& labels, const QJsonArray& children, int depth = 0)
{
if (depth >= MAX_CHILD_RECURSION_DEPTH) {
qWarning() << "walkStorageChildren: max recursion depth reached, stopping traversal";
return;
}
for (const auto& child : children) {
QJsonObject childObj = child.toObject();
QString label = childObj["label"].toString();
QString mountpoint = childObj["mountpoint"].toString();
if (!label.isEmpty()) {
labels.append(label);
}
if (!mountpoint.isEmpty()) {
device.mountpoints.push_back(mountpoint.toStdString());
device.mountpointLabels.push_back(label.toStdString());
}
// Recurse into nested children (e.g., LVM on partition)
QJsonArray subChildren = childObj["children"].toArray();
if (!subChildren.isEmpty()) {
walkStorageChildren(device, labels, subChildren, depth + 1);
}
}
}
/**
* @brief Parse a single block device from JSON
*/
std::optional<DeviceDescriptor> parseBlockDevice(const QJsonObject& bdev, bool embeddedMode)
{
DeviceDescriptor device;
QString name = bdev["kname"].toString();
QString subsystems = bdev["subsystems"].toString();
// Skip devices we never want to show
if (name.isEmpty()) return std::nullopt;
// Skip CD/DVD drives, RAM devices, compressed RAM
if (name.startsWith("/dev/sr") ||
name.startsWith("/dev/ram") ||
name.startsWith("/dev/zram")) {
return std::nullopt;
}
// Skip eMMC boot partitions (special hardware boot areas)
if (name.contains("boot") && name.contains("mmcblk")) {
return std::nullopt;
}
// Populate basic device info
device.device = name.toStdString();
device.raw = name.toStdString(); // Linux uses same path for raw access
device.busType = bdev["busType"].toString().toStdString();
// Determine if virtual based on subsystems
// Pure block devices (subsystems == "block") are virtual (loop devices, etc.)
// Physical devices have additional subsystems like "block:scsi:usb:pci"
//
// Loop devices are always virtual by definition — they are kernel constructs
// backed by files. We check the device name explicitly because lsblk in
// util-linux 2.39.x (shipped in Ubuntu 24.04 LTS) has a bug where the
// "subsystems" column intermittently returns empty for loop devices, causing
// them to flicker between virtual/non-virtual.
// See: https://github.com/util-linux/util-linux/pull/3089
device.isVirtual = name.startsWith("/dev/loop") || (subsystems == "block");
// Physical USB drives and MMC cards should never be marked as virtual
if (subsystems.contains("usb") || subsystems.contains("mmc")) {
device.isVirtual = false;
}
// Parse read-only and removable flags (lsblk returns bool or "0"/"1" string)
if (bdev["ro"].isBool()) {
device.isReadOnly = bdev["ro"].toBool();
device.isRemovable = bdev["rm"].toBool() || bdev["hotplug"].toBool();
} else {
device.isReadOnly = (bdev["ro"].toString() == "1");
device.isRemovable = (bdev["rm"].toString() == "1") || (bdev["hotplug"].toString() == "1");
}
// Parse size (can be string or number depending on lsblk version)
if (bdev["size"].isString()) {
device.size = bdev["size"].toString().toULongLong();
} else {
device.size = static_cast<uint64_t>(bdev["size"].toDouble());
}
// Detect connection type from subsystems
device.isCard = subsystems.contains("mmc");
device.isUSB = subsystems.contains("usb");
device.isSCSI = subsystems.contains("scsi") && !device.isUSB;
// SD cards in internal readers report rm=0 (reader is fixed), but the
// media is removable. USB devices are always removable.
if (device.isCard || device.isUSB) {
device.isRemovable = true;
}
// System drive: non-removable and non-virtual
device.isSystem = !device.isRemovable && !device.isVirtual;
// Parse block sizes
device.blockSize = static_cast<uint32_t>(bdev["phy-sec"].toInt());
device.logicalBlockSize = static_cast<uint32_t>(bdev["log-sec"].toInt());
if (device.blockSize == 0) device.blockSize = 512;
if (device.logicalBlockSize == 0) device.logicalBlockSize = 512;
// Build description from label, vendor, model
// Pre-filter empty strings to avoid multiple removeAll("") calls
QStringList descParts;
descParts.reserve(4);
auto addIfNotEmpty = [&descParts](const QString& s) {
QString trimmed = s.trimmed();
if (!trimmed.isEmpty()) {
descParts.append(trimmed);
}
};
addIfNotEmpty(bdev["label"].toString());
addIfNotEmpty(bdev["vendor"].toString());
addIfNotEmpty(bdev["model"].toString());
// Special case for internal SD card reader
if (name == "/dev/mmcblk0" && descParts.isEmpty()) {
descParts.append(QObject::tr("Internal SD card reader"));
}
// Collect mountpoints from this device
QString mountpoint = bdev["mountpoint"].toString();
if (!mountpoint.isEmpty()) {
device.mountpoints.push_back(mountpoint.toStdString());
device.mountpointLabels.push_back(bdev["label"].toString().toStdString());
}
// Walk children (partitions) for additional mountpoints
QStringList labels;
QJsonArray children = bdev["children"].toArray();
walkStorageChildren(device, labels, children);
// Append partition labels to description
if (!labels.isEmpty()) {
descParts.append("(" + labels.join(", ") + ")");
}
// Sanitize description to prevent Unicode display attacks
// (e.g., RTL override characters that could make device names misleading)
device.description = sanitizeForDisplay(descParts.join(" ").toStdString());
// For virtual devices, check if they're backing system paths
if (device.isVirtual && !device.isSystem) {
for (const auto& mp : device.mountpoints) {
QString mpStr = QString::fromStdString(mp);
if (mpStr == "/" ||
mpStr == "/usr" ||
mpStr == "/var" ||
mpStr == "/home" ||
mpStr == "/boot" ||
mpStr.startsWith("/snap/")) {
device.isSystem = true;
break;
}
}
}
// Handle NVMe drives: mark as system by default to avoid showing internal drives
// In embedded mode (Raspberry Pi), allow unmounted NVMe drives
if (device.isSystem && subsystems.contains("nvme")) {
if (embeddedMode) {
bool hasCriticalMount = false;
for (const auto& mp : device.mountpoints) {
if (!QString::fromStdString(mp).startsWith("/media/")) {
hasCriticalMount = true;
break;
}
}
if (!hasCriticalMount) {
device.isSystem = false;
}
}
// Non-embedded mode: keep NVMe marked as system (filtered out)
}
// Check whether the current effective user can write to the device node.
// This is relevant when running with --non-root: the application is started
// directly as a regular user (no sudo, no setuid), so the real UID equals
// the effective UID and ::access() gives the correct result. Root users will
// always get isWritableByUser = true since they own all device nodes.
device.isWritableByUser = (::access(name.toLocal8Bit().constData(), W_OK) == 0);
return device;
}
/**
* @brief Execute lsblk and return JSON output
*/
std::optional<QByteArray> executeLsblk()
{
QProcess process;
// Note: We intentionally do NOT exclude loop devices (major 7) because
// mounted disk images are valid imaging targets. The isVirtual/isSystem
// flags handle filtering appropriately.
QStringList args = {
"--bytes",
"--json",
"--paths",
"--tree",
"--output", "kname,type,subsystems,ro,rm,hotplug,size,phy-sec,log-sec,label,vendor,model,mountpoint"
};
process.start("lsblk", args);
// Use 5 second timeout - embedded systems with many block devices or slow
// USB hubs may take longer than 2 seconds to enumerate
if (!process.waitForFinished(5000)) {
qWarning() << "lsblk timed out after 5000ms";
qWarning() << " state:" << process.state() << "error:" << process.error();
// Capture any partial stderr for debugging
QByteArray partialStderr = process.readAllStandardError();
if (!partialStderr.isEmpty()) {
qWarning() << " stderr:" << partialStderr;
}
process.kill();
process.waitForFinished(1000);
return std::nullopt;
}
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
qWarning() << "lsblk failed with exit code" << process.exitCode();
qWarning() << " stderr:" << process.readAllStandardError();
return std::nullopt;
}
QByteArray output = process.readAll();
if (output.isEmpty()) {
qWarning() << "lsblk returned empty output";
return std::nullopt;
}
return output;
}
} // anonymous namespace
// ============================================================================
// Public API
// ============================================================================
std::vector<DeviceDescriptor> ListStorageDevices()
{
std::vector<DeviceDescriptor> deviceList;
auto jsonOutput = executeLsblk();
if (!jsonOutput) {
// Return a sentinel device with error message so UI can display failure
// instead of showing an empty list (which looks like "no drives found")
DeviceDescriptor errorDevice;
errorDevice.device = "__error__";
errorDevice.error = "Failed to enumerate drives: lsblk command failed or timed out";
errorDevice.description = "Drive enumeration failed";
deviceList.push_back(std::move(errorDevice));
return deviceList;
}
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(*jsonOutput, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse lsblk JSON:" << parseError.errorString();
DeviceDescriptor errorDevice;
errorDevice.device = "__error__";
errorDevice.error = "Failed to enumerate drives: " + parseError.errorString().toStdString();
errorDevice.description = "Drive enumeration failed";
deviceList.push_back(std::move(errorDevice));
return deviceList;
}
const bool embeddedMode = ::isEmbeddedMode();
const QJsonArray blockDevices = doc.object().value("blockdevices").toArray();
// Reserve capacity to avoid reallocations during enumeration
deviceList.reserve(static_cast<size_t>(blockDevices.size()));
for (const auto& item : blockDevices) {
auto device = parseBlockDevice(item.toObject(), embeddedMode);
if (device) {
deviceList.push_back(std::move(*device));
}
}
return deviceList;
}
// ============================================================================
// Test API
// ============================================================================
#ifdef DRIVELIST_ENABLE_TEST_API
namespace testing {
std::vector<DeviceDescriptor> parseLinuxBlockDevices(const std::string& jsonOutput, bool embeddedMode)
{
std::vector<DeviceDescriptor> deviceList;
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(jsonOutput), &parseError);
if (parseError.error != QJsonParseError::NoError) {
return deviceList;
}
const QJsonArray blockDevices = doc.object().value("blockdevices").toArray();
// Reserve capacity to avoid reallocations during enumeration
deviceList.reserve(static_cast<size_t>(blockDevices.size()));
for (const auto& item : blockDevices) {
auto device = parseBlockDevice(item.toObject(), embeddedMode);
if (device) {
deviceList.push_back(std::move(*device));
}
}
return deviceList;
}
} // namespace testing
#endif // DRIVELIST_ENABLE_TEST_API
} // namespace Drivelist