Validate PCI Address (domain is 16 bits, bus is 8 bits, device is 5 bits, function is 3 bits)

it also changes the data type from uint32 to uint16 for PCI Root and PCI Address

Signed-off-by: Shingo Omura <everpeace@gmail.com>
This commit is contained in:
Shingo Omura
2025-06-14 15:26:12 +09:00
parent 752a043692
commit cb26d065d3
4 changed files with 341 additions and 92 deletions

View File

@@ -24,19 +24,12 @@ func TestAddStandardDeviceAttributes(t *testing.T) {
})
testSysFs := utils.NewSysfsWithRoot(testSysfsDir)
// "0123:45:67.8"
testPCIAddress := utils.PCIAddress{
Domain: 0x0123,
Bus: 0x45,
Device: 0x67,
Function: 0x8,
}
// "0123:45"
testPCIRoot := utils.PCIRoot{
Domain: 0x1234,
Bus: 0x56,
}
// /sys/bus/pci/devices/0123:45:67.8 --> /sys/devices/pci1234:56/0123:45:67.8
// "0123:45:1e.7"
testPCIAddress := utils.MustNewPCIAddress(0x0123, 0x45, 0x1e, 0x7)
// "1234:56"
testPCIRoot := utils.MustNewPCIRoot(0x1234, 0x56)
// /sys/bus/pci/devices/0123:45:1e.7 --> /sys/devices/pci1234:56/0123:45:1e.7
testPCIDevicePath := testSysFs.Devices("pci" + testPCIRoot.String() + "/" + testPCIAddress.String())
dratesting.TouchFile(t, testPCIDevicePath)
testPCIBusPath := testSysFs.Bus("pci/devices/" + testPCIAddress.String())
@@ -49,7 +42,7 @@ func TestAddStandardDeviceAttributes(t *testing.T) {
"pci attrs": {
opts: []StandardDeviceAttributesOption{
WithStandardPCIDeviceAttributesOpts(
WithPCIDeviceAddress(&testPCIAddress),
WithPCIDeviceAddress(testPCIAddress),
withSysfs(testSysFs), // Use the mock sysfs
),
},

View File

@@ -17,8 +17,8 @@ import (
func TestStandardPCIDeviceAttributes(t *testing.T) {
pcieRootAttrName := resourceapi.QualifiedName(resourceapi.StandardDeviceAttributePCIeRoot)
address := &utils.PCIAddress{Domain: 0, Bus: 1, Device: 2, Function: 3}
root := &utils.PCIRoot{Domain: 0, Bus: 1}
address := utils.MustNewPCIAddress(0x0, 0x1, 0x2, 0x3)
root := utils.MustNewPCIRoot(0x0, 0x1)
standardAttributes := map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
pcieRootAttrName: {
StringValue: ptr.To("pci" + root.String()),

View File

@@ -2,6 +2,7 @@ package utils
import (
"fmt"
"math"
"os"
"path/filepath"
"regexp"
@@ -9,17 +10,45 @@ import (
"strings"
)
const (
// PCI address consists of:
// - domain: 16 bits, it can be represented in 4-digit hexadecimal number
// - bus: 8 bits, it can be represented in 2-digit hexadecimal number
// - device: 5 bits, it can be represented in 2-digit hexadecimal number
// - function: 3 bits, it can be represented in 1-digit hexadecimal number
//
// ref: "Chapter 12. PCI Drivers - PCI Addressing"
// Linux Device Drivers, 3rd Edition by Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman
// https://www.oreilly.com/library/view/linux-device-drivers/0596005903/ch12.html
PCIDomainBits = uint16(16)
PCIDomainMax = uint16(math.MaxUint16)
PCIBusBits = uint16(8)
PCIBusMax = uint16((1 << PCIBusBits) - 1)
PCIDeviceBits = uint16(5)
PCIDeviceMax = uint16((1 << PCIDeviceBits) - 1)
PCIFunctionBits = uint16(3)
PCIFunctionMax = uint16((1 << PCIFunctionBits) - 1)
)
var (
// bdfRegexp matches PCI address in BDF notation.
// The format is <domain>:<bus>:<device>.<function>
// where:
// - domain: 4-digit hexadecimal number representing the PCI domain
// - bus: 2-digit hexadecimal number representing the PCI bus
// - device: 2-digit hexadecimal number representing the PCI device
// - function: 1-digit hexadecimal number representing the PCI function
// - domain: 4-digit hexadecimal number representing the PCI domain (16 bits)
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
// - device: 2-digit hexadecimal number representing the PCI device (5 bits)
// - function: 1-digit hexadecimal number representing the PCI function (3 bits)
//
// Example: "0000:0e:1f.0" represents domain 0, bus 14, device 31, function 0.
//
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
bdfRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`)
// pciRootRegexp matches PCI root address in the format "pci<domain>:<bus>".
// The format is:
// - domain: 4-digit hexadecimal number representing the PCI domain (32 bits)
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
pciRootRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2})$`)
)
// PCIRoot represents a PCI root address in the combination of Domain and Bus.
@@ -28,34 +57,84 @@ var (
//
// ref: https://docs.kernel.org/PCI/sysfs-pci.html
type PCIRoot struct {
Domain uint32
Bus uint32
domain uint16
bus uint16
}
func NewPCIRoot(domain, bus uint16) (*PCIRoot, error) {
// no validation for domain, as it uses full 16 bits
if bus > PCIBusMax {
return nil, fmt.Errorf("invalid PCI bus number: %02x, must be in range 0-%d (%dbits)", bus, PCIBusMax, PCIBusBits)
}
return &PCIRoot{
domain: domain,
bus: bus,
}, nil
}
func MustNewPCIRoot(domain, bus uint16) *PCIRoot {
root, err := NewPCIRoot(domain, bus)
if err != nil {
panic(fmt.Sprintf("failed to create PCIRoot: %v", err))
}
return root
}
// String returns the string representation of the PCIRoot in the format "domain:bus".
func (p *PCIRoot) String() string {
return fmt.Sprintf("%04x:%02x", p.Domain, p.Bus)
return fmt.Sprintf("%04x:%02x", p.domain, p.bus)
}
// The PCIAddress holds PCI address components in BDF notation.
// <domain>:<bus>:<device>.<function>
// where:
// - domain: 4-digit hexadecimal number representing the PCI domain
// - bus: 2-digit hexadecimal number representing the PCI bus
// - device: 2-digit hexadecimal number representing the PCI device
// - function: 1-digit hexadecimal number representing the PCI function
// - domain: 4-digit hexadecimal number representing the PCI domain (16 bits)
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
// - device: 2-digit hexadecimal number representing the PCI device (5 bits)
// - function: 1-digit hexadecimal number representing the PCI function (3 bits)
//
// Example: "0000:0e:1f.0" represents domain 0, bus 14, device 31, function 0.
//
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
// ref:
// - BDF Notation: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
// - PCI Addressing: https://docs.kernel.org/PCI/sysfs-pci.html
type PCIAddress struct {
Domain uint32
Bus uint32
Device uint32
Function uint32
domain uint16
bus uint16
device uint16
function uint16
}
func NewPCIAddress(domain, bus, device, function uint16) (*PCIAddress, error) {
// no validation for domain, as it uses full 16 bits
if bus > PCIBusMax {
return nil, fmt.Errorf("invalid PCI bus number: %02x, must be in range 0-%d (%dbits)", bus, PCIBusMax, PCIBusBits)
}
if device > PCIDeviceMax {
return nil, fmt.Errorf("invalid PCI device number: %02x, must be in range 0-%d (%dbits)", device, PCIDeviceMax, PCIDeviceBits)
}
if function > PCIFunctionMax {
return nil, fmt.Errorf("invalid PCI function number: %01x, must be in range 0-%d (%dbits)", function, PCIFunctionMax, PCIFunctionBits)
}
return &PCIAddress{
domain: domain,
bus: bus,
device: device,
function: function,
}, nil
}
func MustNewPCIAddress(domain, bus, device, function uint16) *PCIAddress {
addr, err := NewPCIAddress(domain, bus, device, function)
if err != nil {
panic(fmt.Sprintf("failed to create PCIAddress: %v", err))
}
return addr
}
func (p *PCIAddress) String() string {
return fmt.Sprintf("%04x:%02x:%02x.%01x", p.Domain, p.Bus, p.Device, p.Function)
return fmt.Sprintf("%04x:%02x:%02x.%01x", p.domain, p.bus, p.device, p.function)
}
// ResolvePCIRoot resolves the PCI root for the PCIAddress.
@@ -75,7 +154,7 @@ func (p *PCIAddress) ResolvePCIRoot(sysfs Sysfs) (*PCIRoot, error) {
}
pciRootPart := strings.Split(strings.TrimPrefix(sysDevicesPath, sysfs.Devices("")+"/"), "/")[0]
pciRoot, err := parsePCIRoot(
pciRoot, err := ParsePCIRoot(
pciRootPart[3:], // skip "pci" prefix
)
if err != nil {
@@ -120,38 +199,36 @@ func (p *PCIAddress) resolveSysDevicesPath(sysfs Sysfs) (string, error) {
return targetAbs, nil
}
// parsePCIRoot parses a PCIRoot from a string in the format "domain:bus".
// ParsePCIRoot parses a PCIRoot from a string in the format "domain:bus".
// The format is:
// - domain: 4-digit hexadecimal number representing the PCI domain
// - bus: 2-digit hexadecimal number representing the PCI bus
// Example: "0000:01" represents domain 0, bus 1.
func parsePCIRoot(str string) (*PCIRoot, error) {
parts := strings.Split(str, ":")
if len(parts) != 2 {
func ParsePCIRoot(str string) (*PCIRoot, error) {
match := pciRootRegexp.FindStringSubmatch(str)
if len(match) == 0 {
return nil, fmt.Errorf("invalid PCIRoot format: %s", str)
}
parsePart := func(name, part string) (uint32, error) {
value, err := parseHexToUint32(part)
parsePart := func(name, part string) (uint16, error) {
value, err := parseHexTouint16(part)
if err != nil {
return 0, fmt.Errorf("invalid value %s in PCIRoot %s: %w", name, str, err)
}
return uint32(value), nil
return uint16(value), nil
}
var domain, bus uint32
var domain, bus uint16
var err error
if domain, err = parsePart("domain", parts[0]); err != nil {
if domain, err = parsePart("domain", match[1]); err != nil {
return nil, err
}
if bus, err = parsePart("bus", parts[1]); err != nil {
if bus, err = parsePart("bus", match[2]); err != nil {
return nil, err
}
return &PCIRoot{
Domain: domain,
Bus: bus,
}, nil
return NewPCIRoot(domain, bus)
}
// ParsePCIAddress parses a PCI address in BDF notation.
@@ -173,15 +250,15 @@ func ParsePCIAddress(bdfString string) (*PCIAddress, error) {
return nil, fmt.Errorf("invalid PCI address format: %s", bdfString)
}
parsePart := func(name, part string) (uint32, error) {
value, err := parseHexToUint32(part)
parsePart := func(name, part string) (uint16, error) {
value, err := parseHexTouint16(part)
if err != nil {
return 0, fmt.Errorf("invalid value %s in PCI address %s: %w", name, bdfString, err)
}
return uint32(value), nil
return uint16(value), nil
}
var domain, bus, device, function uint32
var domain, bus, device, function uint16
var err error
if domain, err = parsePart("domain", match[1]); err != nil {
return nil, err
@@ -196,18 +273,13 @@ func ParsePCIAddress(bdfString string) (*PCIAddress, error) {
return nil, err
}
return &PCIAddress{
Domain: uint32(domain),
Bus: uint32(bus),
Device: uint32(device),
Function: uint32(function),
}, nil
return NewPCIAddress(domain, bus, device, function)
}
func parseHexToUint32(s string) (uint32, error) {
func parseHexTouint16(s string) (uint16, error) {
value, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, err
}
return uint32(value), nil
return uint16(value), nil
}

View File

@@ -3,20 +3,63 @@ package utils
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
dratesting "k8s.io/dynamic-resource-allocation/testing"
)
func TestNewPCIAddress(t *testing.T) {
tests := map[string]struct {
domain, bus, device, function uint16
expectsErr bool
expectedErrMsg string
}{
"valid zero address": {0, 0, 0, 0, false, ""},
"valid simple address": {1, 2, 3, 4, false, ""},
"valid complex address": {0x1234, 0x56, 0x1e, 0x5, false, ""},
// no invalid domain case because domain uses full 16 bits
"invalid bus": {0, PCIBusMax + 1, 0, 0, true, "invalid PCI bus number"},
"invalid device": {0, 0, PCIDeviceMax + 1, 0, true, "invalid PCI device number"},
"invalid function": {0, 0, 0, PCIFunctionMax + 1, true, "invalid PCI function number"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
address, err := NewPCIAddress(test.domain, test.bus, test.device, test.function)
if test.expectsErr {
if err == nil {
t.Errorf("Expected error but got none")
return
}
if !strings.Contains(err.Error(), test.expectedErrMsg) {
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
return
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if address.domain != test.domain || address.bus != test.bus || address.device != test.device || address.function != test.function {
t.Errorf("Expected PCIAddress(%d, %d, %d, %d), got (%d, %d, %d, %d)",
test.domain, test.bus, test.device, test.function,
address.domain, address.bus, address.device, address.function)
return
}
})
}
}
func TestPCIAddressString(t *testing.T) {
tests := map[string]struct {
address PCIAddress
address *PCIAddress
expected string
}{
"zero": {PCIAddress{Domain: 0, Bus: 0, Device: 0, Function: 0}, "0000:00:00.0"},
"simple": {PCIAddress{Domain: 1, Bus: 2, Device: 3, Function: 4}, "0001:02:03.4"},
"complex": {PCIAddress{Domain: 0x1234, Bus: 0x56, Device: 0x78, Function: 0x9}, "1234:56:78.9"},
"zero": {MustNewPCIAddress(0, 0, 0, 0), "0000:00:00.0"},
"simple": {MustNewPCIAddress(0x0001, 0x02, 0x03, 0x4), "0001:02:03.4"},
"complex": {MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x5), "1234:56:1e.5"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
@@ -29,27 +72,107 @@ func TestPCIAddressString(t *testing.T) {
func TestParsePCIAddress(t *testing.T) {
tests := map[string]struct {
input string
expected *PCIAddress
expectsErr bool
input string
expected *PCIAddress
expectsErr bool
expectedErrMsg string
}{
"zero": {"0000:00:00.0", &PCIAddress{Domain: 0, Bus: 0, Device: 0, Function: 0}, false},
"simple": {"0001:02:03.4", &PCIAddress{Domain: 1, Bus: 2, Device: 3, Function: 4}, false},
"complex": {"1234:56:78.9", &PCIAddress{Domain: 0x1234, Bus: 0x56, Device: 0x78, Function: 0x9}, false},
"invalid format": {"0000:00:00", nil, true},
"too many parts": {"0000:00:00.0.1", nil, true},
"invalid hex value": {"0000:00:00.0a", nil, true},
"completely invalid": {"invalid", nil, true},
"valid zero": {"0000:00:00.0", MustNewPCIAddress(0x0, 0x0, 0x0, 0x0), false, ""},
"valid simple": {"0001:02:03.4", MustNewPCIAddress(0x1, 0x2, 0x3, 0x4), false, ""},
"valid complex": {"1234:56:1e.7", MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x7), false, ""},
"invalid format": {"0000-00-00-0", nil, true, "invalid PCI address forma"},
"too less parts": {"0000:00:00", nil, true, "invalid PCI address forma"},
"too many parts": {"0000:00:00.0.1", nil, true, "invalid PCI address forma"},
"invalid hex value": {"0000:00:00.0g", nil, true, "invalid PCI address format"},
"completely invalid": {"invalid", nil, true, "invalid PCI address forma"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
result, err := ParsePCIAddress(test.input)
if (err != nil) != test.expectsErr {
t.Errorf("ParsePCIAddress(%s) error = %v, wantErr %v", test.input, err, test.expectsErr)
if test.expectsErr {
if err == nil {
t.Errorf("Expected error but got none for input %s", test.input)
return
}
if test.expectedErrMsg != "" && !strings.Contains(err.Error(), test.expectedErrMsg) {
t.Errorf("Expected error message %q contains %q for input %s", err.Error(), test.expectedErrMsg, test.input)
return
}
return
}
if !test.expectsErr && result.String() != test.expected.String() {
t.Errorf("ParsePCIAddress(%s) = %s, want %s", test.input, result.String(), test.expected.String())
if err != nil {
t.Errorf("Unexpected error for input %s: %v", test.input, err)
return
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("ParsePCIAddress(%s) = %+v, want %+v", test.input, result, test.expected)
return
}
})
}
}
func TestParsePCIAddressRoundTrip(t *testing.T) {
tests := map[string]struct {
addressStr string
expected *PCIAddress
}{
"zero": {"0000:00:00.0", MustNewPCIAddress(0x0, 0x0, 0x0, 0x0)},
"simple": {"0001:02:03.4", MustNewPCIAddress(0x1, 0x2, 0x3, 0x4)},
"complex": {"1234:56:1e.5", MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x5)},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
address, err := ParsePCIAddress(test.addressStr)
if err != nil {
t.Errorf("ParsePCIAddress(%s) returned error: %v", test.addressStr, err)
return
}
if !reflect.DeepEqual(address, test.expected) {
t.Errorf("ParsePCIAddress(%s) = %+v, want %+v", test.addressStr, address, test.expected)
return
}
if address.String() != test.expected.String() {
t.Errorf("Expected %s, got %s", test.expected.String(), address.String())
}
})
}
}
func TestNewPCIRoot(t *testing.T) {
tests := map[string]struct {
domain uint16
bus uint16
address *PCIRoot
expectsErr bool
expectedErrMsg string
}{
"valid zero root": {0, 0, MustNewPCIRoot(0x0, 0x0), false, ""},
"valid simple root": {1, 2, MustNewPCIRoot(0x1, 0x2), false, ""},
"valid complex root": {0x1234, 0x56, MustNewPCIRoot(0x1234, 0x56), false, ""},
// no invalid domain case because domain uses full 16 bits
"invalid bus": {0, PCIBusMax + 1, nil, true, "invalid PCI bus number"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
root, err := NewPCIRoot(test.domain, test.bus)
if test.expectsErr {
if err == nil {
t.Errorf("Expected error but got none")
return
}
if !strings.Contains(err.Error(), test.expectedErrMsg) {
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
return
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if root.String() != test.address.String() {
t.Errorf("Expected PCIRoot %s, got %s", test.address.String(), root.String())
return
}
})
@@ -58,12 +181,12 @@ func TestParsePCIAddress(t *testing.T) {
func TestPCIRootString(t *testing.T) {
tests := map[string]struct {
root PCIRoot
root *PCIRoot
expected string
}{
"zero": {PCIRoot{Domain: 0, Bus: 0}, "0000:00"},
"simple": {PCIRoot{Domain: 1, Bus: 2}, "0001:02"},
"complex": {PCIRoot{Domain: 0x1234, Bus: 0x56}, "1234:56"},
"zero": {MustNewPCIRoot(0x0, 0x0), "0000:00"},
"simple": {MustNewPCIRoot(0x1, 0x2), "0001:02"},
"complex": {MustNewPCIRoot(0x1234, 0x56), "1234:56"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
@@ -74,17 +197,78 @@ func TestPCIRootString(t *testing.T) {
}
}
func TestParsePCIRoot(t *testing.T) {
tests := map[string]struct {
input string
expected *PCIRoot
expectsErr bool
expectedErrMsg string
}{
"valid zero": {"0000:00", MustNewPCIRoot(0x0, 0x0), false, ""},
"valid simple": {"0001:02", MustNewPCIRoot(0x1, 0x2), false, ""},
"valid complex": {"1234:56", MustNewPCIRoot(0x1234, 0x56), false, ""},
"invalid format": {"0000-00", nil, true, "invalid PCIRoot format"},
"invalid too less parts": {"0000", nil, true, "invalid PCIRoot format"},
"invalid too many parts": {"0000:00:00", nil, true, "invalid PCIRoot format"},
"invalid hex value": {"0000:0g", nil, true, "invalid PCIRoot format"},
"invalid completely invalid": {"invalid", nil, true, "invalid PCIRoot format"},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
result, err := ParsePCIRoot(test.input)
if test.expectsErr {
if err == nil {
t.Errorf("Expected error but got none")
return
}
if test.expectedErrMsg != "" && !strings.Contains(err.Error(), test.expectedErrMsg) {
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
return
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("Expected PCIRoot %s, got %s", test.expected.String(), result.String())
return
}
})
}
}
func TestParsePCIRootRoundTrip(t *testing.T) {
tests := map[string]struct {
rootStr string
expected *PCIRoot
}{
"zero": {"0000:00", MustNewPCIRoot(0x0, 0x0)},
"simple": {"0001:02", MustNewPCIRoot(0x1, 0x2)},
"complex": {"1234:56", MustNewPCIRoot(0x1234, 0x56)},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
root, err := ParsePCIRoot(test.rootStr)
if err != nil {
t.Errorf("ParsePCIRoot(%s) returned error: %v", test.rootStr, err)
return
}
if !reflect.DeepEqual(root, test.expected) {
t.Errorf("ParsePCIRoot(%s) = %+v, want %+v", test.rootStr, root, test.expected)
return
}
if root.String() != test.expected.String() {
t.Errorf("Expected %s, got %s", test.expected.String(), root.String())
}
})
}
}
func TestPCIAddressResolvePCIRoot(t *testing.T) {
address := PCIAddress{
Domain: 0x1234,
Bus: 0x56,
Device: 0x78,
Function: 0x9,
}
root := PCIRoot{
Domain: 0x2345,
Bus: 0x67,
}
address := MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x7)
root := MustNewPCIRoot(0x2345, 0x67)
tests := map[string]struct {
sysfs func(*testing.T, string) Sysfs