-
Notifications
You must be signed in to change notification settings - Fork 52
feat(mirage): add dynamic device naming via manifest and annotations #363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -390,3 +390,4 @@ Liaskovitis | |
| Mallikopoulou | ||
| Medfouni | ||
| Khitem | ||
| namesz | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,89 +15,312 @@ | |
| package unikernels | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "debug/elf" | ||
| "encoding/binary" | ||
| "encoding/json" | ||
| "fmt" | ||
| "regexp" | ||
| "strings" | ||
|
|
||
| "github.com/urunc-dev/urunc/pkg/unikontainers/types" | ||
| ) | ||
|
|
||
| const MirageUnikernel string = "mirage" | ||
| const ( | ||
| // MirageUnikernel is the unikernel type for MirageOS | ||
| MirageUnikernel string = "mirage" | ||
| // AnnotationNetMap is the annotation key for network device mapping | ||
| AnnotationNetMap string = "urunc.dev/mirage-net-map" | ||
| // AnnotationBlockMap is the annotation key for block device mapping | ||
| AnnotationBlockMap string = "urunc.dev/mirage-block-map" | ||
| ) | ||
|
|
||
| // Constants from solo5 mft_abi.h | ||
| const ( | ||
| mftDevBlockBasic = 0 | ||
| mftDevNetBasic = 1 | ||
| mftNameMax = 64 | ||
| ) | ||
|
|
||
| // Regular expression to validate device names (alphanumeric + underscore) | ||
| var validDeviceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) | ||
|
|
||
| // Mirage represents a MirageOS unikernel configuration | ||
| type Mirage struct { | ||
| Command string | ||
| Monitor string | ||
| Net MirageNet | ||
| Block []MirageBlock | ||
| Command string | ||
| Monitor string | ||
| Net MirageNet | ||
| Block []MirageBlock | ||
| BinaryPath string | ||
| Manifest *Solo5Manifest | ||
| Annotations map[string]string | ||
| } | ||
|
|
||
| // Solo5Manifest represents the parsed devices from the unikernel binary | ||
| type Solo5Manifest struct { | ||
| Devices []Solo5Device | ||
| } | ||
|
|
||
| // Solo5Device represents a single device entry in the manifest | ||
| type Solo5Device struct { | ||
| Name string | ||
| Type string | ||
| } | ||
|
|
||
| // MftHeader matches the 12-byte header found in the binary. | ||
| // 0x00: Pad/Reserved (4) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The padding is <4 and depends on the note owner and such. I don't think this is right. |
||
| // 0x04: Version (4) | ||
| // 0x08: Entries (4) | ||
| type MftHeader struct { | ||
| Pad uint32 | ||
| Version uint32 | ||
| Entries uint32 | ||
| } | ||
|
|
||
| // MftEntry matches the 104-byte stride. | ||
| // Name (65) + Pad (7) = 72 | ||
| // Type (8) = 80 | ||
| // Flags (8) = 88 | ||
| // Pad2 (16) = 104 | ||
| // NOTE: All fields MUST be exported (Capitalized) for binary.Read to work. | ||
| type MftEntry struct { | ||
| Name [mftNameMax + 1]byte | ||
| Pad [7]byte // Exported to avoid panic in binary.Read | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No padding |
||
| Type uint64 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type is an int32 |
||
| Flags uint64 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is |
||
| Pad2 [16]byte // Exported to avoid panic in binary.Read | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ??? |
||
| } | ||
|
|
||
| // MirageNet holds network configuration | ||
| type MirageNet struct { | ||
| Address string | ||
| Gateway string | ||
| } | ||
|
|
||
| // MirageBlock holds block device configuration | ||
| type MirageBlock struct { | ||
| ID string | ||
| HostPath string | ||
| } | ||
|
|
||
| // CommandString returns the command line arguments for the unikernel | ||
| func (m *Mirage) CommandString() (string, error) { | ||
| return fmt.Sprintf("%s %s %s", m.Net.Address, | ||
| m.Net.Gateway, | ||
| m.Command), nil | ||
| } | ||
|
|
||
| // SupportsBlock returns true as Mirage supports block devices | ||
| func (m *Mirage) SupportsBlock() bool { | ||
| return true | ||
| } | ||
|
|
||
| // SupportsFS returns false as Mirage does not support filesystem passthrough | ||
| func (m *Mirage) SupportsFS(_ string) bool { | ||
| return false | ||
| } | ||
|
|
||
| // MonitorNetCli returns the network arguments for the monitor | ||
| func (m *Mirage) MonitorNetCli(ifName string, mac string) string { | ||
| switch m.Monitor { | ||
| case "hvt", "spt": | ||
| netOption := "--net:service=" + ifName | ||
| netOption += " --net-mac:service=" + mac | ||
| mirageID := m.getMirageDeviceName(ifName, "NET_BASIC", "service") | ||
|
|
||
| netOption := fmt.Sprintf("--net:%s=%s", mirageID, ifName) | ||
| netOption += fmt.Sprintf(" --net-mac:%s=%s", mirageID, mac) | ||
|
Comment on lines
+127
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what the security model is, but you may consider enforcing the constraint that device names are alphanumerical (and at most a fixed length which I don't remember). Otherwise you can inject arbitrary strings here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated getMirageDeviceName to enforce strict validation: Alphanumeric Check: The resolved name is validated against a regex (^[a-zA-Z0-9_]+$). Fallback: If the name contains invalid characters, it safely falls back to the default name (e.g., storage or service) rather than passing the raw string. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, it doesn't make sense to add a fallback. If the name is invalid then solo5-hvt/solo5-spt will error out parsing the ELF. Even if the ELF was not invalid solo5-hvt/solo5-spt will error out because you just made up a device name that doesn't match the manifest. |
||
| return netOption | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| // MonitorBlockCli returns the block device arguments for the monitor | ||
| func (m *Mirage) MonitorBlockCli() []types.MonitorBlockArgs { | ||
| if len(m.Block) == 0 { | ||
| return nil | ||
| } | ||
| switch m.Monitor { | ||
| case "hvt", "spt": | ||
| // TODO: Explore options for multiple block devices in MirageOS | ||
| // over Solo5-spt and Solo5-hvt. Solo5 expects to use as an ID | ||
| // a specific name which the guest is also aware of in order to | ||
| // attach the respective block. As a result, urunc needs to know | ||
| // the correct ID to set, which is not straightforward. Therefore, | ||
| // there are two options. Either we read the Solo5 manifest or, | ||
| // we require specific IDs. Till we decide about that, we will | ||
| // use a single block device. We also need to find some use cases | ||
| // where multiple block devices are configured in MirageOS and check | ||
| // how MirageOS handles/configures them. | ||
| return []types.MonitorBlockArgs{ | ||
| { | ||
| ID: "storage", | ||
| Path: m.Block[0].HostPath, | ||
| }, | ||
| var blockArgs []types.MonitorBlockArgs | ||
|
|
||
| for i, blk := range m.Block { | ||
| defaultName := "storage" | ||
| if i > 0 { | ||
| defaultName = fmt.Sprintf("storage%d", i) | ||
| } | ||
|
|
||
| mirageID := m.getMirageDeviceName(blk.ID, "BLOCK_BASIC", defaultName) | ||
|
|
||
| blockArgs = append(blockArgs, types.MonitorBlockArgs{ | ||
| ID: mirageID, | ||
| Path: blk.HostPath, | ||
| }) | ||
| } | ||
| return blockArgs | ||
| default: | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // MonitorCli returns general monitor arguments | ||
| func (m *Mirage) MonitorCli() types.MonitorCliArgs { | ||
| return types.MonitorCliArgs{} | ||
| } | ||
|
|
||
| // getMirageDeviceName determines the correct Solo5 interface name. | ||
| func (m *Mirage) getMirageDeviceName(hostDev string, devType string, defaultName string) string { | ||
| resolvedName := "" | ||
|
|
||
| // 1. Check Annotations first | ||
| var mapKey string | ||
| if devType == "NET_BASIC" { | ||
| mapKey = AnnotationNetMap | ||
| } else { | ||
| mapKey = AnnotationBlockMap | ||
| } | ||
|
|
||
| if val, ok := m.Annotations[mapKey]; ok { | ||
| var mapping map[string]string | ||
| if err := json.Unmarshal([]byte(val), &mapping); err == nil { | ||
| if mirageName, found := mapping[hostDev]; found { | ||
| resolvedName = mirageName | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 2. If not found in annotations, check Manifest (Auto-detect if only one device exists) | ||
| if resolvedName == "" && m.Manifest != nil { | ||
| var validDevs []string | ||
| for _, dev := range m.Manifest.Devices { | ||
| if dev.Type == devType { | ||
| validDevs = append(validDevs, dev.Name) | ||
| } | ||
| } | ||
| if len(validDevs) == 1 { | ||
| resolvedName = validDevs[0] | ||
| } | ||
| } | ||
|
|
||
| // 3. Fallback | ||
| if resolvedName == "" { | ||
| resolvedName = defaultName | ||
| } | ||
|
|
||
| // Security check: ensure the name is safe | ||
| if !validDeviceName.MatchString(resolvedName) { | ||
| // Log warning here if logger is available | ||
| return defaultName | ||
| } | ||
| return resolvedName | ||
| } | ||
|
|
||
| func parseSolo5ManifestData(data []byte) (*Solo5Manifest, error) { | ||
| // Offset logic based on binary analysis: | ||
| // 0x00-0x13: ELF Note Header | ||
| // 0x14: Start of MftHeader (Pad=0, Ver=1, Ent=2) | ||
|
|
||
| var offset int64 | ||
|
|
||
| // Heuristic to skip ELF Note Header | ||
| if len(data) > 12 { | ||
| var namesz uint32 | ||
| // FIX: Checked error return value (errcheck warning) | ||
| if err := binary.Read(bytes.NewReader(data[0:4]), binary.LittleEndian, &namesz); err != nil { | ||
| // If we can't even read 4 bytes, just ignore this check and use offset 0 | ||
| } else { | ||
| // If namesz is 6 ("Solo5\0"), skip header. | ||
| // Header = 12 + aligned(namesz) | ||
| if namesz == 6 { | ||
| offset = int64(12 + ((namesz + 3) &^ 3)) | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+224
to
+237
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this. Does the ELF library do this or not? It doesn't make sense to maybe do this to me. What is the heuristic exactly? |
||
|
|
||
| r := bytes.NewReader(data) | ||
| if _, err := r.Seek(offset, 0); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var header MftHeader | ||
| if err := binary.Read(r, binary.LittleEndian, &header); err != nil { | ||
| return nil, fmt.Errorf("failed to read mft header: %v", err) | ||
| } | ||
|
|
||
| // Sanity Check | ||
| if header.Version != 1 { | ||
| return nil, fmt.Errorf("invalid manifest version: %d (expected 1)", header.Version) | ||
| } | ||
|
Comment on lines
+249
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should check that the number of entries is at least 1 as there is always the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also check entries is less than or equal to |
||
|
|
||
| manifest := &Solo5Manifest{} | ||
|
|
||
| for i := 0; i < int(header.Entries); i++ { | ||
| var entry MftEntry | ||
| if err := binary.Read(r, binary.LittleEndian, &entry); err != nil { | ||
| break | ||
| } | ||
|
|
||
| // Clean C-String (remove null bytes) | ||
| nameLen := bytes.IndexByte(entry.Name[:], 0) | ||
| if nameLen < 0 { | ||
| nameLen = len(entry.Name) | ||
| } | ||
| cleanName := string(entry.Name[:nameLen]) | ||
|
|
||
| // Skip empty names (often internal devices) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are no such thing as internal devices. Empty names are disallowed. |
||
| if cleanName == "" { | ||
| continue | ||
| } | ||
|
|
||
| var devType string | ||
| switch entry.Type { | ||
|
|
||
| case mftDevNetBasic: | ||
| devType = "NET_BASIC" | ||
| case mftDevBlockBasic: | ||
| devType = "BLOCK_BASIC" | ||
| default: | ||
| devType = fmt.Sprintf("UNKNOWN_%d", entry.Type) | ||
| } | ||
|
Comment on lines
+275
to
+283
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You forgot the |
||
|
|
||
| manifest.Devices = append(manifest.Devices, Solo5Device{ | ||
| Name: cleanName, | ||
| Type: devType, | ||
| }) | ||
| } | ||
|
|
||
| return manifest, nil | ||
| } | ||
|
|
||
| // getSolo5Manifest extracts the .note.solo5.manifest section from the ELF | ||
| func getSolo5Manifest(path string) (*Solo5Manifest, error) { | ||
| f, err := elf.Open(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| section := f.Section(".note.solo5.manifest") | ||
| if section == nil { | ||
| return nil, fmt.Errorf("no solo5 manifest section found") | ||
| } | ||
|
|
||
| data, err := section.Data() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return parseSolo5ManifestData(data) | ||
| } | ||
|
|
||
| // Init initializes the Mirage configuration | ||
| func (m *Mirage) Init(data types.UnikernelParams) error { | ||
| // if Mask is empty, there is no network support | ||
|
|
||
| m.BinaryPath = data.UnikernelPath | ||
| m.Annotations = data.Annotations | ||
| if manifest, err := getSolo5Manifest(m.BinaryPath); err == nil { | ||
| m.Manifest = manifest | ||
| } | ||
|
|
||
| if data.Net.Mask != "" { | ||
| m.Net.Address = "--ipv4=" + data.Net.IP + "/24" | ||
| m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure where you got the number 64. It's not right.