Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/branch-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ jobs:
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
test-matrix: >-
[
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ jobs:
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
test-matrix: >-
[
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-qualification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jobs:
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
test-matrix: >-
[
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"cdi"},
{"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"},
{"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"},
Expand Down
5 changes: 3 additions & 2 deletions .packit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ actions:
# Uses git ls-files + tar instead of git archive so the tarball
# reflects any patching that Packit may have done (e.g. version bumps).
- 'bash -c "VERSION=${PACKIT_PROJECT_VERSION} && TMPDIR=$(mktemp -d) && DIR=openshell-${VERSION} && mkdir -p ${TMPDIR}/${DIR} && git ls-files -z | xargs -0 tar cf - | tar xf - -C ${TMPDIR}/${DIR}/ && tar -czf openshell-${VERSION}.tar.gz -C ${TMPDIR} ${DIR} && rm -rf ${TMPDIR}"'
# Step 2: Create vendored Cargo dependencies tarball for offline RPM build.
- 'bash -c "VERSION=${PACKIT_PROJECT_VERSION} && CARGO_HTTP_TIMEOUT=600 CARGO_NET_RETRY=5 cargo vendor --locked --quiet && tar -cJf openshell-${VERSION}-vendor.tar.xz vendor/ && rm -rf vendor/"'
# Step 2: Preserve Cargo source mappings, including pinned Git dependencies,
# with the vendored dependencies for the offline RPM build.
- 'bash -c "VERSION=${PACKIT_PROJECT_VERSION} && CARGO_HTTP_TIMEOUT=600 CARGO_NET_RETRY=5 cargo vendor --locked > cargo-vendor-config.toml && tar -cJf openshell-${VERSION}-vendor.tar.xz vendor/ cargo-vendor-config.toml && rm -rf vendor/ cargo-vendor-config.toml"'
# Step 3: Return the primary archive name. Packit expects create-archive
# to print one path for Source0; Source1 is patched explicitly below.
- 'bash -c "echo openshell-${PACKIT_PROJECT_VERSION}.tar.gz"'
Expand Down
45 changes: 44 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ rustix = { version = "1.1", features = ["process"] }
socket2 = "0.6"

# Serialization
container-device-interface = { git = "https://github.com/cncf-tags/container-device-interface-rs", rev = "b3fec145b213afc4dfd6900c578edb42dc99e5df", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_path_to_error = "0.1"
Expand Down
71 changes: 71 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,77 @@ nix run .#tmachine -- test fedora-podman-rootless binaries e2e-podman
nix run .#tmachine -- test fedora-podman-rootless binaries driver-podman
```

The `cdi` feature suite tests device injection without GPU hardware in the
`ubuntu-docker-rootful` environment. Build the candidate artifacts, then run:

```shell
nix run .#build-artifacts
nix run .#tmachine -- test ubuntu-docker-rootful deb cdi
```

If candidate binaries, images, and packages are already built, rebuild just the
CDI test archive with `nix run .#build-artifacts-cdi-tests`.

The `binaries` installer also supports this suite. Docker environment setup
creates the named scenario fixtures and a CDI spec that defines one device per
scenario, selected as `tmachine.example/cdi-scenario=<scenario>`. Persistent
spec sources live under `/var/lib/openshell-test-inputs/cdi/specs`; a
systemd-tmpfiles configuration recreates them in `/var/run/cdi` on every VM
boot. This preserves the fixtures across tmachine's setup, installation, and
test disk layers and makes them available to every suite using the Docker
environment. The tests need no GPU hardware.

Each test creates a sandbox that selects one scenario explicitly with `--gpu 1`
and the Docker `cdi_devices` driver option, then checks what the workload
observes or that creation is rejected. Selection by count or default, and the
`nvidia.com/gpu` inventory, are not covered here. Caller driver configuration
is enabled temporarily in the installed gateway's active TOML. The test
playbook restores the configuration and restarts the gateway even after test
failure; the CDI fixtures remain part of the environment. Docker must support
CDI. Branch and release feature-test matrices run this suite on Docker.

| Scenario | Injected resource | Asserted today | Expected with PR #4204 |
| --- | --- | --- | --- |
| `mount-under-read-only-baseline` | File at `/usr/share/tmachine-cdi/baseline-read-only-file`, under a directory the sandbox already makes read-only | Read succeeds, write denied. | Unchanged. |
| `read-only-file` | File at `/tmachine-cdi/read-only/file` | Read and write denied. | Read succeeds outside the baseline grants; write fails. |
| `read-only-directory` | Directory at `/tmachine-cdi/read-only/directory` | Reading its marker and creating files denied. | Reading its marker succeeds; write fails. |
| `library-parent` | Marker named `/tmachine-cdi/library/libfixture.so.1` | Read and write denied. | Resolver derives the library parent; the marker is not an actual shared library. |
| `broad-library-parent` | Marker named `/libtmachinefixture.so.1` | Read and write denied. | Launch rejects expansion to `/`. |
| `writable-file` | Writable file at `/tmachine-cdi/writable/file` | Read and write denied. | Launch rejects it even with exact or ancestor write grants. |
| `writable-directory` | Writable directory at `/tmachine-cdi/writable/directory` | Write denied. | Launch rejects it even with exact or ancestor write grants. |
| `omitted-access-file` | Bind without `ro` or `rw` | Read and write denied. | Effective writable access causes launch rejection. |
| `device-node` | Null character device at `/dev/tmachine-cdi-null` | Non-root, write denied. | Non-root read/write succeeds; an exact authored read-only entry fails launch. |
| `device-node-with-gid` | Group-restricted null device at `/dev/tmachine-cdi-group-null` | Non-root with the device's group, write denied. | The generated supplementary GID permits non-root access. |
| `unix-socket` | Echo socket at `/tmachine-cdi/socket/echo.sock` | Sibling file in the host directory is not visible. | Socket communication succeeds without deriving parent filesystem grants. |

Every scenario also supplies a spec-level environment marker and a read-only
mount at `/usr/share/tmachine-cdi/common/spec-marker`, allowing checks of
spec/device edit merging without depending on derived grants. The device group
is allocated in the disposable guest. A systemd socket unit restores the echo
endpoint after reboot; the existing tmpfiles configuration restores the
scenario spec. Negative scenarios must be selected individually, not combined
with positive cases.

The role publishes each scenario's expectations as `scenarios.json` next to the
fixtures. The `scenario_*` tests load them and assert the behavior recorded in
`vars/scenarios.yaml`, which is the sandbox's behavior today. A change that
alters a scenario's behavior, such as #4204 deriving grants from the spec,
updates those values in the same diff. To record the current behavior, run the
tests with `OPENSHELL_CDI_CAPTURE=1` set for the test process; they then print
what each scenario observed instead of asserting.

To add a scenario, add an entry to `cdi_scenarios` in
`tests/ansible/roles/cdi_fixtures/vars/scenarios.yaml` with the host fixtures
it needs, its CDI edits, and its `expect` (the checks on the workload, or a
rejection), then list it in `cdi_scenarios!` in
`tests/suites/features/cdi/tests/injection.rs`. A new spec file belongs in
`cdi_fixture_spec_files` in the role defaults, which the setup and test
playbooks share.

Directory absence, daemon configuration refresh, and restart tests require
orchestration around these fixtures. Spec projection secrecy is a separate
workload assertion. Hardware CUDA initialization remains outside this suite.

Print the exact tmachine archive selection as a shell `PODMAN_CI_TESTS` array:

```shell
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ openshell-otel = { path = "../openshell-otel", optional = true }
nix = { workspace = true }
rustix = { workspace = true }

[target.'cfg(target_os = "linux")'.dependencies]
container-device-interface = { workspace = true }

[features]
default = ["telemetry"]
## Compile in anonymous telemetry emission support. On by default; disable with
Expand Down
32 changes: 32 additions & 0 deletions crates/openshell-core/src/cdi_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Portable CDI name checking for non-Linux targets.

/// Check `<vendor>/<class>=<name>` syntax without resolving or authorizing devices.
/// Input is neither trimmed nor normalized.
#[must_use]
pub fn is_qualified_name(device: &str) -> bool {
let Some((kind, name)) = device.split_once('=') else {
return false;
};
let Some((vendor, class)) = kind.split_once('/') else {
return false;
};
let valid_component = |value: &str, device_name: bool| {
let bytes = value.as_bytes();
bytes.first().is_some_and(|byte| {
if device_name {
byte.is_ascii_alphanumeric()
} else {
byte.is_ascii_alphabetic()
}
}) && bytes.last().is_some_and(u8::is_ascii_alphanumeric)
&& bytes.iter().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'_' | b'-' | b'.')
|| (device_name && *byte == b':')
})
};
valid_component(vendor, false) && valid_component(class, false) && valid_component(name, true)
}
32 changes: 4 additions & 28 deletions crates/openshell-core/src/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,39 +302,15 @@ fn cdi_nvidia_gpu_suffix(id: &str) -> Option<&str> {
id.strip_prefix(CDI_NVIDIA_GPU_PREFIX)
}

// Local equivalent of the upstream CDI parser until its public API is released:
// https://github.com/cncf-tags/container-device-interface-rs/pull/177
fn is_qualified_name(device: &str) -> bool {
let Some((kind, name)) = device.split_once('=') else {
return false;
};
let Some((vendor, class)) = kind.split_once('/') else {
return false;
};
let valid_component = |value: &str, device_name: bool| {
let bytes = value.as_bytes();
bytes.first().is_some_and(|byte| {
if device_name {
byte.is_ascii_alphanumeric()
} else {
byte.is_ascii_alphabetic()
}
}) && bytes.last().is_some_and(u8::is_ascii_alphanumeric)
&& bytes.iter().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'_' | b'-' | b'.')
|| (device_name && *byte == b':')
})
};
valid_component(vendor, false) && valid_component(class, false) && valid_component(name, true)
}

/// Validate vendor-agnostic CDI qualified names without resolving or authorizing devices.
///
/// # Errors
/// Returns an error for host paths or malformed `<vendor>/<class>=<name>` selectors.
pub fn validate_cdi_device_names(devices: &[String], field: &str) -> Result<(), String> {
if devices.iter().any(|device| !is_qualified_name(device)) {
if devices
.iter()
.any(|device| !crate::cdi_name::is_qualified_name(device))
{
return Err(format!(
"{field} must contain CDI qualified names (<vendor>/<class>=<name>)"
));
Expand Down
6 changes: 6 additions & 0 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

pub mod activity;
pub mod auth;
#[cfg(target_os = "linux")]
pub mod cdi_name {
pub use container_device_interface::parser::is_qualified_name;
}
#[cfg(not(target_os = "linux"))]
pub mod cdi_name;
pub mod config;
pub mod container_paths;
pub mod denial;
Expand Down
2 changes: 1 addition & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ external-default-features = "allow"
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
allow-git = ["https://github.com/cncf-tags/container-device-interface-rs"]
Loading
Loading