Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Style.qml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Item {
/// the check-boxes/radio-buttons have labels that might be disabled
readonly property color formLabelColor: "black"
readonly property color formLabelErrorColor: "red"
readonly property color formLabelWarningColor: "#e8a000"
readonly property color formLabelDisabledColor: "grey"
// Active color for radio buttons, checkboxes, and switches
readonly property color formControlActiveColor: "#1955AE"
Expand Down
3 changes: 2 additions & 1 deletion src/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,15 @@ int Cli::run()
{"quiet", "Only write to console on error"},
{"log-file", "Log output to file (for debugging)", "path", ""},
{"secure-boot-key", "Path to RSA private key (PEM format) for secure boot signing", "key-file", ""},
{"non-root", "Allow running without elevated privileges"}
});

parser.addPositionalArgument("src", "Image file/URL");
parser.addPositionalArgument("dst", "Destination device");
parser.process(*_app);

// Check for elevated privileges on platforms that require them (Linux/Windows)
if (!PlatformQuirks::hasElevatedPrivileges())
if (!PlatformQuirks::hasElevatedPrivileges() && !parser.isSet("non-root"))
{
// Common error message
const char* commonMsg = "Writing to storage devices requires elevated privileges.";
Expand Down
1 change: 1 addition & 0 deletions src/drivelist/drivelist.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ struct DeviceDescriptor {
bool isSCSI = false; ///< Connected via SCSI/SAS
bool isUSB = false; ///< Connected via USB
bool isUAS = false; ///< Connected via USB Attached SCSI
bool isWritableByUser = true; ///< Current user has write access to the device node (Linux only; always true on other platforms)

// Null indicators (for optional fields)
bool busVersionNull = true;
Expand Down
8 changes: 8 additions & 0 deletions src/drivelist/drivelist_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "embedded_config.h"

#include <optional>
#include <unistd.h>
#include <QProcess>
#include <QJsonArray>
#include <QJsonDocument>
Expand Down Expand Up @@ -219,6 +220,13 @@ std::optional<DeviceDescriptor> parseBlockDevice(const QJsonObject& bdev, bool e
// 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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/drivelistitem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
#include "drivelistitem.h"

DriveListItem::DriveListItem(QString device, QString description, quint64 size, bool isUsb, bool isScsi, bool readOnly, bool isSystem, QStringList mountpoints, QStringList childDevices,
bool isRpiboot,
bool isRpiboot, bool isWritableByUser,
QObject *parent)
: QObject(parent), _device(device), _description(description), _mountpoints(mountpoints), _childDevices(childDevices), _size(size), _isUsb(isUsb), _isScsi(isScsi), _isReadOnly(readOnly), _isSystem(isSystem)
, _isRpiboot(isRpiboot)
, _isRpiboot(isRpiboot), _isWritableByUser(isWritableByUser)
{

}
Expand Down
3 changes: 3 additions & 0 deletions src/drivelistitem.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DriveListItem : public QObject
public:
explicit DriveListItem(QString device, QString description, quint64 size, bool isUsb = false, bool isScsi = false, bool readOnly = false, bool isSystem = false, QStringList mountpoints = QStringList(), QStringList childDevices = QStringList(),
bool isRpiboot = false,
bool isWritableByUser = true,
QObject *parent = nullptr);

Q_PROPERTY(QString device MEMBER _device CONSTANT)
Expand All @@ -27,6 +28,7 @@ class DriveListItem : public QObject
Q_PROPERTY(bool isReadOnly MEMBER _isReadOnly CONSTANT)
Q_PROPERTY(bool isSystem MEMBER _isSystem CONSTANT)
Q_PROPERTY(bool isRpiboot MEMBER _isRpiboot CONSTANT)
Q_PROPERTY(bool isWritableByUser MEMBER _isWritableByUser CONSTANT)
Q_INVOKABLE int sizeInGb();

signals:
Expand All @@ -44,6 +46,7 @@ public slots:
bool _isReadOnly;
bool _isSystem;
bool _isRpiboot;
bool _isWritableByUser;
};

#endif // DRIVELISTITEM_H
7 changes: 5 additions & 2 deletions src/drivelistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ DriveListModel::DriveListModel(QObject *parent)
{isSystemRole, "isSystem"},
{mountpointsRole, "mountpoints"},
{childDevicesRole, "childDevices"},
{isRpibootRole, "isRpiboot"}
{isRpibootRole, "isRpiboot"},
{isWritableByUserRole, "isWritableByUser"}
};

// Enumerate drives in separate thread, but process results in UI thread
Expand Down Expand Up @@ -102,6 +103,7 @@ void DriveListModel::processDriveList(std::vector<Drivelist::DeviceDescriptor> l
QStringList mountpoints;
QStringList childDevices;
bool isRpiboot = false;
bool isWritableByUser = true;
};
QList<NewDriveInfo> drivesToAdd;

Expand Down Expand Up @@ -174,6 +176,7 @@ void DriveListModel::processDriveList(std::vector<Drivelist::DeviceDescriptor> l
info.mountpoints = mountpoints;
info.childDevices = childDevices;
info.isRpiboot = isRpibootDevice;
info.isWritableByUser = i.isWritableByUser;
drivesToAdd.append(info);
}
}
Expand Down Expand Up @@ -221,7 +224,7 @@ void DriveListModel::processDriveList(std::vector<Drivelist::DeviceDescriptor> l
info.device, info.description, info.size,
info.isUSB, info.isScsi, info.isReadOnly, info.isSystem,
info.mountpoints, info.childDevices,
info.isRpiboot,
info.isRpiboot, info.isWritableByUser,
this);
endInsertRows();

