mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-15 10:46:41 -04:00
manager: Lock down the communication between the daemons
This is done by starting an additional dbus daemon on a private socket, then passing an fd handle to the user process. Requesting a handle is validated to ensure that the caller is the user daemon, otherwise it returns an error.
This commit is contained in:
parent
194646b8f1
commit
79cd65747d
8 changed files with 216 additions and 27 deletions
1
Makefile
1
Makefile
|
@ -29,6 +29,7 @@ install: target/release/steamos-manager target/release/steamosctl
|
|||
install -D -m644 LICENSE "$(DESTDIR)/usr/share/licenses/steamos-manager/LICENSE"
|
||||
|
||||
install -m644 "data/platform.toml" "$(DESTDIR)/usr/share/steamos-manager/"
|
||||
install -m644 "data/root-dbus.conf" "$(DESTDIR)/usr/share/steamos-manager/"
|
||||
|
||||
install -D -m644 -t "$(DESTDIR)/usr/share/dbus-1/interfaces" "data/interfaces/"*
|
||||
|
||||
|
|
12
README.md
12
README.md
|
@ -83,18 +83,6 @@ out from under SteamOS Manager if something on the system bypasses it. While
|
|||
this should never be the case if the user doesn't prod at the underlying system
|
||||
manually, it's something that interface users should be aware of.
|
||||
|
||||
## Implementation details
|
||||
|
||||
SteamOS Manager is compromised of two daemons: one runs as the logged in user
|
||||
and exposes a public DBus API on the session bus, and the second daemon runs as
|
||||
the `root` user. The root daemon exposes a limited DBus API on the system bus
|
||||
for tasks that require elevated permissions to execute.
|
||||
|
||||
The DBus API exposed on the system bus is considered a private implementation
|
||||
detail of SteamOS Manager and it may be changed at any moment and without
|
||||
warning. For this reason, we don't provide an XML schema for the system
|
||||
daemon's interface and clients shouldn't use it directly.
|
||||
|
||||
## Extending the API
|
||||
|
||||
To extend the API with a new method or property update the XML schema and
|
||||
|
|
18
data/root-dbus.conf
Normal file
18
data/root-dbus.conf
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<type>session</type>
|
||||
<keep_umask/>
|
||||
<listen>unix:tmpdir=/var/run/steamos-manager</listen>
|
||||
<auth>EXTERNAL</auth>
|
||||
<auth>ANONYMOUS</auth>
|
||||
<allow_anonymous/>
|
||||
<policy context="default">
|
||||
<allow send_destination="*" eavesdrop="true"/>
|
||||
<allow eavesdrop="true"/>
|
||||
<deny own="*"/>
|
||||
</policy>
|
||||
<policy user="root">
|
||||
<allow own="*"/>
|
||||
</policy>
|
||||
</busconfig>
|
|
@ -5,8 +5,11 @@
|
|||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{ensure, Result};
|
||||
use clap::Parser;
|
||||
use tokio::fs::read_link;
|
||||
use zbus::fdo::DBusProxy;
|
||||
use zbus::Connection;
|
||||
|
||||
use steamos_manager::daemon;
|
||||
|
||||
|
@ -15,11 +18,32 @@ struct Args {
|
|||
/// Run the root manager daemon
|
||||
#[arg(short, long)]
|
||||
root: bool,
|
||||
|
||||
#[arg(long, exclusive(true), hide(true))]
|
||||
validate_bus_owner: Option<u32>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
if let Some(pid) = args.validate_bus_owner {
|
||||
let connection = Connection::session().await?;
|
||||
let dbus = DBusProxy::new(&connection).await?;
|
||||
ensure!(
|
||||
dbus.get_connection_unix_process_id("com.steampowered.SteamOSManager1".try_into()?)
|
||||
.await?
|
||||
== pid,
|
||||
"Given pid does not match bus name"
|
||||
);
|
||||
let their_exe = read_link(format!("/proc/{pid}/exe")).await?;
|
||||
let our_exe = read_link(format!("/proc/self/exe")).await?;
|
||||
ensure!(
|
||||
their_exe == our_exe,
|
||||
"Bus name is not owned by steamos-manager"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.root {
|
||||
daemon::root().await
|
||||
} else {
|
||||
|
|
|
@ -5,9 +5,16 @@
|
|||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::str::FromStr;
|
||||
use tokio::fs::{create_dir_all, set_permissions};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
@ -16,11 +23,12 @@ use tracing::subscriber::set_global_default;
|
|||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter, Registry};
|
||||
use zbus::connection::{Builder, Connection};
|
||||
use zbus::Address;
|
||||
|
||||
use crate::daemon::{channel, Daemon, DaemonCommand, DaemonContext};
|
||||
use crate::ds_inhibit::Inhibitor;
|
||||
use crate::inputplumber::DeckService;
|
||||
use crate::manager::root::SteamOSManager;
|
||||
use crate::manager::root::{HandleContext, SteamOSManager};
|
||||
use crate::path;
|
||||
use crate::power::SysfsWriterService;
|
||||
use crate::sls::ftrace::Ftrace;
|
||||
|
@ -169,13 +177,66 @@ impl DaemonContext for RootContext {
|
|||
pub(crate) type Command = DaemonCommand<RootCommand>;
|
||||
|
||||
async fn create_connection(channel: Sender<Command>) -> Result<Connection> {
|
||||
create_dir_all("/var/run/steamos-manager").await?;
|
||||
set_permissions("/var/run/steamos-manager", Permissions::from_mode(0o700)).await?;
|
||||
|
||||
let mut process = process::Command::new("/usr/bin/dbus-daemon")
|
||||
.args([
|
||||
"--print-address",
|
||||
"--config-file=/usr/share/steamos-manager/root-dbus.conf",
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdout = BufReader::new(
|
||||
process
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or(anyhow!("Couldn't capture stdout"))?,
|
||||
);
|
||||
|
||||
let address = stdout
|
||||
.lines()
|
||||
.next_line()
|
||||
.await?
|
||||
.ok_or(anyhow!("Failed to read address"))?;
|
||||
let address = address.trim_end();
|
||||
|
||||
let sockpath = address
|
||||
.split_once(':')
|
||||
.map(|(_, params)| params.split(','))
|
||||
.and_then(|mut params| {
|
||||
params.find_map(|pair| {
|
||||
let (key, value) = pair.split_once('=')?;
|
||||
if key == "path" {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok_or(anyhow!("Failed to parse address"))?;
|
||||
|
||||
let connection = Builder::system()?
|
||||
.name("com.steampowered.SteamOSManager1")?
|
||||
.build()
|
||||
.await?;
|
||||
let manager = SteamOSManager::new(connection.clone(), channel).await?;
|
||||
connection
|
||||
.object_server()
|
||||
.at(
|
||||
"/com/steampowered/SteamOSManager1",
|
||||
HandleContext {
|
||||
sockpath: sockpath.into(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let root = Builder::address(Address::from_str(address)?)?
|
||||
.name("com.steampowered.SteamOSManager1")?
|
||||
.build()
|
||||
.await?;
|
||||
let manager = SteamOSManager::new(root.clone(), channel).await?;
|
||||
root.object_server()
|
||||
.at("/com/steampowered/SteamOSManager1", manager)
|
||||
.await?;
|
||||
Ok(connection)
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::mpsc::{unbounded_channel, Sender};
|
||||
use tracing::subscriber::set_global_default;
|
||||
|
@ -19,9 +21,11 @@ use tracing_subscriber::{fmt, EnvFilter, Registry};
|
|||
use xdg::BaseDirectories;
|
||||
use zbus::connection::{Builder, Connection};
|
||||
use zbus::fdo::ObjectManager;
|
||||
use zbus::AuthMechanism;
|
||||
|
||||
use crate::daemon::{channel, Daemon, DaemonCommand, DaemonContext};
|
||||
use crate::job::{JobManager, JobManagerService};
|
||||
use crate::manager::root::HandleContextProxy;
|
||||
use crate::manager::user::{create_interfaces, SignalRelayService};
|
||||
use crate::path;
|
||||
use crate::power::TdpManagerService;
|
||||
|
@ -111,6 +115,7 @@ pub(crate) type Command = DaemonCommand<()>;
|
|||
async fn create_connections(
|
||||
channel: Sender<Command>,
|
||||
) -> Result<(
|
||||
Connection,
|
||||
Connection,
|
||||
Connection,
|
||||
JobManagerService,
|
||||
|
@ -123,24 +128,42 @@ async fn create_connections(
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
let fd = HandleContextProxy::new(&system).await?.get_handle().await?;
|
||||
|
||||
let stream = UnixStream::from(OwnedFd::from(fd));
|
||||
let stream = tokio::net::UnixStream::from_std(stream)?;
|
||||
|
||||
let root = Builder::unix_stream(stream)
|
||||
.auth_mechanism(AuthMechanism::Anonymous)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let (jm_tx, rx) = unbounded_channel();
|
||||
let job_manager = JobManager::new(connection.clone()).await?;
|
||||
let jm_service = JobManagerService::new(job_manager, rx, system.clone());
|
||||
let jm_service = JobManagerService::new(job_manager, rx, root.clone());
|
||||
|
||||
let (tdp_tx, rx) = unbounded_channel();
|
||||
let tdp_service = TdpManagerService::new(rx, &system, &connection).await;
|
||||
let tdp_service = TdpManagerService::new(rx, &root, &connection).await;
|
||||
let tdp_tx = if tdp_service.is_ok() {
|
||||
Some(tdp_tx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let signal_relay_service =
|
||||
create_interfaces(connection.clone(), system.clone(), channel, jm_tx, tdp_tx).await?;
|
||||
let signal_relay_service = create_interfaces(
|
||||
connection.clone(),
|
||||
system.clone(),
|
||||
root.clone(),
|
||||
channel,
|
||||
jm_tx,
|
||||
tdp_tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
connection,
|
||||
system,
|
||||
root,
|
||||
jm_service,
|
||||
tdp_service,
|
||||
signal_relay_service,
|
||||
|
@ -157,7 +180,7 @@ pub async fn daemon() -> Result<()> {
|
|||
.with(EnvFilter::from_default_env());
|
||||
let (tx, rx) = channel::<UserContext>();
|
||||
|
||||
let (session, system, mirror_service, tdp_service, signal_relay_service) =
|
||||
let (session, system, _root, mirror_service, tdp_service, signal_relay_service) =
|
||||
match create_connections(tx).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
|
|
|
@ -9,14 +9,22 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use tokio::fs::File;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs::{metadata, File};
|
||||
use tokio::net::UnixSocket;
|
||||
use tokio::process;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::{error, info};
|
||||
use zbus::fdo::{self, DBusProxy};
|
||||
use zbus::message::Header;
|
||||
use zbus::names::BusName;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{self, Fd};
|
||||
use zbus::{fdo, interface, proxy, Connection};
|
||||
use zbus::zvariant::{self, Fd, OwnedFd as ZOwnedFd};
|
||||
use zbus::{interface, proxy, Connection};
|
||||
|
||||
use crate::daemon::root::{Command, RootCommand};
|
||||
use crate::daemon::DaemonCommand;
|
||||
|
@ -203,7 +211,7 @@ impl SteamOSManager {
|
|||
let result = File::create(als_path).await;
|
||||
|
||||
match result {
|
||||
Ok(f) => Ok(Fd::Owned(std::os::fd::OwnedFd::from(f.into_std().await))),
|
||||
Ok(f) => Ok(Fd::Owned(OwnedFd::from(f.into_std().await))),
|
||||
Err(message) => {
|
||||
error!("Error opening sysfs file for giving file descriptor: {message}");
|
||||
Err(fdo::Error::IOError(message.to_string()))
|
||||
|
@ -495,6 +503,70 @@ impl SteamOSManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HandleContext {
|
||||
pub(crate) sockpath: PathBuf,
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.HandleContext")]
|
||||
impl HandleContext {
|
||||
async fn get_handle(
|
||||
&self,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
) -> fdo::Result<Fd> {
|
||||
// To prevent things from snooping on or sending messages to the root
|
||||
// daemon indiscriminately, the communication channel between the user
|
||||
// and root daemons is done via a unix socket that is owned by root.
|
||||
// The GetHandle function will return a file descriptor to that socket
|
||||
// that the user daemon can use, but it validates that the caller is in
|
||||
// fact the user daemon. To do this, it validates that the user daemon
|
||||
// is both running the same executable as the root daemon as well as
|
||||
// that the user daemon owns the bus name on the session bus.
|
||||
let Some(sender) = header.sender() else {
|
||||
return Err(fdo::Error::InvalidArgs(String::from("No sender found")));
|
||||
};
|
||||
let dbus = DBusProxy::new(connection).await?;
|
||||
let sender = BusName::Unique(sender.clone());
|
||||
let pid = dbus.get_connection_unix_process_id(sender.clone()).await?;
|
||||
let uid = dbus.get_connection_unix_user(sender).await?;
|
||||
let gid = metadata(format!("/proc/{pid}"))
|
||||
.await
|
||||
.map_err(|e| fdo::Error::AuthFailed(e.to_string()))?
|
||||
.gid();
|
||||
|
||||
let result = process::Command::new("/proc/self/exe")
|
||||
.args(&["--validate-bus-owner", pid.to_string().as_str()])
|
||||
.uid(uid)
|
||||
.gid(gid)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| fdo::Error::AuthFailed(format!("Failed to authenticate sender: {e}")))?;
|
||||
|
||||
if !result.status.success() {
|
||||
return Err(fdo::Error::AccessDenied(
|
||||
String::from_utf8_lossy(&result.stderr).trim().to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let sock = UnixSocket::new_stream().map_err(to_zbus_fdo_error)?;
|
||||
let stream = sock
|
||||
.connect(&self.sockpath)
|
||||
.await
|
||||
.map_err(to_zbus_fdo_error)?;
|
||||
let fd = stream.into_std().map_err(to_zbus_fdo_error)?;
|
||||
Ok(Fd::Owned(OwnedFd::from(fd)))
|
||||
}
|
||||
}
|
||||
|
||||
#[proxy(
|
||||
interface = "com.steampowered.SteamOSManager1.HandleContext",
|
||||
default_service = "com.steampowered.SteamOSManager1",
|
||||
default_path = "/com/steampowered/SteamOSManager1"
|
||||
)]
|
||||
pub(crate) trait HandleContext {
|
||||
async fn get_handle(&self) -> fdo::Result<ZOwnedFd>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
|
@ -1076,11 +1076,12 @@ async fn create_device_interfaces(
|
|||
pub(crate) async fn create_interfaces(
|
||||
session: Connection,
|
||||
system: Connection,
|
||||
root: Connection,
|
||||
daemon: Sender<Command>,
|
||||
job_manager: UnboundedSender<JobManagerCommand>,
|
||||
tdp_manager: Option<UnboundedSender<TdpManagerCommand>>,
|
||||
) -> Result<SignalRelayService> {
|
||||
let proxy = Builder::<Proxy>::new(&system)
|
||||
let proxy = Builder::<Proxy>::new(&root)
|
||||
.destination("com.steampowered.SteamOSManager1")?
|
||||
.path("/com/steampowered/SteamOSManager1")?
|
||||
.interface("com.steampowered.SteamOSManager1.RootManager")?
|
||||
|
@ -1088,7 +1089,7 @@ pub(crate) async fn create_interfaces(
|
|||
.build()
|
||||
.await?;
|
||||
|
||||
let manager = SteamOSManager::new(system.clone(), proxy.clone(), job_manager.clone()).await?;
|
||||
let manager = SteamOSManager::new(root.clone(), proxy.clone(), job_manager.clone()).await?;
|
||||
|
||||
let als = AmbientLightSensor1 {
|
||||
proxy: proxy.clone(),
|
||||
|
@ -1313,6 +1314,7 @@ mod test {
|
|||
.set(|_, _| Ok((0, String::from("Interface wlan0"))));
|
||||
power::test::create_nodes().await?;
|
||||
create_interfaces(
|
||||
connection.clone(),
|
||||
connection.clone(),
|
||||
connection.clone(),
|
||||
tx_ctx,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue