Skip to content

Conversation

@stgraber
Copy link
Member

No description provided.

@mschiff
Copy link

mschiff commented Nov 12, 2025

Hi @stgraber, thanks for that!

Unfortunately I think this wont work, as

  1. multiple cats need to be seperated by comma (,)
  2. you cannot have the same cat twice, so ID 111111 or 111111111 would both use the same cat as ID 111 (c111)
  3. cats must not have leading zeros
  4. cats should be sorted because the order does not matter, so id 123456 would use the same cats as 456123

I will apply your change, fix the issues, test it and then paste the result here.

@stgraber
Copy link
Member Author

Okay, this stuff is pretty frustrating.

We need to be able to run around 20k instances per system without any chance of two getting the same categories.

Picking two random numbers doesn't prevent them from being the same nor does it prevent clashes with other instances on the range system.

The best would be to be able to encode the instance ID as categories but that needs to support a full 64 bit integer.

The alternative would need to be looking at running instances on the system, allocating the next unused category on startup with some kind of global lock going over all the instances, that won't be particularly fast but may be the best bet here.

@mschiff
Copy link

mschiff commented Nov 13, 2025

The following is working for me. It uses 4 cats based on the instance ID. With that it should be safe to run 20.000+ instances.
The collision propabilty is ~0.44% with 20k instances running at once.

diff '--color=auto' -ur incus-orig/internal/server/instance/drivers/driver_common.go incus/internal/server/instance/drivers/driver_common.go                    02:25:46 [16/8029]
--- incus-orig/internal/server/instance/drivers/driver_common.go        2025-11-12 22:46:55.395055647 +0100
+++ incus/internal/server/instance/drivers/driver_common.go     2025-11-13 02:19:17.576129571 +0100
@@ -3,6 +3,8 @@                                                                          
 import (
        "cmp"                                                                                                                                                                     
        "context"                                                                                                                                                                 
+       "crypto/sha256"                                                                                                                                                           
+       "encoding/binary"
        "errors"
        "fmt"            
        "math"                                                                           
@@ -1726,3 +1728,46 @@                                                                                                                                                            
                                                                                                                                                                                  
        return nil             
 }
