1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // Copyright (C) 2014 Ivan Komissarov <ABBAPOH@gmail.com> |
3 | // Copyright (C) 2016 Intel Corporation. |
4 | // Copyright (C) 2023 Ahmad Samir <a.samirh78@gmail.com> |
5 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
6 | |
7 | #include "qstorageinfo_linux_p.h" |
8 | |
9 | #include "qdirlisting.h" |
10 | #include <private/qcore_unix_p.h> |
11 | #include <private/qtools_p.h> |
12 | |
13 | #include <q20memory.h> |
14 | |
15 | #include <sys/ioctl.h> |
16 | #include <sys/stat.h> |
17 | #include <sys/statfs.h> |
18 | |
19 | // so we don't have to #include <linux/fs.h>, which is known to cause conflicts |
20 | #ifndef FSLABEL_MAX |
21 | # define FSLABEL_MAX 256 |
22 | #endif |
23 | #ifndef FS_IOC_GETFSLABEL |
24 | # define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX]) |
25 | #endif |
26 | |
27 | // or <linux/statfs.h> |
28 | #ifndef ST_RDONLY |
29 | # define ST_RDONLY 0x0001 /* mount read-only */ |
30 | #endif |
31 | |
32 | #if defined(Q_OS_ANDROID) |
33 | // statx() is disabled on Android because quite a few systems |
34 | // come with sandboxes that kill applications that make system calls outside a |
35 | // whitelist and several Android vendors can't be bothered to update the list. |
36 | # undef STATX_BASIC_STATS |
37 | #endif |
38 | |
39 | QT_BEGIN_NAMESPACE |
40 | |
41 | using namespace Qt::StringLiterals; |
42 | |
43 | namespace { |
44 | struct AutoFileDescriptor |
45 | { |
46 | int fd = -1; |
47 | AutoFileDescriptor(const QString &path, int mode = QT_OPEN_RDONLY) |
48 | : fd(qt_safe_open(pathname: QFile::encodeName(fileName: path), flags: mode)) |
49 | {} |
50 | ~AutoFileDescriptor() { if (fd >= 0) qt_safe_close(fd); } |
51 | operator int() const noexcept { return fd; } |
52 | }; |
53 | } |
54 | |
55 | // udev encodes the labels with ID_LABEL_FS_ENC which is done with |
56 | // blkid_encode_string(). Within this function some 1-byte utf-8 |
57 | // characters not considered safe (e.g. '\' or ' ') are encoded as hex |
58 | static QString decodeFsEncString(QString &&str) |
59 | { |
60 | using namespace QtMiscUtils; |
61 | qsizetype start = str.indexOf(c: u'\\'); |
62 | if (start < 0) |
63 | return std::move(str); |
64 | |
65 | // decode in-place |
66 | QString decoded = std::move(str); |
67 | auto ptr = reinterpret_cast<char16_t *>(decoded.begin()); |
68 | qsizetype in = start; |
69 | qsizetype out = start; |
70 | qsizetype size = decoded.size(); |
71 | |
72 | while (in < size) { |
73 | Q_ASSERT(ptr[in] == u'\\'); |
74 | if (size - in >= 4 && ptr[in + 1] == u'x') { // we need four characters: \xAB |
75 | int c = fromHex(c: ptr[in + 2]) << 4; |
76 | c |= fromHex(c: ptr[in + 3]); |
77 | if (Q_UNLIKELY(c < 0)) |
78 | c = QChar::ReplacementCharacter; // bad hex sequence |
79 | ptr[out++] = c; |
80 | in += 4; |
81 | } |
82 | |
83 | for ( ; in < size; ++in) { |
84 | char16_t c = ptr[in]; |
85 | if (c == u'\\') |
86 | break; |
87 | ptr[out++] = c; |
88 | } |
89 | } |
90 | decoded.resize(size: out); |
91 | return decoded; |
92 | } |
93 | |
94 | static inline dev_t deviceIdForPath(const QString &device) |
95 | { |
96 | QT_STATBUF st; |
97 | if (QT_STAT(file: QFile::encodeName(fileName: device), buf: &st) < 0) |
98 | return 0; |
99 | return st.st_dev; |
100 | } |
101 | |
102 | static inline quint64 mountIdForPath(int fd) |
103 | { |
104 | if (fd < 0) |
105 | return 0; |
106 | #if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID) |
107 | // STATX_MNT_ID was added in kernel v5.8 |
108 | struct statx st; |
109 | int r = statx(dirfd: fd, path: "" , AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, buf: &st); |
110 | if (r == 0 && (st.stx_mask & STATX_MNT_ID)) |
111 | return st.stx_mnt_id; |
112 | #endif |
113 | return 0; |
114 | } |
115 | |
116 | static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0) |
117 | { |
118 | // major = 0 implies an anonymous block device, so we need to stat() the |
119 | // actual device to get its dev_t. This is required for btrfs (and possibly |
120 | // others), which always uses them for all the subvolumes (including the |
121 | // root): |
122 | // https://codebrowser.dev/linux/linux/fs/btrfs/disk-io.c.html#btrfs_init_fs_root |
123 | // https://codebrowser.dev/linux/linux/fs/super.c.html#get_anon_bdev |
124 | // For everything else, we trust the parameter. |
125 | if (major(deviceId) != 0) |
126 | return deviceId; |
127 | |
128 | // don't even try to stat() a relative path or "/" |
129 | if (device.size() < 2 || !device.startsWith(c: '/')) |
130 | return 0; |
131 | |
132 | QT_STATBUF st; |
133 | if (QT_STAT(file: device, buf: &st) < 0) |
134 | return 0; |
135 | if (!S_ISBLK(st.st_mode)) |
136 | return 0; |
137 | return st.st_rdev; |
138 | } |
139 | |
140 | static QDirListing devicesByLabel() |
141 | { |
142 | static const char pathDiskByLabel[] = "/dev/disk/by-label" ; |
143 | static constexpr auto LabelFileFilter = QDirListing::IteratorFlag::IncludeHidden; |
144 | return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter); |
145 | } |
146 | |
147 | static inline auto retrieveLabels() |
148 | { |
149 | struct Entry { |
150 | QString label; |
151 | quint64 deviceId; |
152 | }; |
153 | QList<Entry> result; |
154 | |
155 | for (const auto &dirEntry : devicesByLabel()) { |
156 | quint64 deviceId = retrieveDeviceId(device: QFile::encodeName(fileName: dirEntry.filePath())); |
157 | if (!deviceId) |
158 | continue; |
159 | result.emplaceBack(args: Entry{ .label: decodeFsEncString(str: dirEntry.fileName()), .deviceId: deviceId }); |
160 | } |
161 | return result; |
162 | } |
163 | |
164 | static std::optional<QString> retrieveLabelViaIoctl(int fd) |
165 | { |
166 | // FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific. |
167 | if (fd < 0) |
168 | return std::nullopt; |
169 | |
170 | // Note: it doesn't append the null terminator (despite what the man page |
171 | // says) and the return code on success (0) does not indicate the length. |
172 | char label[FSLABEL_MAX] = {}; |
173 | int r = ioctl(fd: fd, FS_IOC_GETFSLABEL, &label); |
174 | if (r < 0) |
175 | return std::nullopt; |
176 | return QString::fromUtf8(utf8: label); |
177 | } |
178 | |
179 | static inline QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId) |
180 | { |
181 | if (auto label = retrieveLabelViaIoctl(fd)) |
182 | return *label; |
183 | |
184 | deviceId = retrieveDeviceId(device: d.device, deviceId); |
185 | if (!deviceId) |
186 | return QString(); |
187 | |
188 | for (const auto &dirEntry : devicesByLabel()) { |
189 | if (retrieveDeviceId(device: QFile::encodeName(fileName: dirEntry.filePath())) == deviceId) |
190 | return decodeFsEncString(str: dirEntry.fileName()); |
191 | } |
192 | return QString(); |
193 | } |
194 | |
195 | void QStorageInfoPrivate::retrieveVolumeInfo() |
196 | { |
197 | struct statfs64 statfs_buf; |
198 | int result; |
199 | QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf)); |
200 | valid = ready = (result == 0); |
201 | if (valid) { |
202 | bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize; |
203 | bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize; |
204 | bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize; |
205 | blockSize = int(statfs_buf.f_bsize); |
206 | readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0; |
207 | } |
208 | } |
209 | |
210 | static std::vector<MountInfo> parseMountInfo(FilterMountInfo filter = FilterMountInfo::All) |
211 | { |
212 | QFile file(u"/proc/self/mountinfo"_s ); |
213 | if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) |
214 | return {}; |
215 | |
216 | QByteArray mountinfo = file.readAll(); |
217 | file.close(); |
218 | |
219 | return doParseMountInfo(mountinfo, filter); |
220 | } |
221 | |
222 | void QStorageInfoPrivate::doStat() |
223 | { |
224 | retrieveVolumeInfo(); |
225 | if (!ready) |
226 | return; |
227 | |
228 | rootPath = QFileInfo(rootPath).canonicalFilePath(); |
229 | if (rootPath.isEmpty()) |
230 | return; |
231 | |
232 | std::vector<MountInfo> infos = parseMountInfo(); |
233 | if (infos.empty()) { |
234 | rootPath = u'/'; |
235 | return; |
236 | } |
237 | |
238 | MountInfo *best = nullptr; |
239 | AutoFileDescriptor fd(rootPath); |
240 | if (quint64 mntid = mountIdForPath(fd)) { |
241 | // We have the mount ID for this path, so find the matching line. |
242 | auto it = std::find_if(first: infos.begin(), last: infos.end(), |
243 | pred: [mntid](const MountInfo &info) { return info.mntid == mntid; }); |
244 | if (it != infos.end()) |
245 | best = q20::to_address(ptr: it); |
246 | } else { |
247 | // We have failed to get the mount ID for this path, usually because |
248 | // the path cannot be open()ed by this user (e.g., /root), so we fall |
249 | // back to a string search. |
250 | // We iterate over the /proc/self/mountinfo list backwards because then any |
251 | // matching isParentOf must be the actual mount point because it's the most |
252 | // recent mount on that path. Linux does allow mounting over non-empty |
253 | // directories, such as in: |
254 | // # mount | tail -2 |
255 | // tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64) |
256 | // tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64) |
257 | // |
258 | // We try to match the device ID in case there's a mount --move. |
259 | // We can't *rely* on it because some filesystems like btrfs will assign |
260 | // device IDs to subvolumes that aren't listed in /proc/self/mountinfo. |
261 | |
262 | const QString oldRootPath = std::exchange(obj&: rootPath, new_val: QString()); |
263 | const dev_t rootPathDevId = deviceIdForPath(device: oldRootPath); |
264 | for (auto it = infos.rbegin(); it != infos.rend(); ++it) { |
265 | if (!isParentOf(parent: it->mountPoint, dirName: oldRootPath)) |
266 | continue; |
267 | if (rootPathDevId == it->stDev) { |
268 | // device ID matches; this is definitely the best option |
269 | best = q20::to_address(ptr: it); |
270 | break; |
271 | } |
272 | if (!best) { |
273 | // if we can't find a device ID match, this parent path is probably |
274 | // the correct one |
275 | best = q20::to_address(ptr: it); |
276 | } |
277 | } |
278 | } |
279 | if (best) { |
280 | auto stDev = best->stDev; |
281 | setFromMountInfo(std::move(*best)); |
282 | name = retrieveLabel(d: *this, fd, deviceId: stDev); |
283 | } |
284 | } |
285 | |
286 | QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes() |
287 | { |
288 | std::vector<MountInfo> infos = parseMountInfo(filter: FilterMountInfo::Filtered); |
289 | if (infos.empty()) |
290 | return QList{root()}; |
291 | |
292 | std::optional<decltype(retrieveLabels())> labelMap; |
293 | auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, int fd, quint64 devid) { |
294 | if (d.fileSystemType == "tmpfs" ) |
295 | return QString(); |
296 | |
297 | if (auto label = retrieveLabelViaIoctl(fd)) |
298 | return *label; |
299 | |
300 | devid = retrieveDeviceId(device: d.device, deviceId: devid); |
301 | if (!devid) |
302 | return QString(); |
303 | |
304 | if (!labelMap) |
305 | labelMap = retrieveLabels(); |
306 | for (auto &[deviceLabel, deviceId] : std::as_const(t&: *labelMap)) { |
307 | if (devid == deviceId) |
308 | return deviceLabel; |
309 | } |
310 | return QString(); |
311 | }; |
312 | |
313 | QList<QStorageInfo> volumes; |
314 | volumes.reserve(asize: infos.size()); |
315 | for (auto it = infos.begin(); it != infos.end(); ++it) { |
316 | MountInfo &info = *it; |
317 | AutoFileDescriptor fd(info.mountPoint); |
318 | |
319 | // find out if the path as we see it matches this line from mountinfo |
320 | quint64 mntid = mountIdForPath(fd); |
321 | if (mntid == 0) { |
322 | // statx failed, so scan the later lines to see if any is a parent |
323 | // to this |
324 | auto isParent = [&info](const MountInfo &maybeParent) { |
325 | return isParentOf(parent: maybeParent.mountPoint, dirName: info.mountPoint); |
326 | }; |
327 | if (std::find_if(first: it + 1, last: infos.end(), pred: isParent) != infos.end()) |
328 | continue; |
329 | } else if (mntid != info.mntid) { |
330 | continue; |
331 | } |
332 | |
333 | const auto infoStDev = info.stDev; |
334 | QStorageInfoPrivate d(std::move(info)); |
335 | d.retrieveVolumeInfo(); |
336 | if (d.bytesTotal <= 0 && d.rootPath != u'/') |
337 | continue; |
338 | d.name = labelForDevice(d, fd, infoStDev); |
339 | volumes.emplace_back(args: QStorageInfo(*new QStorageInfoPrivate(std::move(d)))); |
340 | } |
341 | return volumes; |
342 | } |
343 | |
344 | QT_END_NAMESPACE |
345 | |