What changed, and why it matters
This commit adds Linux support to a Monero wallet app. Most changes are ordinary cross-platform plumbing, but two items deserve a closer look: a new helper script edits a binary shared library to clear an 'executable stack' flag, and the app now stores wallet files, Tor data, and logs in the user's home directory under a hidden folder on Linux. The commit itself does not describe these as security fixes, and there is no external advisory or researcher attribution supplied.
Treat this as a platform-enablement change rather than a confirmed security patch. Reviewers should verify the provenance and hash of linux/monero_libwallet2_api_c.so, confirm fix_execstack.sh is run at build time, and audit that $HOME/.skylight_wallet is created with restrictive permissions (e.g., 0700) since it will hold wallet files, Tor state, and logs. Consider adding a follow-up commit to set directory permissions explicitly.
Security signals we found
New ELF binary added to repository (linux/monero_libwallet2_api_c.so)
New script modifies executable stack flag in shipped shared library
Wallet/Tor/log directory moved to hidden home directory on Linux
Desktop notification logic added for incoming transactions
Mobile-only background task and permission flows gated by Platform checks
Evidence from the diff
The patch enables Linux builds and adjusts platform-specific behavior. Notable changes: (1) linux/fix_execstack.sh is added to parse the ELF program header of linux/monero_libwallet2_api_c.so and clear the PF_X bit on the PT_GNU_STACK segment, which is a defensive hardening step. (2) lib/util/dirs.dart introduces getAppDir()/createAppDir(), which on Linux returns $HOME/.skylight_wallet and is used for wallet storage, Tor data, and logs. (3) Notifications are wired up for Linux/Windows desktops. (4) Several mobile-only features (QR scan, brightness max, background Workmanager tasks, notification toggle) are gated to Android/iOS. (5) A prebuilt binary monero_libwallet2_api_c.so is added. No direct vulnerability is visible in the diff, but shipping a prebuilt native library and modifying its ELF headers in-tree is a supply-chain/hardening concern rather than an obvious exploit.
Changed components
lib/util/dirs.dartlib/util/wallet.dartlib/services/tor_service.dartlib/util/logging.dartlib/services/notifications_service.dartlib/models/wallet_model.dartlinux/fix_execstack.shlinux/monero_libwallet2_api_c.solinux/CMakeLists.txtInspect captured patch +225 / −69
diff --git a/lib/main.dart b/lib/main.dart
index f6980be..5846366 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,9 +3,11 @@ import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/screens/privacy_policy.dart';
import 'package:skylight_wallet/screens/terms_of_service.dart';
import 'package:skylight_wallet/screens/unlock.dart';
+import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:skylight_wallet/util/dirs.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:skylight_wallet/models/fiat_rate_model.dart';
@@ -37,8 +39,11 @@ import 'package:skylight_wallet/util/logging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
timeago.setLocaleMessages('pt', timeago.PtBrMessages());
+ await createAppDir();
TorService.sharedInstance.start();
- registerPeriodicTasks();
+ await registerPeriodicTasks();
+ await NotificationService().init();
+
cleanOldLogFiles();
runApp(MyApp());
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 36180c7..b96765a 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -8,6 +8,7 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
+import 'package:dart_date/dart_date.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:monero/monero.dart' as monero;
@@ -15,7 +16,7 @@ import 'package:monero/src/monero.dart';
import 'package:monero/src/wallet2.dart';
import 'package:http/http.dart' as http;
-import 'package:skylight_wallet/consts.dart';
+import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/util/formatting.dart';
@@ -24,6 +25,7 @@ import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/socks_http.dart';
import 'package:skylight_wallet/util/wallet.dart';
import 'package:skylight_wallet/util/wallet_password.dart';
+import 'package:skylight_wallet/consts.dart' as consts;
String generateHexString(int length) {
final Random random = Random.secure();
@@ -132,7 +134,6 @@ class WalletModel with ChangeNotifier {
.getLWSFWalletManager();
Wallet2Wallet? _w2Wallet;
-
Wallet2TransactionHistory? _w2TxHistory;
late String _connectionAddress;
@@ -140,6 +141,7 @@ class WalletModel with ChangeNotifier {
late bool _connectionUseTor;
late bool _connectionUseSsl;
+ final _sessionStartedAt = DateTime.now().secondsSinceEpoch;
var _hasAttemptedConnection = false;
var _isConnected = false;
var _isSynced = false;
@@ -262,8 +264,28 @@ class WalletModel with ChangeNotifier {
}
if (txCount > _txHistory.length || hasPendingTx) {
+ final txCountDiff = txCount - _txHistory.length;
+
_txHistory = await _getFullTxHistory();
+ // Notify new transactions on desktop
+ if ((Platform.isLinux || Platform.isWindows) &&
+ _isConnected &&
+ _isSynced &&
+ _syncedHeight is int &&
+ _syncedHeight! > 0) {
+ for (int i = 0; i < txCountDiff; i++) {
+ final tx = _txHistory[i];
+
+ if (tx.direction == consts.txDirectionIncoming &&
+ tx.timestamp > _sessionStartedAt) {
+ NotificationService().showIncomingTxNotification(tx.amount);
+ // Only notify one new transaction
+ break;
+ }
+ }
+ }
+
if (persistCount) {
await persistTxHistoryCount();
}
@@ -1103,7 +1125,7 @@ class WalletModel with ChangeNotifier {
final TxDetails txDetails = TxDetails(
index: null,
- direction: txDirectionOutgoing,
+ direction: consts.txDirectionOutgoing,
hash: tx.txid(''),
amount: doubleAmountFromInt(tx.amount()),
fee: doubleAmountFromInt(tx.fee()),
diff --git a/lib/periodic_tasks.dart b/lib/periodic_tasks.dart
index 7fc808b..42a75b4 100644
--- a/lib/periodic_tasks.dart
+++ b/lib/periodic_tasks.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
@@ -87,46 +89,51 @@ void _callbackDispatcher() {
});
}
-Future<void> registerTxNotifierTaskIfEnabled() async {
- final notificationsIsAllowed = await NotificationService().promptPermission();
-
- if (!notificationsIsAllowed) {
- await SharedPreferencesService.set<bool>(
- SharedPreferencesKeys.notificationsEnabled,
- false,
- );
- return;
- }
-
+Future<void> registerTxNotifierTaskIfAllowed() async {
final notificationsEnabled =
await SharedPreferencesService.get<bool>(
SharedPreferencesKeys.notificationsEnabled,
) ??
false;
- if (notificationsEnabled) {
- // This will replace an existing task, so we can prevent code from eg an old
- // release from remaining forever.
- await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
- await Workmanager().registerPeriodicTask(
- "New transactions check",
- PeriodicTasks.txNotifier,
- frequency: Duration(minutes: 15),
- constraints: Constraints(
- networkType: NetworkType.connected,
- requiresBatteryNotLow: true,
- ),
+ if (!notificationsEnabled) {
+ return;
+ }
+
+ final notificationsAreAllowed = await NotificationService()
+ .promptPermission();
+
+ if (!notificationsAreAllowed) {
+ await SharedPreferencesService.set<bool>(
+ SharedPreferencesKeys.notificationsEnabled,
+ false,
);
+ return;
}
+
+ // Cancelling will replace an existing task, so we can prevent code from an
+ // old release from remaining forever.
+ await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
+ await Workmanager().registerPeriodicTask(
+ "New transactions check",
+ PeriodicTasks.txNotifier,
+ frequency: Duration(minutes: 15),
+ constraints: Constraints(
+ networkType: NetworkType.connected,
+ requiresBatteryNotLow: true,
+ ),
+ );
}
-Future<void> unregisterTxNotifierTask() async {
+Future<void> unregisterPeriodicTasks() async {
await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
}
Future<void> registerPeriodicTasks() async {
- await NotificationService().init();
- Workmanager().initialize(_callbackDispatcher, isInDebugMode: true);
+ if (!(Platform.isAndroid || Platform.isIOS)) {
+ return;
+ }
- await registerTxNotifierTaskIfEnabled();
+ Workmanager().initialize(_callbackDispatcher, isInDebugMode: true);
+ await registerTxNotifierTaskIfAllowed();
}
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index ca34dfc..02a3ff5 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
@@ -22,7 +24,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
void initState() {
super.initState();
- _setBrightnessToMax();
+ if (Platform.isAndroid || Platform.isIOS) {
+ _setBrightnessToMax();
+ }
}
@override
diff --git a/lib/screens/send.dart b/lib/screens/send.dart
index 9a805d2..a2dc580 100644
--- a/lib/screens/send.dart
+++ b/lib/screens/send.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -456,11 +458,12 @@ class _SendScreenState extends State<SendScreen> {
onTap: _pasteAddressFromClipboard,
child: Icon(Icons.paste),
),
- GestureDetector(
- onTap: () =>
- Navigator.pushNamed(context, '/scan_qr'),
- child: Icon(Icons.qr_code),
- ),
+ if (Platform.isAndroid || Platform.isIOS)
+ GestureDetector(
+ onTap: () =>
+ Navigator.pushNamed(context, '/scan_qr'),
+ child: Icon(Icons.qr_code),
+ ),
GestureDetector(
onTap: _showContactPicker,
child: Icon(Icons.contacts_outlined),
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index 847ac6d..b2acc68 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:provider/provider.dart';
@@ -79,14 +81,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
SharedPreferencesKeys.notificationsEnabled,
true,
);
- await registerTxNotifierTaskIfEnabled();
+ await registerTxNotifierTaskIfAllowed();
}
} else {
await SharedPreferencesService.set<bool>(
SharedPreferencesKeys.notificationsEnabled,
false,
);
- await unregisterTxNotifierTask();
+ await unregisterPeriodicTasks();
}
}
@@ -367,19 +369,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
Switch(value: _appLockEnabled, onChanged: _setAppLockEnabled),
],
),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- i18n.settingsNotifyNewTxsLabel,
- style: TextStyle(fontSize: 18),
- ),
- Switch(
- value: _newTxNotificationsEnabled,
- onChanged: _setTxNotificationsEnabled,
- ),
- ],
- ),
+ if (Platform.isAndroid || Platform.isIOS)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ i18n.settingsNotifyNewTxsLabel,
+ style: TextStyle(fontSize: 18),
+ ),
+ Switch(
+ value: _newTxNotificationsEnabled,
+ onChanged: _setTxNotificationsEnabled,
+ ),
+ ],
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart
index c587a5f..e7305a1 100644
--- a/lib/services/notifications_service.dart
+++ b/lib/services/notifications_service.dart
@@ -9,7 +9,14 @@ class NotificationService {
'@mipmap/ic_launcher',
);
- const initSettings = InitializationSettings(android: initSettingsAndroid);
+ const initSettingsLinux = LinuxInitializationSettings(
+ defaultActionName: 'Open wallet',
+ );
+
+ const initSettings = InitializationSettings(
+ android: initSettingsAndroid,
+ linux: initSettingsLinux,
+ );
await notificationsPlugin.initialize(initSettings);
}
diff --git a/lib/services/tor_service.dart b/lib/services/tor_service.dart
index 682ca86..647817a 100644
--- a/lib/services/tor_service.dart
+++ b/lib/services/tor_service.dart
@@ -2,8 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:skylight_wallet/util/dirs.dart';
import 'package:skylight_wallet/util/logging.dart';
-import 'package:path_provider/path_provider.dart';
import 'package:tor_ffi_plugin/tor_ffi_plugin.dart';
final pTorService = Provider((_) => TorService.sharedInstance);
@@ -47,7 +47,7 @@ class TorService {
/// Returns a Future that completes when the Tor service has started.
Future<void> start() async {
_tor ??= Tor.instance;
- _torDataDirPath ??= (await getApplicationDocumentsDirectory()).path;
+ _torDataDirPath ??= (await getAppDir()).path;
// Start the Tor service.
try {
diff --git a/lib/util/dirs.dart b/lib/util/dirs.dart
index 74dec35..577c5e2 100644
--- a/lib/util/dirs.dart
+++ b/lib/util/dirs.dart
@@ -1,11 +1,27 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
-import 'package:skylight_wallet/consts.dart' as consts;
-Future<Directory> getTorDataDir() async {
- final documentsDirPath = (await getApplicationDocumentsDirectory());
- final torDataDirName = consts.torDataDirName;
- final torDataDir = Directory('${documentsDirPath.path}/$torDataDirName');
- return torDataDir;
+Future<void> createAppDir() async {
+ final appDir = await getAppDir();
+ if (!await appDir.exists()) {
+ await appDir.create(recursive: true);
+ }
+}
+
+Future<Directory> getAppDir() async {
+ final documentsDir = await getApplicationDocumentsDirectory();
+ var appDir = documentsDir;
+
+ if (Platform.isLinux) {
+ final homeDir = Platform.environment['HOME'];
+
+ if (homeDir != null) {
+ appDir = Directory('$homeDir/.skylight_wallet');
+ } else {
+ throw Exception('HOME environment variable is not set');
+ }
+ }
+
+ return appDir;
}
diff --git a/lib/util/logging.dart b/lib/util/logging.dart
index dbdf10d..0e81c03 100644
--- a/lib/util/logging.dart
+++ b/lib/util/logging.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
+import 'package:skylight_wallet/util/dirs.dart';
enum LogLevel { info, warn, error }
@@ -13,10 +14,8 @@ Future<File> _getLogFile() async {
// Get external storage directory for Android, or documents directory for iOS
if (Platform.isAndroid) {
directory = await getExternalStorageDirectory();
- } else if (Platform.isIOS) {
- directory = await getApplicationDocumentsDirectory();
} else {
- directory = await getApplicationDocumentsDirectory();
+ directory = await getAppDir();
}
if (directory == null) {
@@ -43,9 +42,9 @@ Future<void> cleanOldLogFiles() async {
if (Platform.isAndroid) {
directory = await getExternalStorageDirectory();
} else if (Platform.isIOS) {
- directory = await getApplicationDocumentsDirectory();
+ directory = await getAppDir();
} else {
- directory = await getApplicationDocumentsDirectory();
+ directory = await getAppDir();
}
if (directory == null) {
diff --git a/lib/util/wallet.dart b/lib/util/wallet.dart
index a7425ea..648a4e4 100644
--- a/lib/util/wallet.dart
+++ b/lib/util/wallet.dart
@@ -1,8 +1,8 @@
-import 'package:path_provider/path_provider.dart';
import 'package:skylight_wallet/consts.dart' as consts;
+import 'package:skylight_wallet/util/dirs.dart';
Future<String> getWalletPath([String? walletFileName]) async {
- var path = (await getApplicationDocumentsDirectory()).path;
+ var path = (await getAppDir()).path;
String walletName = walletFileName ?? consts.walletFileName;
path = '$path/$walletName';
return path;
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
index bfd34b0..2758fb1 100644
--- a/linux/CMakeLists.txt
+++ b/linux/CMakeLists.txt
@@ -44,6 +44,7 @@ function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+ target_compile_options(${TARGET} PRIVATE -Wno-deprecated)
endfunction()
# Flutter library and tool build rules.
@@ -112,6 +113,14 @@ install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
+# Install monero_libwallet2_api_c.so
+set(MONERO_LIB "${CMAKE_CURRENT_SOURCE_DIR}/monero_libwallet2_api_c.so")
+if(EXISTS "${MONERO_LIB}")
+ install(FILES "${MONERO_LIB}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
diff --git a/linux/fix_execstack.sh b/linux/fix_execstack.sh
new file mode 100755
index 0000000..b814be6
--- /dev/null
+++ b/linux/fix_execstack.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+# Script to fix executable stack flag in monero_libwallet2_api_c.so
+# This should be run whenever monero_libwallet2_api_c.so file is updated.
+
+LIB_PATH="$(dirname "$0")/monero_libwallet2_api_c.so"
+
+if [ ! -f "$LIB_PATH" ]; then
+ echo "Error: $LIB_PATH not found"
+ exit 1
+fi
+
+python3 << PYTHON_SCRIPT
+import struct
+import sys
+
+lib_path = "$LIB_PATH"
+
+# Read the ELF file
+with open(lib_path, 'rb') as f:
+ data = bytearray(f.read())
+
+# Check ELF magic
+if data[:4] != b'\x7fELF':
+ print('Error: Not an ELF file')
+ sys.exit(1)
+
+# Get ELF class (32 or 64 bit)
+elf_class = data[4]
+if elf_class == 1: # 32-bit
+ phoff = struct.unpack('<I', data[28:32])[0]
+ phentsize = struct.unpack('<H', data[42:44])[0]
+ phnum = struct.unpack('<H', data[44:46])[0]
+else: # 64-bit
+ phoff = struct.unpack('<Q', data[32:40])[0]
+ phentsize = struct.unpack('<H', data[54:56])[0]
+ phnum = struct.unpack('<H', data[56:58])[0]
+
+# Find GNU_STACK segment and clear executable flag
+found = False
+for i in range(phnum):
+ offset = phoff + i * phentsize
+ if elf_class == 1: # 32-bit
+ p_type = struct.unpack('<I', data[offset:offset+4])[0]
+ else: # 64-bit
+ p_type = struct.unpack('<I', data[offset:offset+4])[0]
+
+ # Check if it's PT_GNU_STACK (0x6474e551)
+ if p_type == 0x6474e551:
+ found = True
+ if elf_class == 1: # 32-bit
+ p_flags_offset = offset + 24
+ else: # 64-bit
+ p_flags_offset = offset + 4
+
+ # Read current flags
+ p_flags = struct.unpack('<I', data[p_flags_offset:p_flags_offset+4])[0]
+ # Clear executable bit (remove PF_X = 0x1)
+ p_flags = p_flags & ~0x1
+ # Write back
+ data[p_flags_offset:p_flags_offset+4] = struct.pack('<I', p_flags)
+ print(f'Cleared executable flag. New flags: 0x{p_flags:x}')
+ break
+
+if found:
+ # Write back the modified file
+ with open(lib_path, 'wb') as f:
+ f.write(data)
+ print('File updated successfully')
+else:
+ print('Warning: GNU_STACK segment not found')
+ sys.exit(1)
+PYTHON_SCRIPT
+
+if [ $? -eq 0 ]; then
+ echo "Successfully fixed executable stack flag in $LIB_PATH"
+else
+ echo "Failed to fix executable stack flag"
+ exit 1
+fi
+
diff --git a/linux/monero_libwallet2_api_c.so b/linux/monero_libwallet2_api_c.so
new file mode 100755
index 0000000..5357e8b
Binary files /dev/null and b/linux/monero_libwallet2_api_c.so differ
diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc
index 37ae633..8f54899 100644
--- a/linux/runner/my_application.cc
+++ b/linux/runner/my_application.cc
@@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) {
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
- gboolean use_header_bar = TRUE;
+ gboolean use_header_bar = FALSE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
index 0d2952f..810cf02 100644
--- a/windows/CMakeLists.txt
+++ b/windows/CMakeLists.txt
@@ -43,6 +43,7 @@ function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
+ target_compile_options(${TARGET} PRIVATE -Wno-deprecated)
endfunction()
# Flutter library and tool build rules.
Why this scored 31/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.