+                                                                                                                                                                                 
+// selinuxCategory returns the SELinux category suffix.
+func (d *common) selinuxCategory() string { 
+       h := sha256.New()
+       binary.Write(h, binary.LittleEndian, int64(d.id))
+       hash := h.Sum(nil)
+
+       // Extract three 32-bit values from hash
+       v1 := binary.LittleEndian.Uint32(hash[0:4])
+       v2 := binary.LittleEndian.Uint32(hash[4:8])
+       v3 := binary.LittleEndian.Uint32(hash[8:12])
+       v4 := binary.LittleEndian.Uint32(hash[12:16])
+
+       // Map to category range [0, 1023]
+       c1 := int(v1 % 1024)
+       c2 := int(v2 % 1024)
+       c3 := int(v3 % 1024)
+       c4 := int(v4 % 1024)
+
+       // Make c2 distinct from c1
+       for c2 == c1 {
+               c2 = (c2 + 1) % 1024
+       }
+
+       // Make c3 distinct from both c1 and c2
+       for c3 == c1 || c3 == c2 {
+               c3 = (c3 + 1) % 1024
+       }
+
+       // Make c4 distinct from c1, c2, and c3
+       for c4 == c1 || c4 == c2 || c4 == c3 {
+               c4 = (c4 + 1) % 1024
+       }
+
+       // Sort for canonical ordering
+       cats := []int{c1, c2, c3, c4}
+       sort.Ints(cats)
+
+       return ":c" + strconv.Itoa(cats[0]) + 
+                  ",c" + strconv.Itoa(cats[1]) + 
+                  ",c" + strconv.Itoa(cats[2]) +
+                  ",c" + strconv.Itoa(cats[3])
+}
diff '--color=auto' -ur incus-orig/internal/server/instance/drivers/driver_lxc.go incus/internal/server/instance/drivers/driver_lxc.go
--- incus-orig/internal/server/instance/drivers/driver_lxc.go   2025-11-12 22:46:55.398389023 +0100
+++ incus/internal/server/instance/drivers/driver_lxc.go        2025-11-12 22:44:01.919598771 +0100
@@ -1022,7 +1022,7 @@
  
        // Setup SELinux.
        if d.state.OS.SELinuxAvailable && d.state.OS.SELinuxContextInstanceLXC != "" {
-               err := lxcSetConfigItem(cc, "lxc.selinux.context", fmt.Sprintf("%s:c%d", d.state.OS.SELinuxContextInstanceLXC, d.id))
+               err := lxcSetConfigItem(cc, "lxc.selinux.context", fmt.Sprintf("%s%s", d.state.OS.SELinuxContextInstanceLXC, d.selinuxCategory()))
                if err != nil {
                        return nil, err
                }

@stgraber
Copy link
Member Author

The following is working for me. It uses 4 cats based on the instance ID. With that it should be safe to run 20.000+ instances. The collision propabilty is ~0.44% with 20k instances running at once.

diff '--color=auto' -ur incus-orig/internal/server/instance/drivers/driver_common.go incus/internal/server/instance/drivers/driver_common.go                    02:25:46 [16/8029]
--- incus-orig/internal/server/instance/drivers/driver_common.go        2025-11-12 22:46:55.395055647 +0100
+++ incus/internal/server/instance/drivers/driver_common.go     2025-11-13 02:19:17.576129571 +0100
@@ -3,6 +3,8 @@                                                                          
 import (
        "cmp"                                                                                                                                                                     
        "context"                                                                                                                                                                 
+       "crypto/sha256"                                                                                                                                                           
+       "encoding/binary"
        "errors"
        "fmt"            
        "math"                                                                           
@@ -1726,3 +1728,46 @@                                                                                                                                                            
                                                                                                                                                                                  
        return nil             
 }
+                                                                                                                                                                                 
+// selinuxCategory returns the SELinux category suffix.
+func (d *common) selinuxCategory() string { 
+       h := sha256.New()
+       binary.Write(h, binary.LittleEndian, int64(d.id))
+       hash := h.Sum(nil)
+
+       // Extract three 32-bit values from hash
+       v1 := binary.LittleEndian.Uint32(hash[0:4])
+       v2 := binary.LittleEndian.Uint32(hash[4:8])
+       v3 := binary.LittleEndian.Uint32(hash[8:12])
+       v4 := binary.LittleEndian.Uint32(hash[12:16])
+
+       // Map to category range [0, 1023]
+       c1 := int(v1 % 1024)
+       c2 := int(v2 % 1024)
+       c3 := int(v3 % 1024)
+       c4 := int(v4 % 1024)
+
+       // Make c2 distinct from c1
+       for c2 == c1 {
+               c2 = (c2 + 1) % 1024
+       }
+
+       // Make c3 distinct from both c1 and c2
+       for c3 == c1 || c3 == c2 {
+               c3 = (c3 + 1) % 1024
+       }
+
+       // Make c4 distinct from c1, c2, and c3
+       for c4 == c1 || c4 == c2 || c4 == c3 {
+               c4 = (c4 + 1) % 1024
+       }
+
+       // Sort for canonical ordering
+       cats := []int{c1, c2, c3, c4}
+       sort.Ints(cats)
+
+       return ":c" + strconv.Itoa(cats[0]) + 
+                  ",c" + strconv.Itoa(cats[1]) + 
+                  ",c" + strconv.Itoa(cats[2]) +
+                  ",c" + strconv.Itoa(cats[3])
+}
diff '--color=auto' -ur incus-orig/internal/server/instance/drivers/driver_lxc.go incus/internal/server/instance/drivers/driver_lxc.go
--- incus-orig/internal/server/instance/drivers/driver_lxc.go   2025-11-12 22:46:55.398389023 +0100
+++ incus/internal/server/instance/drivers/driver_lxc.go        2025-11-12 22:44:01.919598771 +0100
@@ -1022,7 +1022,7 @@
  
        // Setup SELinux.
        if d.state.OS.SELinuxAvailable && d.state.OS.SELinuxContextInstanceLXC != "" {
-               err := lxcSetConfigItem(cc, "lxc.selinux.context", fmt.Sprintf("%s:c%d", d.state.OS.SELinuxContextInstanceLXC, d.id))
+               err := lxcSetConfigItem(cc, "lxc.selinux.context", fmt.Sprintf("%s%s", d.state.OS.SELinuxContextInstanceLXC, d.selinuxCategory()))
                if err != nil {
                        return nil, err
                }

A collision risk of > 0% is a security issue for us as users can create and delete instances as many time as they want until they manage to hit what a collision.

@stgraber stgraber changed the title incusd/instance/lxc: Tweak seccomp category incusd/instance/lxc: Tweak SELinux category Nov 13, 2025
@stgraber
Copy link
Member Author

So I think we should indeed work with a combination of two categories as that will give us more instances than any system can ever run at any one time.

Then we should have a function which will:

  • Load all running local container instances
  • Track down their current SELinux categories and put them in a slice
  • Generate a random pair of categories
  • Check that the new pair isn't already in use, if it is, generate another
  • Return that pair of category for use by the instance

We'll have that function take a global lock so we can limit the load it causes and avoid a race at that stage.

@mschiff
Copy link

mschiff commented Nov 13, 2025

If you can give me a hint about how to get that list of instances and how to take the lock I would change the function to do what you suggested. And maybe it would be good if I put that in a seperate PR then? Or can I somehow push into this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants