feat(core): add support for linux capabilities

This commit is contained in:
Federico Terzi 2021-07-14 20:43:31 +02:00
parent c381da94f9
commit 72d7c19e9f
10 changed files with 257 additions and 18 deletions

39
Cargo.lock generated
View File

@ -132,6 +132,17 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "caps"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c088f2dddef283f86b023ab1ebe2301c653326834996458b2f48d29b804e9540"
dependencies = [
"errno",
"libc",
"thiserror",
]
[[package]]
name = "cc"
version = "1.0.66"
@ -412,11 +423,33 @@ dependencies = [
"syn 1.0.67",
]
[[package]]
name = "errno"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe"
dependencies = [
"errno-dragonfly",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067"
dependencies = [
"gcc",
"libc",
]
[[package]]
name = "espanso"
version = "2.0.0"
dependencies = [
"anyhow",
"caps",
"clap",
"colored",
"crossbeam",
@ -774,6 +807,12 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "getrandom"
version = "0.1.16"

View File

@ -66,4 +66,7 @@ widestring = "0.4.3"
libc = "0.2.98"
[target.'cfg(target_os="macos")'.dependencies]
espanso-mac-utils = { path = "../espanso-mac-utils" }
espanso-mac-utils = { path = "../espanso-mac-utils" }
[target.'cfg(target_os="linux")'.dependencies]
caps = "0.5.2"

View File

@ -0,0 +1,32 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
pub fn can_use_capabilities() -> bool {
false
}
pub fn grant_capabilities() -> Result<()> {
Ok(())
}
pub fn clear_capabilities() -> Result<()> {
Ok(())
}

View File

@ -0,0 +1,42 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use caps::{CapSet, Capability};
use log::error;
pub fn can_use_capabilities() -> bool {
match caps::has_cap(None, CapSet::Permitted, Capability::CAP_DAC_OVERRIDE) {
Ok(has_cap) => has_cap,
Err(err) => {
error!("error while checking if capabilities are enabled: {}", err);
false
},
}
}
pub fn grant_capabilities() -> Result<()> {
caps::raise(None, CapSet::Effective, Capability::CAP_DAC_OVERRIDE)?;
Ok(())
}
pub fn clear_capabilities() -> Result<()> {
caps::clear(None, CapSet::Effective)?;
caps::clear(None, CapSet::Permitted)?;
Ok(())
}

View File

@ -0,0 +1,29 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(not(target_os = "linux"))]
mod fallback;
#[cfg(not(target_os = "linux"))]
pub use fallback::*;

View File

@ -42,6 +42,7 @@ pub struct CliModule {
pub requires_config: bool,
pub subcommand: String,
pub show_in_dock: bool,
pub requires_linux_capabilities: bool,
pub entry: fn(CliModuleArgs)->i32,
}
@ -55,6 +56,7 @@ impl Default for CliModule {
requires_config: false,
subcommand: "".to_string(),
show_in_dock: false,
requires_linux_capabilities: false,
entry: |_| {0},
}
}

View File

