standby unlock: a locked session unlocks by finger without PAM

Phosh's lock screen cannot take a fingerprint: its PAM conversation
answers every prompt with the password. So the daemon follows the seat
session that is locked and in front (logind LockedHint + Active), runs a
verify of its own while the display is on (backlight bl_power), and
answers a match with logind's Session.Unlock.

Only fingers named in that session owner's map unlock. The map is
re-read at every arm and verdict, and a delete cancels a running standby
verify, so a finger deleted while locked stops counting at once. A
verdict from an earlier lock or another session is dropped. no-unlock
rules and FingerMatched apply as for a client match. A client Claim
cancels standby, and its Release, or a claim that fails while pending,
re-arms it. --standby-unlock=off disables it.

Tested on the dev phone before the review fixes (map re-read,
front-session tracking, cancel ordering, pending-claim re-arm): under
Phosh, six unlocks by two fingers and an unnamed template refused; under
Plasma, kscreenlocker's pam_fprintd unlocks while standby yields and
re-arms. With the fixes: suites 8/8, and a daemon restart under a lock
screen picks the locked session up. Not exercised: standby's own
Session.Unlock on kscreenlocker, user switching.
This commit is contained in:
Jorijn van der Graaf 2026-09-23 16:56:06 +02:00
commit ac0a147e95
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 392 additions and 5 deletions

View file

