Skip to content
Draft
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
7 changes: 4 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Released versions of the spec are available as Git tags.
| v0.8.0 | | Remove .ToOCI() functions from specs-go package. |
| v1.0.0 | | Move minimum version logic to specs-go package. |
| v1.1.0 | | Add `NetDevices` to `ContainerEdits`, `Schemata` and `EnableMonitoring` to `IntelRdt`. Dropped `EnableCMT` and `EnableMBM` fields from `IntelRdt`. |
| v1.2.0 | | Allow wildcard device nodes to be specified. These have a `Path` of `*` and allow for device `Major` and `Minor` values of `-1` which is mapped to the `*` device in the OCI runtime spec. |

*Note*: spec loading fails on unknown fields and when the minimum required version is higher than the version specified in the spec. The minimum required version is determined based on the usage of fields mentioned in the table above. For example the minimum required version is v0.6.0 if the `Annotations` field is used in the spec, but `IntelRdt` is not.
`MinimumRequiredVersion` API can be used to get the minimum required version.
Expand Down Expand Up @@ -229,11 +230,11 @@ The `containerEdits` field is referenced in two places in the specification:
The `containerEdits` field has the following definition:
* `env` (array of strings in the format of "VARNAME=VARVALUE", OPTIONAL) describes the environment variables that should be set. These values are appended to the container environment array.
* `deviceNodes` (array of objects, OPTIONAL) describes the device nodes that should be mounted:
* `path` (string, REQUIRED) path of the device within the container.
* `path` (string, REQUIRED) path of the device within the container. As of v1.2.0, if the path is `*` the device node is used to only update the cgroups for the container.
* `hostPath` (string, OPTIONAL) path of the device node on the host. If not specified the value for `path` is used. Added in v0.5.0.
* `type` (string, OPTIONAL) Device type: block, char, etc.
* `major` (int64, OPTIONAL) Device major number.
* `minor` (int64, OPTIONAL) Device minor number.
* `major` (int64, OPTIONAL) Device major number. As of v1.2.0, if the `path` of the device node is `*` the value of `-1` is allowed and represents a group rule matching all device major numbers.
* `minor` (int64, OPTIONAL) Device minor number. As of v1.2.0, if the `path` of the device node is `*` the value of `-1` is allowed and represents a group rule matching all device minor numbers.
* `fileMode` (int64, OPTIONAL) file mode for the device.
* `permissions` (string, OPTIONAL) Cgroups permissions of the device, candidates are one or more of:
* r - allows container to read from the specified device.
Expand Down
133 changes: 97 additions & 36 deletions pkg/cdi/container-edits.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,35 +88,10 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error {
for _, d := range e.DeviceNodes {
dn := DeviceNode{d}

err := dn.fillMissingInfo()
err := dn.addToGenerator(&specgen, spec)
if err != nil {
return err
}
dev := dn.toOCI()
if dev.UID == nil && spec.Process != nil {
if uid := spec.Process.User.UID; uid > 0 {
dev.UID = &uid
}
}
if dev.GID == nil && spec.Process != nil {
if gid := spec.Process.User.GID; gid > 0 {
dev.GID = &gid
}
}

specgen.RemoveDevice(dev.Path)
specgen.AddDevice(dev)

if dev.Type == "b" || dev.Type == "c" {
access := d.Permissions
switch access {
case "":
access = "rwm"
case NoPermissions:
access = ""
}
specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access)
}
}