@ -18,7 +18,7 @@
*/
use anyhow::Result;
use espanso_detect::event::{InputEvent, KeyboardEvent, Status};
use espanso_detect::{SourceCreationOptions, event::{InputEvent, KeyboardEvent, Status}};
use log::{error};
use thiserror::Error;
@ -33,8 +33,7 @@ pub mod secure_input;
pub mod sequencer;
pub mod ui;
// TODO: pass options
pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)> {
pub fn init_and_spawn(source_options: SourceCreationOptions) -> Result<(DetectSource, ModifierStateStore, Sequencer)> {
let (sender, receiver) = crossbeam::channel::unbounded();
let (init_tx, init_rx) = crossbeam::channel::unbounded();
@ -46,7 +45,7 @@ pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)>
if let Err(error) = std::thread::Builder::new()
.name("detect thread".to_string())
.spawn(
move || match espanso_detect::get_source(Default::default()) {
move || match espanso_detect::get_source(source_options) {
Ok(mut source) => {
if source.initialize().is_err() {
init_tx

View File

@ -22,18 +22,39 @@ use std::thread::JoinHandle;
use anyhow::Result;
use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_detect::SourceCreationOptions;
use espanso_inject::InjectorCreationOptions;
use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote};
use log::info;
use log::{debug, error, info, warn};
use crate::{cli::worker::{engine::{dispatch::executor::{
use crate::{
cli::worker::{
engine::{
dispatch::executor::{
clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter,
event_injector::EventInjectorAdapter, icon::IconHandlerAdapter,
key_injector::KeyInjectorAdapter,
}, process::middleware::{image_resolve::PathProviderAdapter, match_select::MatchSelectorAdapter, matcher::{convert::MatchConverter, regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions}, rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions}}, multiplex::MultiplexAdapter, render::{
},
process::middleware::{
image_resolve::PathProviderAdapter,
match_select::MatchSelectorAdapter,
matcher::{
convert::MatchConverter,
regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions},
rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions},
},
multiplex::MultiplexAdapter,
render::{
extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter},
RendererAdapter,
}}}, match_cache::MatchCache}, engine::event::ExitMode};
},
},
},
match_cache::MatchCache,
},
engine::event::ExitMode,
};
use super::secure_input::SecureInputEvent;
@ -41,6 +62,7 @@ pub mod dispatch;
pub mod funnel;
pub mod process;
#[allow(clippy::too_many_arguments)]
pub fn initialize_and_spawn(
paths: Paths,
config_store: Box<dyn ConfigStore>,
@ -49,6 +71,7 @@ pub fn initialize_and_spawn(
exit_signal: Receiver<ExitMode>,
ui_event_receiver: Receiver<UIEvent>,
secure_input_receiver: Receiver<SecureInputEvent>,
use_evdev_backend: bool,
) -> Result<JoinHandle<ExitMode>> {
let handle = std::thread::Builder::new()
.name("engine thread".to_string())
@ -66,18 +89,35 @@ pub fn initialize_and_spawn(
let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager);
let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager);
let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend);
// TODO: pass all the options
let (detect_source, modifier_state_store, sequencer) =
super::engine::funnel::init_and_spawn().expect("failed to initialize detector module");
super::engine::funnel::init_and_spawn(SourceCreationOptions {
use_evdev: use_evdev_backend,
..Default::default()
})
.expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(secure_input_receiver, &sequencer);
let sources: Vec<&dyn crate::engine::funnel::Source> =
vec![&detect_source, &exit_source, &ui_source, &secure_input_source];
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
secure_input_receiver,
&sequencer,
);
let sources: Vec<&dyn crate::engine::funnel::Source> = vec![
&detect_source,
&exit_source,
&ui_source,
&secure_input_source,
];
let funnel = crate::engine::funnel::default(&sources);
let rolling_matcher = RollingMatcherAdapter::new(&match_converter.get_rolling_matches(), RollingMatcherAdapterOptions {
char_word_separators: config_manager.default().word_separators(),
});
let rolling_matcher = RollingMatcherAdapter::new(
&match_converter.get_rolling_matches(),
RollingMatcherAdapterOptions {
char_word_separators: config_manager.default().word_separators(),
},
);
let regex_matcher = RegexMatcherAdapter::new(
&match_converter.get_regex_matches(),
&RegexMatcherAdapterOptions {
@ -92,8 +132,11 @@ pub fn initialize_and_spawn(
let selector = MatchSelectorAdapter::new(&modulo_search_ui, &match_cache);
let multiplexer = MultiplexAdapter::new(&match_cache);
let injector = espanso_inject::get_injector(Default::default())
.expect("failed to initialize injector module"); // TODO: handle the options
let injector = espanso_inject::get_injector(InjectorCreationOptions {
use_evdev: use_evdev_backend,
..Default::default()
})
.expect("failed to initialize injector module"); // TODO: handle the options
let clipboard = espanso_clipboard::get_clipboard(Default::default())
.expect("failed to initialize clipboard module"); // TODO: handle options
@ -161,6 +204,13 @@ pub fn initialize_and_spawn(
&icon_adapter,
);
// Disable previously granted linux capabilities if not needed anymore
if has_granted_capabilities {
if let Err(err) = crate::capabilities::clear_capabilities() {
error!("unable to revoke linux capabilities: {}", err);
}
}
let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher);
let exit_mode = engine.run();
@ -172,3 +222,30 @@ pub fn initialize_and_spawn(
Ok(handle)
}
fn grant_linux_capabilities(use_evdev_backend: bool) -> bool {
if use_evdev_backend {
if crate::capabilities::can_use_capabilities() {
debug!("using linux capabilities to grant permissions needed by EVDEV backend");
if let Err(err) = crate::capabilities::grant_capabilities() {
error!("unable to grant CAP_DAC_OVERRIDE capability: {}", err);
false
} else {
debug!("successfully granted permissions using capabilities");
true
}
} else {
warn!("EVDEV backend is being used, but without enabling linux capabilities.");
warn!(" Although you CAN run espanso EVDEV backend as root, it's not recommended due");
warn!(
" to security reasons. Espanso supports linux capabilities to limit the attack surface"
);
warn!(" area by only leveraging on the CAP_DAC_OVERRIDE capability (needed to work with");
warn!(" /dev/input/* devices to detect and inject text) and disabling it as soon as the");
warn!(" initial setup is completed.");
false
}
} else {
false
}
}

View File

@ -46,6 +46,7 @@ pub fn new() -> CliModule {
CliModule {
requires_paths: true,
requires_config: true,
requires_linux_capabilities: true,
enable_logs: true,
log_mode: super::LogMode::AppendOnly,
subcommand: "worker".to_string(),
@ -81,6 +82,12 @@ fn worker_main(args: CliModuleArgs) -> i32 {
// TODO: show config loading errors in a GUI, if any
let use_evdev_backend = if cfg!(feature = "wayland") {
true
} else {
std::env::var("USE_EVDEV").unwrap_or_else(|_| "false".to_string()) == "true"
};
let icon_paths =
crate::icon::load_icon_paths(&paths.runtime).expect("unable to initialize icons");
@ -112,6 +119,7 @@ fn worker_main(args: CliModuleArgs) -> i32 {
engine_exit_receiver,
engine_ui_event_receiver,
engine_secure_input_receiver,
use_evdev_backend,
)
.expect("unable to initialize engine");

View File

@ -35,6 +35,7 @@ use simplelog::{
use crate::cli::{LogMode, PathsOverrides};
mod capabilities;
mod cli;
mod config;
mod engine;
@ -401,6 +402,13 @@ fn main() {
log_panics::init();
}
// If the process doesn't require linux capabilities, disable them
if !handler.requires_linux_capabilities {
if let Err(err) = crate::capabilities::clear_capabilities() {
error!("unable to clear linux capabilities: {}", err);
}
}
// If explicitly requested, we show the Dock icon on macOS
// We need to enable this selectively, otherwise we would end up with multiple
// dock icons due to the multi-process nature of espanso.