@ -33,6 +33,29 @@ fprintd-verify user (wrong finger) verify-no-match, on the first press
fprintd-verify user (enrolled finger) verify-match, on the first press
```
## Lock-screen unlock
Two routes, both on by default:
- **PAM.** The daemon provides `fprintd`, so a lock screen that runs
`pam_fprintd` beside its password prompt unlocks by finger the usual way.
Plasma's does (`kde-fingerprint`; the `fingerprint-auth` and `postlogin`
services it needs are shipped here). Phosh's cannot: its PAM conversation
answers every prompt with the password, so a fingerprint module in its stack
would only stall the password behind the fingerprint timeout.
- **Standby.** While the session in front on its seat is locked and the
display is on, the daemon runs a verify of its own and answers a match with
logind's `Session.Unlock`, which Phosh honours. No verify runs while the
display is off (an open verify polls the sensor at ~5 Hz; the display state
comes from the backlight's `bl_power` or the panel's DRM `dpms`, and a
device with neither is treated as always on). Any fprintd client takes
precedence: a `Claim` cancels the standby verify, a `Release` re-arms it.
Only fingers named in that session owner's map unlock, and the map is
re-read at every arm, so a finger deleted while locked stops counting at
once. Per-finger `no-unlock` rules apply here too, and every match is
announced as `FingerMatched` for the session agent. `--standby-unlock=off`
disables it.
## Runtime dependencies
The `focal64` trustlet is proprietary and is not in this repo. It is

View file

@ -1943,7 +1943,7 @@ private:
// jobs; results and progress come back through the GLib main loop.
// =============================================================================
struct Job {
enum class Kind { Claim, Enroll, Verify, Remove } kind;
enum class Kind { Claim, Enroll, Verify, Remove, Standby } kind;
std::uint32_t uid = 0;
std::string finger;
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
@ -1955,7 +1955,7 @@ struct Job {
// so the D-Bus emission happens on the thread that owns the connection.
struct Event {
enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus,
RemoveDone } kind;
RemoveDone, StandbyStatus } kind;
bool ok = false;
bool done = false;
std::string status;
@ -1966,6 +1966,14 @@ struct Event {
void PostEvent(std::unique_ptr<Event> ev); // defined with the D-Bus code
// Standby unlock (see the block before PostEvent): "wanted" is the main
// thread's intent, read by the worker when a standby job starts and between
// its scan windows; "running" is the worker's word that a standby verify is
// the op in progress, so a cancel from the main thread can be sure whose op
// it is stopping.
std::atomic<bool> g_standbyWanted{false};
std::atomic<bool> g_standbyRunning{false};
class Worker {
public:
void Start() {
@ -2076,6 +2084,42 @@ private:
PostEvent(std::move(ev));
break;
}
case Job::Kind::Standby: {
// The daemon's own verify, no client behind it. Same scan as
// a client's, but it ends the moment the main thread stops
// wanting it (display off, session unlocked, a client
// claiming the device), and its verdict goes to the standby
// code rather than onto the bus.
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::StandbyStatus });
// `running` is published BEFORE `wanted` is read; StandbyCancel
// does the reverse. Whichever side goes second sees the other,
// so a cancel cannot fall between this check and the verify
// (cancel_ was reset at dequeue, so a CancelOp from here on holds).
g_standbyRunning.store(true);
if (!g_standbyWanted.load()) {
g_standbyRunning.store(false);
ev->status = ""; ev->done = false; // cancelled while queued
PostEvent(std::move(ev));
break;
}
auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
while (!o.decided && !o.cancelled && o.presses == 0 && g_standbyWanted.load())
o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
g_standbyRunning.store(false);
std::println("standby verify: {} over {} press(es), {} frame(s)",
o.cancelled ? "cancelled" : !o.decided ? "undecided"
: o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames);
ev->done = true;
ev->fid = o.fid;
if (o.cancelled || (!o.decided && o.presses == 0)) { ev->status = ""; ev->done = false; }
else if (!o.decided) ev->status = "verify-retry-scan";
else if (o.matched) ev->status = "verify-match";
else ev->status = "verify-no-match";
PostEvent(std::move(ev));
if (o.matched && g_learn) session_.HarvestTemplate(g_learnMaxFrames);
session_.FlushTemplate();
break;
}
case Job::Kind::Verify: {
// fprintd's contract is that a verify runs until the client
// stops it. The frame cap bounds one trustlet scan session,
@ -2283,6 +2327,9 @@ Claim g_claim;
GDBusMethodInvocation* g_pendingClaim = nullptr;
guint g_claimWatch = 0;
void StandbyEvaluate();
void StandbyCancel();
void DropClaim(const char* why) {
if (g_claimWatch) {
g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch);
@ -2292,6 +2339,7 @@ void DropClaim(const char* why) {
if (g_claim.held)
std::println("claim by {} for {} dropped: {}", g_claim.sender, g_claim.user, why);
g_claim = {};
StandbyEvaluate(); // the sensor is free again
}
// A claim is held by a bus connection. If that connection goes away -- the
@ -2318,16 +2366,16 @@ std::string MapPath(std::uint32_t uid) {
return fingerprintd::store::PathForUid(g_stateDir, uid);
}
fingerprintd::store::Map LoadMap(std::uint32_t uid) {
fingerprintd::store::Map LoadMap(std::uint32_t uid, bool quiet = false) {
std::string path = MapPath(uid);
std::ifstream f(path);
if (!f) {
std::println("map {}: {}", path, ::strerror(errno));
if (!quiet) std::println("map {}: {}", path, ::strerror(errno));
return {};
}
std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
auto m = fingerprintd::store::Map::Decode(text);
std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size());
if (!quiet) std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size());
return m;
}
@ -2390,6 +2438,302 @@ bool Authorised(GDBusMethodInvocation* inv, std::uint32_t targetUid) {
return caller && (*caller == 0 || *caller == targetUid);
}
// -----------------------------------------------------------------------------
// Standby unlock: a verify the daemon arms ITSELF while a seat session is
// locked and its display is on, answered with logind's Session.Unlock.
//
// The PAM route needs a lock screen that runs pam_fprintd beside its password
// prompt. Plasma's does; Phosh's cannot (its PAM conversation answers every
// prompt with the password, so a fingerprint module would only stall the
// password behind its timeout). Phosh honours logind's Unlock, and root may
// call it, so this needs no polkit and no desktop cooperation. It yields to
// any fprintd client at once: a Claim cancels the standby verify and the
// claim's release re-arms it.
//
// It follows ONE session: the one that is locked AND in front on its seat,
// i.e. the lock screen actually on the display. Whose finger counts is
// decided by that session's owner and a map re-read at every arm and verdict,
// so a finger deleted while locked stops counting at once.
//
// Cost: an open verify polls the trustlet at ~5 Hz, so it only runs while the
// display is on (sysfs bl_power, the DRM connector's dpms as a fallback; a
// device with neither is treated as always on). While a session is locked the
// display state itself is read at 2 Hz. --standby-unlock=off turns it all off.
bool g_standbyEnabled = true;
struct Standby {
std::string sessionPath, sessionId;
std::uint32_t uid = 0;
bool locked = false;
bool screenOn = false;
bool armed = false; // a standby job is queued or running
unsigned lockGen = 0; // bumped whenever a session starts being followed
unsigned armedGen = 0; // lockGen the outstanding job was armed under
fingerprintd::store::Map fingers;
guint pollSource = 0;
guint rearmSource = 0;
};
Standby g_standby;
std::string g_blPowerPath, g_dpmsPath;
void FindDisplayNodes() {
std::error_code ec;
for (const auto& d : std::filesystem::directory_iterator("/sys/class/backlight", ec)) {
auto p = d.path() / "bl_power";
if (std::filesystem::exists(p, ec)) { g_blPowerPath = p.string(); break; }
}
for (const auto& d : std::filesystem::directory_iterator("/sys/class/drm", ec)) {
std::string n = d.path().filename().string();
if (n.find("-DSI-") == std::string::npos && n.find("-eDP-") == std::string::npos) continue;
auto p = d.path() / "dpms";
if (std::filesystem::exists(p, ec)) { g_dpmsPath = p.string(); break; }
}
}
bool ReadTrimmed(const std::string& path, std::string& out) {
std::ifstream f(path);
if (!f) return false;
std::getline(f, out);
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) out.pop_back();
return true;
}
// bl_power: 0 = FB_BLANK_UNBLANK, anything else is some flavour of off.
bool ScreenOn() {
std::string v;
if (!g_blPowerPath.empty() && ReadTrimmed(g_blPowerPath, v)) return v == "0";
if (!g_dpmsPath.empty() && ReadTrimmed(g_dpmsPath, v)) return v == "On";
return true; // nothing to read: better armed than deaf
}
void StandbyCancel() {
g_standbyWanted.store(false);
if (g_standbyRunning.load() && g_worker) g_worker->CancelOp();
}
void StandbyEvaluate() {
bool want = g_ready && g_standbyEnabled && g_worker && g_standby.locked && g_standby.screenOn
&& !g_claim.held && !g_pendingClaim;
if (want && !g_standby.armed) {
g_standby.fingers = LoadMap(g_standby.uid, /*quiet=*/true);
if (g_standby.fingers.Size() == 0) return;
std::vector<std::uint32_t> fids;
for (const auto& e : g_standby.fingers.Entries()) fids.push_back(e.fid);
g_standby.armed = true;
g_standby.armedGen = g_standby.lockGen;
g_standbyWanted.store(true);
std::println("standby: listening for a finger (session {}, uid {}, {} finger(s))",
g_standby.sessionId, g_standby.uid, fids.size());
g_worker->Post(Job{ .kind = Job::Kind::Standby, .uid = g_standby.uid,
.acceptFids = std::move(fids) });
} else if (!want && g_standby.armed) {
StandbyCancel(); // `armed` clears when the job reports back
}
}
void StandbyScheduleEvaluate(unsigned ms) {
if (g_standby.rearmSource) g_source_remove(g_standby.rearmSource);
g_standby.rearmSource = g_timeout_add(ms, [](gpointer) -> gboolean {
g_standby.rearmSource = 0;
StandbyEvaluate();
return G_SOURCE_REMOVE;
}, nullptr);
}
gboolean StandbyPoll(gpointer) {
bool on = ScreenOn();
if (on != g_standby.screenOn) {
g_standby.screenOn = on;
std::println("standby: display {}", on ? "on" : "off");
StandbyEvaluate();
}
return G_SOURCE_CONTINUE;
}
void StandbyOnStatus(const Event& ev) {
namespace ac = fingerprintd::actions;
namespace store = fingerprintd::store;
g_standby.armed = false;
if (ev.status.empty()) { // cancelled, or skipped while queued
StandbyEvaluate();
return;
}
if (!g_standby.locked) return;
if (g_standby.armedGen != g_standby.lockGen) { // armed for a lock we have since left
StandbyEvaluate();
return;
}
if (ev.status == "verify-match" && ev.fid != 0) {
g_standby.fingers = LoadMap(g_standby.uid, /*quiet=*/true);
auto matched = g_standby.fingers.Lookup(ev.fid);
const ac::Rule* rule = matched ? ac::Find(g_actions, *matched) : nullptr;
if (!matched) {
std::println("standby: matched fid {} that no finger name maps to; not unlocking", ev.fid);
} else if (rule && !rule->unlocks) {
std::println("standby: {} is configured no-unlock; not unlocking", store::NameOf(*matched));
} else {
std::println("standby: {} matched -- unlocking session {}",
store::NameOf(*matched), g_standby.sessionId);
g_dbus_connection_call(
g_conn, "org.freedesktop.login1", g_standby.sessionPath.c_str(),
"org.freedesktop.login1.Session", "Unlock", nullptr, nullptr,
G_DBUS_CALL_FLAGS_NONE, 5000, nullptr,
[](GObject* src, GAsyncResult* res, gpointer) {
GError* err = nullptr;
GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err);
if (!r) {
std::println(std::cerr, "standby: Unlock failed: {}", err ? err->message : "?");
if (err) g_error_free(err);
} else {
g_variant_unref(r);
}
}, nullptr);
}
// The same announcement a client-driven match makes (see VerifyStatus).
if (matched && g_conn)
g_dbus_connection_emit_signal(
g_conn, nullptr, DevicePath, ActionIface, "FingerMatched",
g_variant_new("(su)", std::string(store::NameOf(*matched)).c_str(),
static_cast<guint32>(g_standby.uid)),
nullptr);
if (rule && !rule->command.empty()) {
std::println("action: {} -> running {}", store::NameOf(*matched), rule->command);
RunSystemAction(rule->command, std::string(store::NameOf(*matched)));
}
// LockedHint normally flips first and disarms us; if the unlock did
// not take, this listens again.
StandbyScheduleEvaluate(1500);
return;
}
std::println("standby: {} -- listening again", ev.status);
StandbyScheduleEvaluate(300);
}
struct SessionInfo { std::string id, seat; std::uint32_t uid = 0; bool locked = false, active = false; };
std::optional<SessionInfo> QuerySession(const std::string& path) {
GError* err = nullptr;
GVariant* r = g_dbus_connection_call_sync(
g_conn, "org.freedesktop.login1", path.c_str(), "org.freedesktop.DBus.Properties", "GetAll",
g_variant_new("(s)", "org.freedesktop.login1.Session"), G_VARIANT_TYPE("(a{sv})"),
G_DBUS_CALL_FLAGS_NONE, 3000, nullptr, &err);
if (!r) {
std::println(std::cerr, "standby: cannot read session {}: {}", path, err ? err->message : "?");
if (err) g_error_free(err);
return std::nullopt;
}
SessionInfo si;
GVariant* dict = g_variant_get_child_value(r, 0);
const gchar* id = nullptr;
if (g_variant_lookup(dict, "Id", "&s", &id) && id) si.id = id;
guint32 uid = 0; const gchar* upath = nullptr;
if (g_variant_lookup(dict, "User", "(u&o)", &uid, &upath)) si.uid = uid;
const gchar* seat = nullptr; const gchar* spath = nullptr;
if (g_variant_lookup(dict, "Seat", "(&s&o)", &seat, &spath) && seat) si.seat = seat;
gboolean locked = FALSE, active = FALSE;
if (g_variant_lookup(dict, "LockedHint", "b", &locked)) si.locked = locked;
if (g_variant_lookup(dict, "Active", "b", &active)) si.active = active;
g_variant_unref(dict);
g_variant_unref(r);
return si;
}
// Re-reads logind and follows the seat session that is locked AND active, or
// none. Called on every LockedHint/Active change and session removal, so a
// lock, an unlock and a user switch all land here.
void StandbyRescan() {
GError* err = nullptr;
GVariant* r = g_dbus_connection_call_sync(
g_conn, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
"ListSessions", nullptr, G_VARIANT_TYPE("(a(susso))"), G_DBUS_CALL_FLAGS_NONE, 3000, nullptr, &err);
if (!r) {
std::println(std::cerr, "standby: ListSessions failed: {}", err ? err->message : "?");
if (err) g_error_free(err);
return;
}
std::string frontPath;
SessionInfo front;
GVariant* arr = g_variant_get_child_value(r, 0);
GVariantIter it;
g_variant_iter_init(&it, arr);
const gchar *id, *user, *seat, *path; guint32 uid;
while (g_variant_iter_next(&it, "(&su&s&s&o)", &id, &uid, &user, &seat, &path)) {
if (!seat || !*seat) continue; // not a seat session: nothing to unlock on
auto si = QuerySession(path);
if (si && si->locked && si->active && !si->seat.empty()) { frontPath = path; front = *si; break; }
}
g_variant_unref(arr);
g_variant_unref(r);
if (g_standby.locked && g_standby.sessionPath == frontPath) return; // unchanged
if (g_standby.locked) {
std::println("standby: session {} unlocked or no longer in front", g_standby.sessionId);
g_standby.locked = false;
if (g_standby.pollSource) { g_source_remove(g_standby.pollSource); g_standby.pollSource = 0; }
if (g_standby.armed) StandbyCancel(); // armed for the session just left
}
if (!frontPath.empty()) {
g_standby.sessionPath = frontPath;
g_standby.sessionId = front.id;
g_standby.uid = front.uid;
g_standby.lockGen++;
g_standby.fingers = LoadMap(front.uid);
g_standby.locked = true;
g_standby.screenOn = ScreenOn();
if (!g_standby.pollSource) g_standby.pollSource = g_timeout_add(500, StandbyPoll, nullptr);
std::println("standby: session {} (uid {}) locked; display {}; {} finger(s) enrolled",
front.id, front.uid, g_standby.screenOn ? "on" : "off", g_standby.fingers.Size());
}
StandbyEvaluate();
}
bool PropertyTouched(GVariant* changed, GVariant* invalidated, std::string_view key) {
if (GVariant* v = g_variant_lookup_value(changed, std::string(key).c_str(), nullptr)) {
g_variant_unref(v);
return true;
}
GVariantIter it;
const gchar* name = nullptr;
g_variant_iter_init(&it, invalidated);
while (g_variant_iter_next(&it, "&s", &name))
if (name && key == name) return true;
return false;
}
void OnLogindProperties(GDBusConnection*, const gchar*, const gchar* path, const gchar*, const gchar*,
GVariant* params, gpointer) {
if (!path || std::string_view(path).find("/org/freedesktop/login1/session/") != 0) return;
const gchar* iface = nullptr;
GVariant* changed = nullptr;
GVariant* invalidated = nullptr;
g_variant_get(params, "(&s@a{sv}@as)", &iface, &changed, &invalidated);
if (iface && std::string_view(iface) == "org.freedesktop.login1.Session"
&& (PropertyTouched(changed, invalidated, "LockedHint")
|| PropertyTouched(changed, invalidated, "Active")))
StandbyRescan();
g_variant_unref(changed);
g_variant_unref(invalidated);
}
void OnSessionRemoved(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*,
GVariant*, gpointer) {
StandbyRescan();
}
void StandbyInit() {
if (!g_standbyEnabled) { std::println("standby unlock: off"); return; }
FindDisplayNodes();
std::println("standby unlock: on (display state from {})",
!g_blPowerPath.empty() ? g_blPowerPath : !g_dpmsPath.empty() ? g_dpmsPath : "nothing -- assumed on");
g_dbus_connection_signal_subscribe(g_conn, "org.freedesktop.login1", "org.freedesktop.DBus.Properties",
"PropertiesChanged", nullptr, nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
OnLogindProperties, nullptr, nullptr);
g_dbus_connection_signal_subscribe(g_conn, "org.freedesktop.login1", "org.freedesktop.login1.Manager",
"SessionRemoved", "/org/freedesktop/login1", nullptr,
G_DBUS_SIGNAL_FLAGS_NONE, OnSessionRemoved, nullptr, nullptr);
StandbyRescan(); // a session locked before we started (a daemon restart under a lock screen)
}
void PostEvent(std::unique_ptr<Event> ev) {
g_idle_add([](gpointer data) -> gboolean {
std::unique_ptr<Event> ev(static_cast<Event*>(data));
@ -2397,6 +2741,10 @@ void PostEvent(std::unique_ptr<Event> ev) {
case Event::Kind::Ready:
g_ready = true;
std::println("fingerprintd: ready");
StandbyEvaluate();
break;
case Event::Kind::StandbyStatus:
StandbyOnStatus(*ev);
break;
case Event::Kind::StartFailed:
std::println(std::cerr, "fingerprintd: session bring-up failed -- exiting for systemd");
@ -2413,6 +2761,9 @@ void PostEvent(std::unique_ptr<Event> ev) {
ReturnError(ev->invocation, "Internal", "could not select the user's group");
}
g_pendingClaim = nullptr;
// A claim dropped while it was pending (the claimant left, or
// the group failed) evaluated standby too early to arm it.
StandbyEvaluate();
}
break;
case Event::Kind::RemoveDone:
@ -2542,6 +2893,7 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
g_claim.fingers = LoadMap(g_claim.uid);
g_pendingClaim = inv;
WatchClaimant(g_claim.sender);
StandbyCancel(); // the client's op comes first; standby returns with Release
g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv });
return;
}
@ -2603,6 +2955,13 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
if (one) m.Remove(*one); else m.Clear();
SaveMap(uid, m);
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
// A standby verify for this user holds the fids it was armed with and
// keeps the worker busy; stop it so the removal below runs, and so it
// re-arms from the new map. The verdict check re-reads the map anyway.
if (g_standby.locked && g_standby.uid == uid) {
g_standby.fingers = m;
StandbyCancel();
}
// The name is dropped first and the template second, in that order on
// purpose: a finger whose template survives a failed removal is a slot
// leak, while a name that survives a successful one would keep
@ -2731,6 +3090,7 @@ void OnBusAcquired(GDBusConnection* conn, const gchar*, gpointer) {
g_dbus_connection_register_object(conn, DevicePath, node->interfaces[1], &g_vtable, nullptr, nullptr, nullptr);
g_dbus_node_info_unref(node);
std::println("objects registered at {} and {}", ManagerPath, DevicePath);
StandbyInit();
}
gboolean OnTerm(gpointer) {
@ -2952,6 +3312,8 @@ int main(int argc, char** argv) {
// The observer thread and the loop would both drain the same fd, so
// the diagnostic and the wake are mutually exclusive.
if (a == "--edge-wake") g_edgeWake = true;
if (a == "--standby-unlock=off") g_standbyEnabled = false;
if (a == "--standby-unlock=on") g_standbyEnabled = true;
if (a.starts_with("--log-dir=")) { g_logDir = a.substr(10); g_logDirExplicit = true; }
if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12);
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
@ -2985,6 +3347,8 @@ int main(int argc, char** argv) {
" template, as stock does (default on, 8)\n"
" --actions=FILE per-finger actions (default\n"
" /etc/fingerprintd/actions.conf, absent = off)\n"
" --standby-unlock=on|off unlock the locked session in front by finger,\n"
" via logind (default on)\n"
" --sfs-root=DIR --sfs-writable --rpmb-write storage policy",
Version);
return 1;