Repository: osbuild/osbuild-composer
Pull request #2865 (2022-07-28 15:58:16)

Allow for `/boot` to be customized
The interesting commit is the last one, with the commit message:

Since the LVM support was added to all distros, our disk related code is adaptive, i.e. we will set the correct BLS and grub2 prefix if there a `boot` partiton is present in the layout after all customizations happen, which includes LVMification. One thing that was not yet fully working was layouts that do not yet have a `/boot` partition but allow LVMification. In that case `NewPartitionTable` and if `/boot` was the first (or only) customization, would LVMify the partition which in turn would create the `/boot` partition; but after `newPT.ensureLVM()` the call to `newPT.createFilesystem` with `/boot` would try to create another `/boot` mountpoint. In order to deal with this situation correctly we are now using a two phase approach: 1) enlarge existing mountpoints and collect new ones. 2) if there are new ones and LMVify was allowed, switch to LVM layout. Do a second pass and now create or enlarge existing partitions, handling `/boot` in the process.

gicmo: I have added a simple trie data structure implementation and converted the mount point allow list to it in order to allow `/boot` but none if its sub-directories. Additionally, the customisation code is now extracted into a new helper method `applyCustomizations` and re-used between the two passes.

Patch:
diff --git a/internal/disk/disk.go b/internal/disk/disk.go
index 6a6f0a38d..85680feb3 100644
--- a/internal/disk/disk.go
+++ b/internal/disk/disk.go
@@ -18,10 +18,12 @@ package disk
 
 import (
 	"encoding/hex"
+	"fmt"
 	"io"
 	"math/rand"
 
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
 )
 
 const (
@@ -50,6 +52,19 @@ const (
 	XBootLDRPartitionGUID = "BC13C2FF-59E6-4262-A352-B275FD6F7172"
 )
 
+var MountpointPolicies = NewPathPolicies(map[string]PathPolicy{
+	"/":     {Exact: true},
+	"/boot": {Exact: true},
+	"/var":  {},
+	"/opt":  {},
+	"/srv":  {},
+	"/usr":  {},
+	"/app":  {},
+	"/data": {},
+	"/home": {},
+	"/tmp":  {},
+})
+
 // Entity is the base interface for all disk-related entities.
 type Entity interface {
 	// IsContainer indicates if the implementing type can
@@ -164,3 +179,19 @@ func NewVolIDFromRand(r *rand.Rand) string {
 	}
 	return hex.EncodeToString(volid)
 }
+
+func CheckMountpoints(mountpoints []blueprint.FilesystemCustomization, mountpointAllowList *PathPolicies) error {
+	invalidMountpoints := []string{}
+	for _, m := range mountpoints {
+		err := mountpointAllowList.Check(m.Mountpoint)
+		if err != nil {
+			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
+		}
+	}
+
+	if len(invalidMountpoints) > 0 {
+		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	}
+
+	return nil
+}
diff --git a/internal/disk/partition_table.go b/internal/disk/partition_table.go
index 93677ac5f..3c12bad1b 100644
--- a/internal/disk/partition_table.go
+++ b/internal/disk/partition_table.go
@@ -21,23 +21,25 @@ type PartitionTable struct {
 
 func NewPartitionTable(basePT *PartitionTable, mountpoints []blueprint.FilesystemCustomization, imageSize uint64, lvmify bool, rng *rand.Rand) (*PartitionTable, error) {
 	newPT := basePT.Clone().(*PartitionTable)
-	for _, mnt := range mountpoints {
-		size := newPT.AlignUp(clampFSSize(mnt.Mountpoint, mnt.MinSize))
-		if path := entityPath(newPT, mnt.Mountpoint); len(path) != 0 {
-			resizeEntityBranch(path, size)
-		} else {
-			if lvmify {
-				err := newPT.ensureLVM()
-				if err != nil {
-					return nil, err
-				}
-			}
-			if err := newPT.createFilesystem(mnt.Mountpoint, size); err != nil {
-				return nil, err
-			}
+
+	// first pass: enlarge existing mountpoints and collect new ones
+	newMountpoints, _ := newPT.applyCustomization(mountpoints, false)
+
+	// if there is any new mountpoint and lvmify is enabled, ensure we have LVM layout
+	if lvmify && len(newMountpoints) > 0 {
+		err := newPT.ensureLVM()
+		if err != nil {
+			return nil, err
 		}
 	}
 
+	// second pass: deal with new mountpoints and newly created ones, after switching to
+	// the LVM layout, if requested, which might introduce new mount points, i.e. `/boot`
+	_, err := newPT.applyCustomization(newMountpoints, true)
+	if err != nil {
+		return nil, err
+	}
+
 	// TODO: make these overrideable for each image type
 	newPT.EnsureDirectorySizes(map[string]uint64{
 		"/":    1073741824,
@@ -312,6 +314,31 @@ func (pt *PartitionTable) HeaderSize() uint64 {
 	return header
 }
 
+// Apply filesystem filesystem customization to the partiton table. If create is false
+// will only apply customizations to existing partitions and return unhandled, i.e new
+// ones. An error can only occur if create is set. Conversely, it will only return non
+// empty list of new mountpoints if create is false.
+// Does not relayout the table, i.e. a call to relayout might be needed.
+func (pt *PartitionTable) applyCustomization(mountpoints []blueprint.FilesystemCustomization, create bool) ([]blueprint.FilesystemCustomization, error) {
+
+	newMountpoints := []blueprint.FilesystemCustomization{}
+
+	for _, mnt := range mountpoints {
+		size := pt.AlignUp(clampFSSize(mnt.Mountpoint, mnt.MinSize))
+		if path := entityPath(pt, mnt.Mountpoint); len(path) != 0 {
+			resizeEntityBranch(path, size)
+		} else {
+			if !create {
+				newMountpoints = append(newMountpoints, mnt)
+			} else if err := pt.createFilesystem(mnt.Mountpoint, size); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return newMountpoints, nil
+}
+
 // Dynamically calculate and update the start point for each of the existing
 // partitions. Adjusts the overall size of image to either the supplied
 // value in `size` or to the sum of all partitions if that is lager.
@@ -467,7 +494,13 @@ func (pt *PartitionTable) FindMountable(mountpoint string) Mountable {
 
 func clampFSSize(mountpoint string, size uint64) uint64 {
 	// set a minimum size of 1GB for all mountpoints
+	// with the exception for '/boot' (= 500 MB)
 	var minSize uint64 = 1073741824
+
+	if mountpoint == "/boot" {
+		minSize = 524288000
+	}
+
 	if minSize > size {
 		return minSize
 	}
diff --git a/internal/disk/path_policy.go b/internal/disk/path_policy.go
new file mode 100644
index 000000000..d1383381f
--- /dev/null
+++ b/internal/disk/path_policy.go
@@ -0,0 +1,54 @@
+package disk
+
+import (
+	"fmt"
+	"path"
+)
+
+type PathPolicy struct {
+	Deny  bool // explicitly do not allow this entry
+	Exact bool // require and exact match, no subdirs
+}
+
+type PathPolicies = PathTrie
+
+// Create a new PathPolicies trie from a map of path to PathPolicy
+func NewPathPolicies(entries map[string]PathPolicy) *PathPolicies {
+
+	noType := make(map[string]interface{}, len(entries))
+
+	for k, v := range entries {
+		noType[k] = v
+	}
+
+	return NewPathTrieFromMap(noType)
+}
+
+// Check a given path at dir against the PathPolicies
+func (pol *PathPolicies) Check(dir string) error {
+
+	// Quickly check we have a mountpoint and it is absolute
+	if dir == "" || dir[0] != '/' {
+		return fmt.Errorf("mountpoint must be absolute path")
+	}
+
+	// ensure that only clean mountpoints are valid
+	if dir != path.Clean(dir) {
+		return fmt.Errorf("mountpoint must be a canonical path")
+	}
+
+	node, left := pol.Lookup(dir)
+	policy, ok := node.Payload.(PathPolicy)
+	if !ok {
+		panic("programming error: invalid path trie payload")
+	}
+
+	// 1) path is explicitly not allowed or
+	// 2) a subpath was match but an explicit match is required
+	if policy.Deny || (policy.Exact && len(left) > 0) {
+		return fmt.Errorf("path '%s ' is not allowed", dir)
+	}
+
+	// exact match or recursive mountpoints allowed
+	return nil
+}
diff --git a/internal/disk/path_trie.go b/internal/disk/path_trie.go
new file mode 100644
index 000000000..1d43df3f7
--- /dev/null
+++ b/internal/disk/path_trie.go
@@ -0,0 +1,123 @@
+package disk
+
+import (
+	"sort"
+	"strings"
+)
+
+// splits the path into its individual components. Retruns the
+// empty list if the path is just the absolute root, i.e. "/".
+func pathTrieSplitPath(path string) []string {
+	path = strings.Trim(path, "/")
+	if path == "" {
+		return []string{}
+	}
+
+	return strings.Split(path, "/")
+}
+
+type PathTrie struct {
+	Name    []string
+	Paths   []*PathTrie
+	Payload interface{}
+}
+
+// match checks if the given trie is a prefix of path
+func (trie *PathTrie) match(path []string) bool {
+	if len(trie.Name) > len(path) {
+		return false
+	}
+
+	for i := range trie.Name {
+		if path[i] != trie.Name[i] {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (trie *PathTrie) get(path []string) (*PathTrie, []string) {
+	if len(path) < 1 {
+		panic("programming error: expected root node")
+	}
+
+	var node *PathTrie
+	for i := range trie.Paths {
+		if trie.Paths[i].match(path) {
+			node = trie.Paths[i]
+			break
+		}
+	}
+
+	// no subpath match, we are the best match
+	if node == nil {
+		return trie, path
+	}
+
+	// node, or one of its sub-nodes, is a match
+	prefix := len(node.Name)
+
+	// the node is a perfect match, return it
+	if len(path) == prefix {
+		return node, nil
+	}
+
+	// check if any sub-path's of node match
+	return node.get(path[prefix:])
+}
+
+func (trie *PathTrie) add(path []string) *PathTrie {
+	node := &PathTrie{Name: path}
+
+	if trie.Paths == nil {
+		trie.Paths = make([]*PathTrie, 0, 1)
+	}
+
+	trie.Paths = append(trie.Paths, node)
+
+	return node
+}
+
+// Construct a new trie from a map of paths to their payloads.
+// Returns the root node of the trie.
+func NewPathTrieFromMap(entries map[string]interface{}) *PathTrie {
+	root := &PathTrie{Name: []string{}}
+
+	keys := make([]string, 0, len(entries))
+	for k := range entries {
+		keys = append(keys, k)
+	}
+
+	sort.Strings(keys)
+
+	for _, k := range keys {
+		node, left := root.Lookup(k)
+
+		if len(left) > 0 {
+			node = node.add(left)
+		}
+
+		node.Payload = entries[k]
+	}
+
+	return root
+}
+
+// Lookup returns the node that is the prefix of path and
+// the unmatched path segment. Must be called on the root
+// trie node.
+func (root *PathTrie) Lookup(path string) (*PathTrie, []string) {
+
+	if len(root.Name) != 0 {
+		panic("programming error: lookup on non-root trie node")
+	}
+
+	elements := pathTrieSplitPath(path)
+
+	if len(elements) == 0 {
+		return root, elements
+	}
+
+	return root.get(elements)
+}
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 4d629450a..d27973bfe 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -3,8 +3,6 @@ package distro
 import (
 	"encoding/json"
 	"fmt"
-	"path"
-	"strings"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/container"
@@ -260,22 +258,3 @@ func MakePackageSetChains(t ImageType, packageSets map[string]rpmmd.PackageSet,
 
 	return chainedSets
 }
-
-func IsMountpointAllowed(mountpoint string, allowlist []string) bool {
-	for _, allowed := range allowlist {
-		match, _ := path.Match(allowed, mountpoint)
-		if match {
-			return true
-		}
-		// ensure that only clean mountpoints
-		// are valid
-		if strings.Contains(mountpoint, "//") {
-			return false
-		}
-		match = strings.HasPrefix(mountpoint, allowed+"/")
-		if allowed != "/" && match {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/distro/fedora/distro.go b/internal/distro/fedora/distro.go
index 56affff1d..df1be4ba6 100644
--- a/internal/distro/fedora/distro.go
+++ b/internal/distro/fedora/distro.go
@@ -46,10 +46,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	oscapProfileAllowList = []oscap.Profile{
 		oscap.Ospp,
 		oscap.PciDss,
@@ -700,15 +696,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel7/distro.go b/internal/distro/rhel7/distro.go
index 00726e639..2bae70616 100644
--- a/internal/distro/rhel7/distro.go
+++ b/internal/distro/rhel7/distro.go
@@ -29,10 +29,6 @@ const (
 	blueprintPkgsKey = "blueprint"
 )
 
-var mountpointAllowList = []string{
-	"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-}
-
 // RHEL-based OS image configuration defaults
 var defaultDistroImageConfig = &distro.ImageConfig{
 	Timezone: "America/New_York",
@@ -437,15 +433,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 
 	mountpoints := customizations.GetFilesystems()
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index a2d54ef07..f845ab1c5 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -69,10 +69,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	// rhel8 allow all
 	oscapProfileAllowList = []oscap.Profile{
 		oscap.AnssiBp28Enhanced,
@@ -667,15 +663,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {
diff --git a/internal/distro/rhel90/distro.go b/internal/distro/rhel90/distro.go
index 6880e719d..0dd49ab7f 100644
--- a/internal/distro/rhel90/distro.go
+++ b/internal/distro/rhel90/distro.go
@@ -39,10 +39,6 @@ const (
 )
 
 var (
-	mountpointAllowList = []string{
-		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-	}
-
 	// rhel9 & cs9 share the same list
 	// of allowed profiles so a single
 	// allow list can be used
@@ -603,15 +599,9 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("Custom mountpoints are not supported for ostree types")
 	}
 
-	invalidMountpoints := []string{}
-	for _, m := range mountpoints {
-		if !distro.IsMountpointAllowed(m.Mountpoint, mountpointAllowList) {
-			invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
-		}
-	}
-
-	if len(invalidMountpoints) > 0 {
-		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
+	err := disk.CheckMountpoints(mountpoints, disk.MountpointPolicies)
+	if err != nil {
+		return err
 	}
 
 	if osc := customizations.GetOpenSCAP(); osc != nil {


Test patch:
diff --git a/internal/client/compose_test.go b/internal/client/compose_test.go
index fb224eb47..dd9b5e874 100644
--- a/internal/client/compose_test.go
+++ b/internal/client/compose_test.go
@@ -553,7 +553,7 @@ func TestComposeUnsupportedMountPointV0(t *testing.T) {
 		description="TestComposeUnsupportedMountPointV0"
 		version="0.0.1"
 		[[customizations.filesystem]]
-		mountpoint = "/boot"
+		mountpoint = "/etc"
 		size = 4294967296
 		`
 	resp, err := PostTOMLBlueprintV0(testState.socket, bp)
diff --git a/internal/disk/disk_test.go b/internal/disk/disk_test.go
index 6f9b38d36..7e98ced62 100644
--- a/internal/disk/disk_test.go
+++ b/internal/disk/disk_test.go
@@ -617,6 +617,36 @@ func TestMinimumSizes(t *testing.T) {
 	}
 }
 
+func TestNewBootWithSizeLVMify(t *testing.T) {
+	pt := testPartitionTables["plain-noboot"]
+	assert := assert.New(t)
+
+	// math/rand is good enough in this case
+	/* #nosec G404 */
+	rng := rand.New(rand.NewSource(13))
+
+	custom := []blueprint.FilesystemCustomization{
+		{
+			Mountpoint: "/boot",
+			MinSize:    700 * MiB,
+		},
+	}
+
+	mpt, err := NewPartitionTable(&pt, custom, uint64(3*GiB), true, rng)
+	assert.NoError(err)
+
+	for idx, c := range custom {
+		mnt, minSize := c.Mountpoint, c.MinSize
+		path := entityPath(mpt, mnt)
+		assert.NotNil(path, "[%d] mountpoint %q not found", idx, mnt)
+		parent := path[1]
+		part, ok := parent.(*Partition)
+		assert.True(ok, "%q parent (%v) is not a partition", mnt, parent)
+		assert.GreaterOrEqual(part.GetSize(), minSize,
+			"[%d] %q size %d should be greater or equal to %d", idx, mnt, part.GetSize(), minSize)
+	}
+}
+
 func collectEntities(pt *PartitionTable) []Entity {
 	entities := make([]Entity, 0)
 	collector := func(ent Entity, path []Entity) error {
diff --git a/internal/disk/path_policy_test.go b/internal/disk/path_policy_test.go
new file mode 100644
index 000000000..2d30c1c90
--- /dev/null
+++ b/internal/disk/path_policy_test.go
@@ -0,0 +1,50 @@
+package disk
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPathPolicyCheck(t *testing.T) {
+	assert := assert.New(t)
+
+	entires := map[string]PathPolicy{
+		"/":          {Exact: true},
+		"/boot":      {Exact: true},
+		"/boot/efi":  {Exact: true},
+		"/var":       {},
+		"/var/empty": {Deny: true},
+		"/srv":       {},
+		"/home":      {},
+	}
+
+	policies := NewPathPolicies(entires)
+	assert.NotNil(policies)
+
+	tests := map[string]bool{
+		"/":                true,
+		"/custom":          false,
+		"/boot":            true,
+		"/boot/grub2":      false,
+		"/boot/efi":        true,
+		"/boot/efi/redora": false,
+		"/srv":             true,
+		"/srv/www":         true,
+		"/srv/www/data":    true,
+		"/var":             true,
+		"/var/log":         true,
+		"/var/empty":       false,
+		"/var/empty/dir":   false,
+	}
+
+	for k, v := range tests {
+		err := policies.Check(k)
+		if v {
+			assert.NoError(err)
+		} else {
+			assert.Errorf(err, "unexpected error for path '%s'", k)
+
+		}
+	}
+}
diff --git a/internal/disk/path_tree_test.go b/internal/disk/path_tree_test.go
new file mode 100644
index 000000000..076a451fc
--- /dev/null
+++ b/internal/disk/path_tree_test.go
@@ -0,0 +1,134 @@
+package disk
+
+import (
+	"testing"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewPathTrieFromMap(t *testing.T) {
+	assert := assert.New(t)
+
+	type testCase struct {
+		entries map[string]interface{}
+		trie    *PathTrie
+	}
+
+	tests := []testCase{
+		{
+			entries: map[string]interface{}{},
+			trie: &PathTrie{
+				Name: []string{},
+			},
+		},
+		{
+			entries: map[string]interface{}{
+				"/": common.IntToPtr(1),
+			},
+			trie: &PathTrie{
+				Name:    []string{},
+				Payload: common.IntToPtr(1),
+			},
+		},
+		{
+			entries: map[string]interface{}{
+				"/":                            common.IntToPtr(1),
+				"/var":                         common.IntToPtr(2),
+				"/var/lib/chrony":              common.IntToPtr(3),
+				"/var/lib/chrony/logs":         common.IntToPtr(4),
+				"/var/lib/osbuild":             common.IntToPtr(5),
+				"/var/lib/osbuild/store/cache": common.IntToPtr(6),
+				"/boot":                        common.IntToPtr(7),
+				"/boot/efi":                    common.IntToPtr(8),
+			},
+			trie: &PathTrie{
+				Name:    []string{},
+				Payload: common.IntToPtr(1),
+				Paths: []*PathTrie{
+					{
+						Name:    []string{"boot"},
+						Payload: common.IntToPtr(7),
+						Paths: []*PathTrie{
+							{
+								Name:    []string{"efi"},
+								Payload: common.IntToPtr(8),
+							},
+						},
+					},
+					{
+						Name:    []string{"var"},
+						Payload: common.IntToPtr(2),
+						Paths: []*PathTrie{
+							{
+								Name:    []string{"lib", "chrony"},
+								Payload: common.IntToPtr(3),
+								Paths: []*PathTrie{
+									{
+										Name:    []string{"logs"},
+										Payload: common.IntToPtr(4),
+									},
+								},
+							},
+							{
+								Name:    []string{"lib", "osbuild"},
+								Payload: common.IntToPtr(5),
+								Paths: []*PathTrie{
+									{
+										Name:    []string{"store", "cache"},
+										Payload: common.IntToPtr(6),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		have := NewPathTrieFromMap(tc.entries)
+		assert.NotNil(have)
+		assert.Equal(tc.trie, have)
+	}
+}
+
+func TestPathTrieLookup(t *testing.T) {
+	assert := assert.New(t)
+
+	entries := map[string]interface{}{
+		"/":                            "/",
+		"/boot":                        "/boot",
+		"/boot/efi":                    "/boot/efi",
+		"/var":                         "/var",
+		"/var/lib/osbuild":             "/var/lib/osbuild",
+		"/var/lib/osbuild/store/cache": "/var/lib/osbuild/store/cache",
+		"/var/lib/chrony":              "/var/lib/chrony",
+		"/var/lib/chrony/logs":         "/var/lib/chrony/logs",
+	}
+
+	trie := NewPathTrieFromMap(entries)
+
+	testCases := map[string]string{
+		"/":                         "/",
+		"/srv":                      "/",
+		"/srv/data":                 "/",
+		"/boot":                     "/boot",
+		"/boot/efi":                 "/boot/efi",
+		"/boot/grub2":               "/boot",
+		"/boot/efi/fedora":          "/boot/efi",
+		"/var/lib/osbuild":          "/var/lib/osbuild",
+		"/var/lib/osbuild/test":     "/var/lib/osbuild",
+		"/var/lib/chrony":           "/var/lib/chrony",
+		"/var/lib/chrony/test":      "/var/lib/chrony",
+		"/var/lib/chrony/logs":      "/var/lib/chrony/logs",
+		"/var/lib/chrony/logs/data": "/var/lib/chrony/logs",
+	}
+
+	for k, v := range testCases {
+		node, _ := trie.Lookup(k)
+		assert.NotNil(node)
+		assert.Equal(v, node.Payload, "Lookup path: '%s' (%+v)", k, node.Name)
+	}
+}
diff --git a/internal/distro/fedora/distro_test.go b/internal/distro/fedora/distro_test.go
index 4dca8ae4f..5ebd87a38 100644
--- a/internal/distro/fedora/distro_test.go
+++ b/internal/distro/fedora/distro_test.go
@@ -529,7 +529,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -544,7 +544,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "fedora-iot-installer" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/internal/distro/rhel7/distro_test.go b/internal/distro/rhel7/distro_test.go
index b727f941a..24bc87f8e 100644
--- a/internal/distro/rhel7/distro_test.go
+++ b/internal/distro/rhel7/distro_test.go
@@ -275,7 +275,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -285,7 +285,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
 			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, nil, 0)
-			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+			assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 		}
 	}
 }
diff --git a/internal/distro/rhel8/distro_test.go b/internal/distro/rhel8/distro_test.go
index ac7827ad9..645adb311 100644
--- a/internal/distro/rhel8/distro_test.go
+++ b/internal/distro/rhel8/distro_test.go
@@ -614,7 +614,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -629,7 +629,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/internal/distro/rhel90/distro_test.go b/internal/distro/rhel90/distro_test.go
index c1b586469..56f3aea97 100644
--- a/internal/distro/rhel90/distro_test.go
+++ b/internal/distro/rhel90/distro_test.go
@@ -606,7 +606,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			Filesystem: []blueprint.FilesystemCustomization{
 				{
 					MinSize:    1024,
-					Mountpoint: "/boot",
+					Mountpoint: "/etc",
 				},
 			},
 		},
@@ -621,7 +621,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
 				continue
 			} else {
-				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/boot\"]")
+				assert.EqualError(t, err, "The following custom mountpoints are not supported [\"/etc\"]")
 			}
 		}
 	}
diff --git a/test/cases/filesystem.sh b/test/cases/filesystem.sh
index 1cb86529f..8eb22b865 100644
--- a/test/cases/filesystem.sh
+++ b/test/cases/filesystem.sh
@@ -242,7 +242,7 @@ mountpoint = "/etc"
 size = 131072000
 
 [[customizations.filesystem]]
-mountpoint = "/boot"
+mountpoint = "/lost+found"
 size = 131072000
 
 EOF
@@ -254,7 +254,7 @@ build_image "$BLUEPRINT_FILE" rhel85-custom-filesystem-fail qcow2 true
 FAILED_MOUNTPOINTS=()
 
 greenprint "💬 Checking expected failures"
-for MOUNTPOINT in '/etc' '/boot' ; do
+for MOUNTPOINT in '/etc' '/lost+found' ; do
   if ! [[ $ERROR_MSG == *"$MOUNTPOINT"* ]]; then
     FAILED_MOUNTPOINTS+=("$MOUNTPOINT")
   fi