if len(e.NetDevices) > 0 {
Expand Down Expand Up @@ -353,19 +328,11 @@ type DeviceNode struct {

// Validate a CDI Spec DeviceNode.
func (d *DeviceNode) Validate() error {
validTypes := map[string]struct{}{
"": {},
"b": {},
"c": {},
"u": {},
"p": {},
}

if d.Path == "" {
return errors.New("invalid (empty) device path")
}
if _, ok := validTypes[d.Type]; !ok {
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
if strings.Contains(d.Path, "*") && d.Path != "*" {
return errors.New("invalid device device path; wildcard device path")
}
switch {
case d.Permissions == "":
Expand All @@ -375,6 +342,100 @@ func (d *DeviceNode) Validate() error {
d.Path, d.Permissions)
}

switch {
case d.isWildcardDevice():
validTypes := map[string]struct{}{"b": {}, "c": {}}
if _, ok := validTypes[d.Type]; !ok {
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
}
default:
validTypes := map[string]struct{}{
"": {},
"b": {},
"c": {},
"u": {},
"p": {},
}
if _, ok := validTypes[d.Type]; !ok {
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
}
// For a regular device, we also check that we have valid major and
// minor numbers.
if d.Major == -1 {
return fmt.Errorf("wildcard device major requires a wildcard device")
}
if d.Minor == -1 {
return fmt.Errorf("wildcard device minor requires a wildcard device")
}
}

return nil
}

func (d *DeviceNode) addToGenerator(specgen *ocigen.Generator, spec *oci.Spec) error {
if d.isWildcardDevice() {
return d.addAsWildcardDevice(specgen)
}

err := d.fillMissingInfo()
if err != nil {
return err
}
dev := d.toOCI()
if dev.UID == nil && spec.Process != nil {
if uid := spec.Process.User.UID; uid > 0 {
dev.UID = &uid
}
}
if dev.GID == nil && spec.Process != nil {
if gid := spec.Process.User.GID; gid > 0 {
dev.GID = &gid
}
}

specgen.RemoveDevice(dev.Path)
specgen.AddDevice(dev)

if dev.Type == "b" || dev.Type == "c" {
specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, d.getAccessString())
}
return nil
}

func (d *DeviceNode) getAccessString() string {
switch d.Permissions {
case "":
return "rwm"
case NoPermissions:
return ""
default:
return d.Permissions
}
}

// isWildcardDevice returns whether the device node represents a "wildcard" device.
// Such devices do not cause device nodes to be created in the container, but
// do update the cgroups to allow device access. A wildcard device always has
// the path specified as "*" and setting major or minor numbers to -1 will add
// a cgroup rule that matches all major (or minor) numbers.
func (d *DeviceNode) isWildcardDevice() bool {
return d.Path == "*"
}

func (d *DeviceNode) addAsWildcardDevice(specgen *ocigen.Generator) error {
if d.Type != "b" && d.Type != "c" {
return fmt.Errorf("wildcard device node not supported for device type %v", d.Type)
}
var major *int64
if d.Major != -1 {
major = &d.Major
}
var minor *int64
if d.Minor != -1 {
minor = &d.Minor
}
Comment on lines +429 to +436

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels as if this is the CORE change here since this SHOULD allow us to inject the cgroup rule even without performing additional checks as long as SOME non-zero device major is specified. This works because we explicitly ignore errors getting device node information if at least the Major and Type is is specified. What we need is the -1 to nil mapping since nil is interpreted as * by the OCI runtime spec.

@klihub klihub Apr 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a mental note... It's a pity we don't have Major and Minor as *int64 instead, so we could differentiate between zero- and omitted values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wonder, do we need "safety net' to prevent accidental "allow all devices" setting?

specgen.AddLinuxResourcesDevice(true, d.Type, major, minor, d.getAccessString())

return nil
}

Expand Down
73 changes: 73 additions & 0 deletions pkg/cdi/container-edits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ func TestValidateContainerEdits(t *testing.T) {
},
},
},
{
name: "valid wildcard device nodes",
edits: &cdi.ContainerEdits{
DeviceNodes: []*cdi.DeviceNode{
{
Path: "*", Type: "c",
},
{
Path: "*", Type: "b",
},
{
Path: "*", Type: "c", Major: -1,
},
{
Path: "*", Type: "b", Minor: -1,
},
{
Path: "*", Type: "c", Permissions: NoPermissions,
},
{
Path: "*", Type: "c", Permissions: "rwm",
},
},
},
},
{
name: "invalid device, empty path",
edits: &cdi.ContainerEdits{
Expand Down Expand Up @@ -133,6 +158,54 @@ func TestValidateContainerEdits(t *testing.T) {
},
invalid: true,
},
{
name: "invalid wildcard device, wildcard in path",
edits: &cdi.ContainerEdits{
DeviceNodes: []*cdi.DeviceNode{
{
Path: "/dev/vendor*",
Type: "b",
},
},
},
invalid: true,
},
{
name: "invalid wildcard device, non-wildcard with major",
edits: &cdi.ContainerEdits{
DeviceNodes: []*cdi.DeviceNode{
{
Path: "/dev/vendorctl",
Major: -1,
},
},
},
invalid: true,
},
{
name: "invalid wildcard device, non-wildcard with minor",
edits: &cdi.ContainerEdits{
DeviceNodes: []*cdi.DeviceNode{
{
Path: "/dev/vendorctl",
Minor: -1,
},
},
},
invalid: true,
},
{
name: "invalid wildcard device, with incorrect type",
edits: &cdi.ContainerEdits{
DeviceNodes: []*cdi.DeviceNode{
{
Path: "*",
Type: "p",
},
},
},
invalid: true,
},
{
name: "valid device, with NoPermissions",
edits: &cdi.ContainerEdits{
Expand Down
35 changes: 35 additions & 0 deletions specs-go/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
v080 version = "v0.8.0"
v100 version = "v1.0.0"
v110 version = "v1.1.0"
v120 version = "v1.2.0"

// vEarliest is the earliest supported version of the CDI specification
vEarliest version = v030
Expand All @@ -60,6 +61,7 @@ var validSpecVersions = requiredVersionMap{
v080: requiresV080,
v100: requiresV100,
v110: requiresV110,
v120: requiresV120,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self. I still need to add tests for this.

}

// ValidateVersion checks whether the specified spec version is valid.
Expand Down Expand Up @@ -142,6 +144,39 @@ func (r requiredVersionMap) requiredVersion(spec *Spec) version {
return minVersion
}

// requiresV120 returns true if the spec uses v1.2.0 features.
func requiresV120(spec *Spec) bool {
if spec.ContainerEdits.requiresV120() {
return true
}

for _, d := range spec.Devices {
if d.ContainerEdits.requiresV120() {
return true
}
}

return false
}

// requiresV120 returns true if the specified containers uses v1.2.0 features.
// These are:
// - Wildcard devices that can be used to only inject cgroup rules for a
// container.
func (e ContainerEdits) requiresV120() bool {
for _, dn := range e.DeviceNodes {
switch {
case dn.Path == "*":
return true
case dn.Major == -1:
return true
case dn.Minor == -1:
return true
}
}
return false
}

// requiresV110 returns true if the spec uses v1.1.0 features.
func requiresV110(spec *Spec) bool {
if i := spec.ContainerEdits.IntelRdt; i != nil {
Expand Down