Expand Down
2 changes: 1 addition & 1 deletion src/drivelistmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class DriveListModel : public QAbstractListModel

enum driveListRoles {
deviceRole = Qt::UserRole + 1, descriptionRole, sizeRole, isUsbRole, isScsiRole, isReadOnlyRole, isSystemRole, mountpointsRole, childDevicesRole,
isRpibootRole
isRpibootRole, isWritableByUserRole
};

signals:
Expand Down
5 changes: 3 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ int main(int argc, char *argv[])
{"disable-telemetry", "Disable telemetry (persist setting)"},
{"enable-telemetry", "Use default telemetry setting (clear override)"},
{"qml-file-dialogs", "Force use of QML file dialogs instead of native dialogs"},
{"enable-secure-boot", "Force enable secure boot customization step regardless of OS capabilities"}
{"enable-secure-boot", "Force enable secure boot customization step regardless of OS capabilities"},
{"non-root", "Allow running without elevated privileges"}
});

// Accept rpi-imager:// callback URLs as positional argument (used by callback relay on Windows)
Expand Down Expand Up @@ -752,7 +753,7 @@ int main(int argc, char *argv[])
});

// Emit permission warning signal after UI is loaded so dialog can be shown
if (hasPermissionIssue)
if (hasPermissionIssue && !parser.isSet("non-root"))
{
// Common message parts to reduce translation effort
QString header = QObject::tr("Raspberry Pi Imager requires elevated privileges to write to storage devices.");
Expand Down
97 changes: 79 additions & 18 deletions src/wizard/StorageSelectionStep.qml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ WizardStepBase {
property bool hasValidStorageOptions: false
property bool hasAnyDevices: false
property bool hasOnlyReadOnlyDevices: false
property bool hasUnwritableDevices: false
property string enumerationErrorMessage: ""

Component.onCompleted: {
Expand Down Expand Up @@ -149,7 +150,42 @@ WizardStepBase {
Accessible.role: Accessible.AlertMessage
Accessible.name: qsTr("Error: Could not list storage devices. %1").arg(root.enumerationErrorMessage)
}


// Info banner shown when some devices are not writable by the current user
Rectangle {
id: unwritableBanner
Layout.fillWidth: true
Layout.preferredHeight: visible ? unwritableBannerContent.implicitHeight + Style.spacingMedium * 2 : 0
visible: root.hasUnwritableDevices
color: Style.formLabelWarningColor
opacity: 0.9

RowLayout {
id: unwritableBannerContent
anchors.fill: parent
anchors.margins: Style.spacingMedium
spacing: Style.spacingSmall

Text {
text: "⚠"
font.pointSize: Style.fontSizeFormLabel
color: "white"
}

Text {
Layout.fillWidth: true
text: qsTr("Some storage devices cannot be written to by the current user. Run with elevated privileges (e.g. sudo) to access them.")
font.pointSize: Style.fontSizeDescription
font.family: Style.fontFamily
color: "white"
wrapMode: Text.WordWrap
}
}

Accessible.role: Accessible.AlertMessage
Accessible.name: qsTr("Warning: Some storage devices require elevated privileges. Run with sudo to access them.")
}

// Storage device list fills available space
SelectionListView {
id: dstlist
Expand Down Expand Up @@ -335,16 +371,17 @@ WizardStepBase {
required property bool isScsi
required property bool isReadOnly
required property bool isSystem
required property bool isWritableByUser
required property var mountpoints
required property QtObject modelData
property bool isRpiboot: modelData && typeof modelData.isRpiboot !== "undefined" ? modelData.isRpiboot : false

readonly property bool shouldHide: isSystem && filterSystemDrives.checked
readonly property bool unselectable: isReadOnly && !isRpiboot
readonly property bool unselectable: (isReadOnly || !isWritableByUser) && !isRpiboot

// Accessibility properties
Accessible.role: Accessible.ListItem
Accessible.name: dstitem.description + ". " + imageWriter.formatSize(parseFloat(dstitem.size)) + (dstitem.mountpoints.length > 0 ? ". " + qsTr("Mounted as %1").arg(dstitem.mountpoints.join(", ")) : "") + (dstitem.unselectable ? ". " + qsTr("Read-only") : "")
Accessible.name: dstitem.description + ". " + imageWriter.formatSize(parseFloat(dstitem.size)) + (dstitem.mountpoints.length > 0 ? ". " + qsTr("Mounted as %1").arg(dstitem.mountpoints.join(", ")) : "") + (dstitem.isReadOnly && !dstitem.isRpiboot ? ". " + qsTr("Read-only") : (!dstitem.isWritableByUser && !dstitem.isRpiboot ? ". " + qsTr("No write permission") : ""))
Accessible.focusable: true
Accessible.ignored: false

Expand Down Expand Up @@ -460,9 +497,9 @@ WizardStepBase {
}
}

// Read-only indicator
// Read-only / no-permission indicator
Text {
text: qsTr("Read-only")
text: !dstitem.isWritableByUser ? qsTr("No write permission") : qsTr("Read-only")
font.pointSize: Style.fontSizeDescription
font.family: Style.fontFamily
color: Style.formLabelErrorColor
Expand Down Expand Up @@ -524,28 +561,30 @@ WizardStepBase {
var isReadOnlyRole = 0x106
var isSystemRole = 0x107
var mountpointsRole = 0x108

var isWritableByUserRole = 0x10b

var isReadOnly = model.data(modelIndex, isReadOnlyRole)
if (isReadOnly) {
return // Can't select read-only devices
var isWritableByUser = model.data(modelIndex, isWritableByUserRole)
if (isReadOnly || isWritableByUser === false) {
return // Can't select read-only or unwritable devices
}

var isSystem = model.data(modelIndex, isSystemRole)
var device = model.data(modelIndex, deviceRole)
var description = model.data(modelIndex, descriptionRole)
var size = model.data(modelIndex, sizeRole)
var mountpoints = model.data(modelIndex, mountpointsRole) || []

// Create a mock item object with the required properties
var mockItem = {
unselectable: isReadOnly,
unselectable: isReadOnly || isWritableByUser === false,
isSystem: isSystem,
device: device,
description: description,
size: size,
mountpoints: mountpoints
}

root.selectDstItem(mockItem)
}

Expand All @@ -558,14 +597,16 @@ WizardStepBase {

var isReadOnlyRole = 0x106
var isSystemRole = 0x107

var isWritableByUserRole = 0x10b

var idx = model.index(index, 0)
var isReadOnly = model.data(idx, isReadOnlyRole)
var isWritableByUser = model.data(idx, isWritableByUserRole)
var isSystem = model.data(idx, isSystemRole)
// Item is selectable if it's not read-only and either not a system drive or filter is off

// Item is selectable if it's not read-only, writable by user, and not hidden
var shouldHide = isSystem && filterSystemDrives.checked
return !isReadOnly && !shouldHide
return !isReadOnly && isWritableByUser !== false && !shouldHide
}

// Check if there are any selectable items in the list
Expand All @@ -588,7 +629,7 @@ WizardStepBase {
var model = root.imageWriter.getDriveList()
root.hasValidStorageOptions = hasSelectableItems()
root.hasAnyDevices = model && model.rowCount() > 0

// Check if we only have read-only devices
if (root.hasAnyDevices && !root.hasValidStorageOptions) {
var isReadOnlyRole = 0x106
Expand All @@ -605,6 +646,26 @@ WizardStepBase {
} else {
root.hasOnlyReadOnlyDevices = false
}

// Check whether any visible device is unwritable by the current user
var isWritableByUserRole = 0x10b
var isSystemRole = 0x107
var foundUnwritable = false
if (root.hasAnyDevices) {
for (var j = 0; j < model.rowCount(); j++) {
var jdx = model.index(j, 0)
var isSystem = model.data(jdx, isSystemRole)
// Only consider visible (non-hidden) devices
if (isSystem && filterSystemDrives.checked)
continue
var writable = model.data(jdx, isWritableByUserRole)
if (writable === false) {
foundUnwritable = true
break
}
}
}
root.hasUnwritableDevices = foundUnwritable
}

// Get the storage status message for accessibility
Expand Down