crosvm/crosvm/sys/linux/
gpu.rs

1// Copyright 2017 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! GPU related things
6//! depends on "gpu" feature
7static_assertions::assert_cfg!(feature = "gpu");
8
9use std::collections::HashMap;
10use std::env;
11use std::path::PathBuf;
12
13use base::linux::move_proc_to_cgroup;
14use jail::*;
15use serde::Deserialize;
16use serde::Serialize;
17use serde_keyvalue::FromKeyValues;
18
19use super::*;
20use crate::crosvm::config::Config;
21
22pub struct GpuCacheInfo<'a> {
23    directory: Option<&'a str>,
24    environment: Vec<(&'a str, &'a str)>,
25}
26
27pub fn get_gpu_cache_info<'a>(
28    cache_dir: Option<&'a String>,
29    cache_size: Option<&'a String>,
30    foz_db_list_path: Option<&'a String>,
31    sandbox: bool,
32) -> GpuCacheInfo<'a> {
33    let mut dir = None;
34    let mut env = Vec::new();
35
36    // TODO (renatopereyra): Remove deprecated env vars once all src/third_party/mesa* are updated.
37    if let Some(cache_dir) = cache_dir {
38        if !Path::new(cache_dir).exists() {
39            warn!("shader caching dir {} does not exist", cache_dir);
40            // Deprecated in https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/15390
41            env.push(("MESA_GLSL_CACHE_DISABLE", "true"));
42
43            env.push(("MESA_SHADER_CACHE_DISABLE", "true"));
44        } else if cfg!(target_arch = "aarch64") && sandbox {
45            warn!("shader caching not yet supported on ARM with sandbox enabled");
46            // Deprecated in https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/15390
47            env.push(("MESA_GLSL_CACHE_DISABLE", "true"));
48
49            env.push(("MESA_SHADER_CACHE_DISABLE", "true"));
50        } else {
51            dir = Some(cache_dir.as_str());
52
53            // Deprecated in https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/15390
54            env.push(("MESA_GLSL_CACHE_DISABLE", "false"));
55            env.push(("MESA_GLSL_CACHE_DIR", cache_dir.as_str()));
56
57            env.push(("MESA_SHADER_CACHE_DISABLE", "false"));
58            env.push(("MESA_SHADER_CACHE_DIR", cache_dir.as_str()));
59
60            env.push(("MESA_DISK_CACHE_DATABASE", "1"));
61
62            if let Some(foz_db_list_path) = foz_db_list_path {
63                env.push(("MESA_DISK_CACHE_COMBINE_RW_WITH_RO_FOZ", "1"));
64                env.push((
65                    "MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST",
66                    foz_db_list_path,
67                ));
68            }
69
70            if let Some(cache_size) = cache_size {
71                // Deprecated in https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/15390
72                env.push(("MESA_GLSL_CACHE_MAX_SIZE", cache_size.as_str()));
73
74                env.push(("MESA_SHADER_CACHE_MAX_SIZE", cache_size.as_str()));
75            }
76        }
77    }
78
79    GpuCacheInfo {
80        directory: dir,
81        environment: env,
82    }
83}
84
85pub fn create_gpu_device(
86    cfg: &Config,
87    exit_evt_wrtube: &SendTube,
88    gpu_control_tube: Tube,
89    resource_bridges: Vec<Tube>,
90    render_server_fd: Option<SafeDescriptor>,
91    has_vfio_gfx_device: bool,
92    event_devices: Vec<EventDevice>,
93) -> DeviceResult {
94    let is_sandboxed = cfg.jail_config.is_some();
95    let mut gpu_params = cfg.gpu_parameters.clone().unwrap();
96
97    if is_sandboxed {
98        gpu_params.snapshot_scratch_path = Some(Path::new("/tmpfs-gpu-snapshot").to_path_buf());
99    }
100
101    if gpu_params.fixed_blob_mapping {
102        if has_vfio_gfx_device {
103            // TODO(b/323368701): make fixed_blob_mapping compatible with vfio dma_buf mapping for
104            // GPU pci passthrough.
105            debug!("gpu fixed blob mapping disabled: not compatible with passthrough GPU.");
106            gpu_params.fixed_blob_mapping = false;
107        } else if cfg!(feature = "vulkano") {
108            // TODO(b/244591751): make fixed_blob_mapping compatible with vulkano for opaque_fd blob
109            // mapping.
110            debug!("gpu fixed blob mapping disabled: not compatible with vulkano");
111            gpu_params.fixed_blob_mapping = false;
112        }
113    }
114
115    // external_blob must be enforced to ensure that a blob can be exported to a mappable descriptor
116    // (dma_buf, shmem, ...), since:
117    //   - is_sandboxed implies that blob mapping will be done out-of-process by the crosvm
118    //     hypervisor process.
119    //   - fixed_blob_mapping is not yet compatible with VmMemorySource::ExternalMapping
120    gpu_params.external_blob = is_sandboxed || gpu_params.fixed_blob_mapping;
121
122    // Implicit launch is not allowed when sandboxed. A socket fd from a separate sandboxed
123    // render_server process must be provided instead.
124    gpu_params.allow_implicit_render_server_exec =
125        gpu_params.allow_implicit_render_server_exec && !is_sandboxed;
126
127    let mut display_backends = vec![
128        virtio::DisplayBackend::X(cfg.x_display.clone()),
129        virtio::DisplayBackend::Stub,
130    ];
131
132    #[cfg(feature = "android_display")]
133    if let Some(service_name) = &cfg.android_display_service {
134        display_backends.insert(0, virtio::DisplayBackend::Android(service_name.to_string()));
135    }
136
137    // Use the unnamed socket for GPU display screens.
138    if let Some(socket_path) = cfg.wayland_socket_paths.get("") {
139        display_backends.insert(
140            0,
141            virtio::DisplayBackend::Wayland(Some(socket_path.to_owned())),
142        );
143    }
144
145    let dev = virtio::Gpu::new(
146        exit_evt_wrtube
147            .try_clone()
148            .context("failed to clone tube")?,
149        gpu_control_tube,
150        resource_bridges,
151        display_backends,
152        &gpu_params,
153        render_server_fd,
154        event_devices,
155        virtio::base_features(cfg.protection_type),
156        &cfg.wayland_socket_paths,
157        cfg.gpu_cgroup_path.as_ref(),
158    );
159
160    let jail = if let Some(jail_config) = cfg.jail_config.as_ref() {
161        let mut config = SandboxConfig::new(jail_config, "gpu_device");
162        config.bind_mounts = true;
163        // Allow changes made externally take effect immediately to allow shaders to be dynamically
164        // added by external processes.
165        config.remount_mode = Some(libc::MS_SLAVE);
166        let mut jail = create_gpu_minijail(
167            &jail_config.pivot_root,
168            &config,
169            /* render_node_only= */ false,
170            gpu_params.snapshot_scratch_path.as_deref(),
171        )?;
172
173        // Prepare GPU shader disk cache directory.
174        let cache_info = get_gpu_cache_info(
175            gpu_params.cache_path.as_ref(),
176            gpu_params.cache_size.as_ref(),
177            None,
178            cfg.jail_config.is_some(),
179        );
180
181        if let Some(dir) = cache_info.directory {
182            // Manually bind mount recursively to allow DLC shader caches
183            // to be propagated to the GPU process.
184            jail.mount(dir, dir, "", (libc::MS_BIND | libc::MS_REC) as usize)?;
185        }
186        for (key, val) in cache_info.environment {
187            env::set_var(key, val);
188        }
189
190        // Bind mount the wayland socket's directory into jail's root. This is necessary since
191        // each new wayland context must open() the socket. If the wayland socket is ever
192        // destroyed and remade in the same host directory, new connections will be possible
193        // without restarting the wayland device.
194        for socket_path in cfg.wayland_socket_paths.values() {
195            let dir = socket_path.parent().with_context(|| {
196                format!(
197                    "wayland socket path '{}' has no parent",
198                    socket_path.display(),
199                )
200            })?;
201            jail.mount(dir, dir, "", (libc::MS_BIND | libc::MS_REC) as usize)?;
202        }
203
204        Some(jail)
205    } else {
206        None
207    };
208
209    Ok(VirtioDeviceStub {
210        dev: Box::new(dev),
211        jail,
212    })
213}
214
215#[derive(Debug, Deserialize, Serialize, FromKeyValues, PartialEq, Eq)]
216#[serde(deny_unknown_fields, rename_all = "kebab-case")]
217pub struct GpuRenderServerParameters {
218    pub path: PathBuf,
219    pub cache_path: Option<String>,
220    pub cache_size: Option<String>,
221    pub foz_db_list_path: Option<String>,
222    pub precompiled_cache_path: Option<String>,
223    pub ld_preload_path: Option<String>,
224}
225
226fn get_gpu_render_server_environment(
227    cache_info: Option<&GpuCacheInfo>,
228    ld_preload_path: Option<&String>,
229) -> Result<Vec<String>> {
230    let mut env = HashMap::<String, String>::new();
231    let os_env_len = env::vars_os().count();
232
233    if let Some(cache_info) = cache_info {
234        env.reserve(os_env_len + cache_info.environment.len());
235        for (key, val) in cache_info.environment.iter() {
236            env.insert(key.to_string(), val.to_string());
237        }
238    } else {
239        env.reserve(os_env_len);
240    }
241
242    for (key_os, val_os) in env::vars_os() {
243        // minijail should accept OsStr rather than str...
244        let into_string_err = |_| anyhow!("invalid environment key/val");
245        let key = key_os.into_string().map_err(into_string_err)?;
246        let val = val_os.into_string().map_err(into_string_err)?;
247        env.entry(key).or_insert(val);
248    }
249
250    // for debugging purpose, avoid appending if LD_PRELOAD has been set outside
251    if !env.contains_key("LD_PRELOAD") {
252        if let Some(ld_preload_path) = ld_preload_path {
253            env.insert("LD_PRELOAD".to_string(), ld_preload_path.to_string());
254        }
255    }
256
257    // TODO(b/323284290): workaround to advertise 2 graphics queues in ANV
258    if !env.contains_key("ANV_QUEUE_OVERRIDE") {
259        env.insert("ANV_QUEUE_OVERRIDE".to_string(), "gc=2".to_string());
260    }
261
262    // TODO(b/237493180, b/284517235): workaround to enable ETC2/ASTC format emulation in Mesa
263    // TODO(b/284361281, b/328827736): workaround to enable legacy sparse binding in RADV
264    let driconf_options = [
265        "radv_legacy_sparse_binding",
266        "radv_require_etc2",
267        "vk_require_etc2",
268        "vk_require_astc",
269    ];
270    for opt in driconf_options {
271        if !env.contains_key(opt) {
272            env.insert(opt.to_string(), "true".to_string());
273        }
274    }
275
276    // TODO(b/339766043): workaround to disable Vulkan protected memory feature in Mali
277    if !env.contains_key("MALI_BASE_PROTECTED_MEMORY_HEAP_SIZE") {
278        env.insert(
279            "MALI_BASE_PROTECTED_MEMORY_HEAP_SIZE".to_string(),
280            "0".to_string(),
281        );
282    }
283
284    Ok(env.iter().map(|(k, v)| format!("{k}={v}")).collect())
285}
286
287pub fn start_gpu_render_server(
288    cfg: &Config,
289    render_server_parameters: &GpuRenderServerParameters,
290) -> Result<(Minijail, SafeDescriptor)> {
291    let (server_socket, client_socket) =
292        UnixSeqpacket::pair().context("failed to create render server socket")?;
293
294    let (jail, cache_info) = if let Some(jail_config) = cfg.jail_config.as_ref() {
295        let mut config = SandboxConfig::new(jail_config, "gpu_render_server");
296        // Allow changes made externally take effect immediately to allow shaders to be dynamically
297        // added by external processes.
298        config.remount_mode = Some(libc::MS_SLAVE);
299        config.bind_mounts = true;
300        // Run as root in the jail to keep capabilities after execve, which is needed for
301        // mounting to work.  All capabilities will be dropped afterwards.
302        config.run_as = RunAsUser::Root;
303        let mut jail = create_gpu_minijail(
304            &jail_config.pivot_root,
305            &config,
306            /* render_node_only= */ true,
307            /* snapshot_scratch_path= */ None,
308        )?;
309
310        let cache_info = get_gpu_cache_info(
311            render_server_parameters.cache_path.as_ref(),
312            render_server_parameters.cache_size.as_ref(),
313            render_server_parameters.foz_db_list_path.as_ref(),
314            true,
315        );
316
317        if let Some(dir) = cache_info.directory {
318            // Manually bind mount recursively to allow DLC shader caches
319            // to be propagated to the GPU process.
320            jail.mount(dir, dir, "", (libc::MS_BIND | libc::MS_REC) as usize)?;
321        }
322        if let Some(precompiled_cache_dir) = &render_server_parameters.precompiled_cache_path {
323            jail.mount_bind(precompiled_cache_dir, precompiled_cache_dir, true)?;
324        }
325
326        // bind mount /dev/log for syslog
327        let log_path = Path::new("/dev/log");
328        if log_path.exists() {
329            jail.mount_bind(log_path, log_path, true)?;
330        }
331
332        (jail, Some(cache_info))
333    } else {
334        (Minijail::new().context("failed to create jail")?, None)
335    };
336
337    let inheritable_fds = [
338        server_socket.as_raw_descriptor(),
339        libc::STDOUT_FILENO,
340        libc::STDERR_FILENO,
341    ];
342
343    let cmd = &render_server_parameters.path;
344    let cmd_str = cmd
345        .to_str()
346        .ok_or_else(|| anyhow!("invalid render server path"))?;
347    let fd_str = server_socket.as_raw_descriptor().to_string();
348    let args = [cmd_str, "--socket-fd", &fd_str];
349
350    let env = Some(get_gpu_render_server_environment(
351        cache_info.as_ref(),
352        render_server_parameters.ld_preload_path.as_ref(),
353    )?);
354    let mut envp: Option<Vec<&str>> = None;
355    if let Some(ref env) = env {
356        envp = Some(env.iter().map(AsRef::as_ref).collect());
357    }
358
359    let render_server_pid = jail
360        .run_command(minijail::Command::new_for_path(
361            cmd,
362            &inheritable_fds,
363            &args,
364            envp.as_deref(),
365        )?)
366        .context("failed to start gpu render server")?;
367
368    if let Some(gpu_server_cgroup_path) = &cfg.gpu_server_cgroup_path {
369        move_proc_to_cgroup(gpu_server_cgroup_path.to_path_buf(), render_server_pid)?;
370    }
371
372    Ok((jail, SafeDescriptor::from(client_socket)))
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::crosvm::config::from_key_values;
379
380    #[test]
381    fn parse_gpu_render_server_parameters() {
382        let res: GpuRenderServerParameters = from_key_values("path=/some/path").unwrap();
383        assert_eq!(
384            res,
385            GpuRenderServerParameters {
386                path: "/some/path".into(),
387                cache_path: None,
388                cache_size: None,
389                foz_db_list_path: None,
390                precompiled_cache_path: None,
391                ld_preload_path: None,
392            }
393        );
394
395        let res: GpuRenderServerParameters = from_key_values("/some/path").unwrap();
396        assert_eq!(
397            res,
398            GpuRenderServerParameters {
399                path: "/some/path".into(),
400                cache_path: None,
401                cache_size: None,
402                foz_db_list_path: None,
403                precompiled_cache_path: None,
404                ld_preload_path: None,
405            }
406        );
407
408        let res: GpuRenderServerParameters =
409            from_key_values("path=/some/path,cache-path=/cache/path,cache-size=16M").unwrap();
410        assert_eq!(
411            res,
412            GpuRenderServerParameters {
413                path: "/some/path".into(),
414                cache_path: Some("/cache/path".into()),
415                cache_size: Some("16M".into()),
416                foz_db_list_path: None,
417                precompiled_cache_path: None,
418                ld_preload_path: None,
419            }
420        );
421
422        let res: GpuRenderServerParameters = from_key_values(
423            "path=/some/path,cache-path=/cache/path,cache-size=16M,foz-db-list-path=/db/list/path,precompiled-cache-path=/precompiled/path",
424        )
425        .unwrap();
426        assert_eq!(
427            res,
428            GpuRenderServerParameters {
429                path: "/some/path".into(),
430                cache_path: Some("/cache/path".into()),
431                cache_size: Some("16M".into()),
432                foz_db_list_path: Some("/db/list/path".into()),
433                precompiled_cache_path: Some("/precompiled/path".into()),
434                ld_preload_path: None,
435            }
436        );
437
438        let res: GpuRenderServerParameters =
439            from_key_values("path=/some/path,ld-preload-path=/ld/preload/path").unwrap();
440        assert_eq!(
441            res,
442            GpuRenderServerParameters {
443                path: "/some/path".into(),
444                cache_path: None,
445                cache_size: None,
446                foz_db_list_path: None,
447                precompiled_cache_path: None,
448                ld_preload_path: Some("/ld/preload/path".into()),
449            }
450        );
451
452        let res =
453            from_key_values::<GpuRenderServerParameters>("cache-path=/cache/path,cache-size=16M");
454        assert!(res.is_err());
455
456        let res = from_key_values::<GpuRenderServerParameters>("");
457        assert!(res.is_err());
458    }
459}