Skip to content

Commit b71a612

Browse files
authored
Fix SELinux Mode Detection on No SELinux-Related Boot Params in Azure Linux 3.0+ (#717)
Since the SELinux LSM is loaded by default on Azure Linux 3.0+, when an image has no SELinux-related arguments specific on the kernel command line, it is expected that Image Customizer would read the SELinux config to determine the mode. Instead, it treats this state as disabled, causing a cascade of failures, e.g. if any customizations are made that would require `setfiles` to be run. This PR is a targeted fix for Azure Linux 3.0+ that accounts for the SELinux LSM being loaded. Behavior for other distributions and versions are unchanged, except Fedora, which we do not fully support, as it is expected that Fedora shares the same behavior of Azure Linux 4.0, which mirrors Fedora closely. Note: This fix was discovered because Azure Linux 4.0 ships with SELinux enforced but without any SELinux-related arguments, leading to issues with testing. Validated with new tests: - TestBootCustomizerSELinuxMode40 (test data matches files shipped in Azure Linux 4.0 Alpha 2) - TestGetSELinuxModeFromLinuxArgs_AllCombinations
1 parent 0fb4820 commit b71a612

11 files changed

Lines changed: 243 additions & 4 deletions

toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (b *BootCustomizer) getSELinuxModeFromCmdline(buildDir string, imageChroot
197197
}
198198

199199
// Get the SELinux mode from the kernel command-line args.
200-
selinuxMode, err := getSELinuxModeFromLinuxArgs(args)
200+
selinuxMode, err := b.distroHandler.GetSELinuxModeFromLinuxArgs(args)
201201
if err != nil {
202202
return imagecustomizerapi.SELinuxModeDefault, false, err
203203
}

toolkit/tools/pkg/imagecustomizerlib/bootcustomizer_test.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const (
2020

2121
sampleGrubCfg30Path = "bootcfgtests/3.0-grub.cfg"
2222
sampleDefaultGrub30Path = "bootcfgtests/3.0-default-grub"
23+
24+
sampleGrubCfg40Path = "bootcfgtests/4.0-grub.cfg"
25+
sampleDefaultGrub40Path = "bootcfgtests/4.0-default-grub"
2326
)
2427

2528
func TestBootCustomizerAddKernelCommandLine20(t *testing.T) {
@@ -106,7 +109,7 @@ func TestBootCustomizerSELinuxMode30(t *testing.T) {
106109
selinuxMode, found, err := b.getSELinuxModeFromCmdline("", nil)
107110
assert.NoError(t, err)
108111
assert.True(t, found)
109-
assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode)
112+
assert.Equal(t, imagecustomizerapi.SELinuxModeDefault, selinuxMode)
110113

111114
err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModePermissive)
112115
assert.NoError(t, err)
@@ -154,6 +157,61 @@ func TestBootCustomizerSELinuxMode30(t *testing.T) {
154157
checkDiffs30(t, b, "", expectedDefaultGrubFileDiff)
155158
}
156159

160+
func TestBootCustomizerSELinuxMode40(t *testing.T) {
161+
// AZL4 only has GRUB_CMDLINE_LINUX_DEFAULT (no GRUB_CMDLINE_LINUX).
162+
// SELinux args should be added to a new GRUB_CMDLINE_LINUX variable.
163+
b := createBootCustomizerFor40(t)
164+
165+
// AZL4 has no SELinux kernel args at all. The mode should be Default (defer to /etc/selinux/config),
166+
// not Disabled.
167+
selinuxMode, found, err := b.getSELinuxModeFromCmdline("", nil)
168+
assert.NoError(t, err)
169+
assert.True(t, found)
170+
assert.Equal(t, imagecustomizerapi.SELinuxModeDefault, selinuxMode)
171+
172+
err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeDisabled)
173+
assert.NoError(t, err)
174+
175+
// Explicit selinux=0 must read back as Disabled (not Default).
176+
selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil)
177+
assert.NoError(t, err)
178+
assert.True(t, found)
179+
assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode)
180+
181+
expectedDefaultGrubFileDiff := `7a8
182+
> GRUB_CMDLINE_LINUX=" selinux=0 "
183+
`
184+
checkDiffs40(t, b, "", expectedDefaultGrubFileDiff)
185+
186+
err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModePermissive)
187+
assert.NoError(t, err)
188+
189+
// "security=selinux selinux=1" without enforcing=1 means the kernel defers to /etc/selinux/config.
190+
selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil)
191+
assert.NoError(t, err)
192+
assert.True(t, found)
193+
assert.Equal(t, imagecustomizerapi.SELinuxModeDefault, selinuxMode)
194+
195+
expectedDefaultGrubFileDiff = `7a8
196+
> GRUB_CMDLINE_LINUX=" security=selinux selinux=1 "
197+
`
198+
checkDiffs40(t, b, "", expectedDefaultGrubFileDiff)
199+
200+
err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeForceEnforcing)
201+
assert.NoError(t, err)
202+
203+
// enforcing=1 alongside the security/selinux pair locks the mode to enforcing.
204+
selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil)
205+
assert.NoError(t, err)
206+
assert.True(t, found)
207+
assert.Equal(t, imagecustomizerapi.SELinuxModeForceEnforcing, selinuxMode)
208+
209+
expectedDefaultGrubFileDiff = `7a8
210+
> GRUB_CMDLINE_LINUX=" security=selinux selinux=1 enforcing=1 "
211+
`
212+
checkDiffs40(t, b, "", expectedDefaultGrubFileDiff)
213+
}
214+
157215
func TestBootCustomizerVerity20(t *testing.T) {
158216
b := createBootCustomizerFor20(t)
159217

@@ -191,6 +249,11 @@ func checkDiffs30(t *testing.T, b *BootCustomizer, expectedGrubCfgDiff string, e
191249
expectedGrubCfgDiff, expectedDefaultGrubFileDiff)
192250
}
193251

252+
func checkDiffs40(t *testing.T, b *BootCustomizer, expectedGrubCfgDiff string, expectedDefaultGrubFileDiff string) {
253+
checkDiffs(t, b, filepath.Join(testDir, sampleGrubCfg40Path), filepath.Join(testDir, sampleDefaultGrub40Path),
254+
expectedGrubCfgDiff, expectedDefaultGrubFileDiff)
255+
}
256+
194257
func checkDiffs(t *testing.T, b *BootCustomizer, originalGrubCfgPath string, originalDefaultGrubFilePath string,
195258
expectedGrubCfgDiff string, expectedDefaultGrubFileDiff string,
196259
) {
@@ -214,15 +277,21 @@ func calcDiff(t *testing.T, oldPath string, newContent string) string {
214277

215278
func createBootCustomizerFor20(t *testing.T) *BootCustomizer {
216279
return createBootCustomizer(t, filepath.Join(testDir, sampleGrubCfg20Path),
217-
filepath.Join(testDir, sampleDefaultGrub20Path), false)
280+
filepath.Join(testDir, sampleDefaultGrub20Path), false, newAzureLinuxDistroHandler("2.0"))
218281
}
219282

220283
func createBootCustomizerFor30(t *testing.T) *BootCustomizer {
221284
return createBootCustomizer(t, filepath.Join(testDir, sampleGrubCfg30Path),
222-
filepath.Join(testDir, sampleDefaultGrub30Path), true)
285+
filepath.Join(testDir, sampleDefaultGrub30Path), true, newAzureLinuxDistroHandler("3.0"))
286+
}
287+
288+
func createBootCustomizerFor40(t *testing.T) *BootCustomizer {
289+
return createBootCustomizer(t, filepath.Join(testDir, sampleGrubCfg40Path),
290+
filepath.Join(testDir, sampleDefaultGrub40Path), true, newAzureLinuxDistroHandler("4.0"))
223291
}
224292

225293
func createBootCustomizer(t *testing.T, sampleGrubCfgPath string, sampleDefaultGrubFilePath string, isGrubMkconfig bool,
294+
distroHandler DistroHandler,
226295
) *BootCustomizer {
227296
sampleGrubCfgContent, err := os.ReadFile(sampleGrubCfgPath)
228297
assert.NoError(t, err, "failed to read sample grub.cfg file")
@@ -239,6 +308,7 @@ func createBootCustomizer(t *testing.T, sampleGrubCfgPath string, sampleDefaultG
239308
grubCfgContent: string(sampleGrubCfgContent),
240309
defaultGrubFileContent: string(sampleDefaultGrubFileContent),
241310
bootConfigType: bootConfigType,
311+
distroHandler: distroHandler,
242312
}
243313
return b
244314
}

toolkit/tools/pkg/imagecustomizerlib/distrohandler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ type DistroHandler interface {
5858
// Reports whether SELinux configuration is supported by the tool for this distro.
5959
SELinuxSupported() bool
6060

61+
// GetSELinuxModeFromLinuxArgs interprets parsed kernel command-line args and returns the effective SELinux mode
62+
// the kernel will boot with. Returns SELinuxModeDefault to indicate the caller should fall back to reading
63+
// /etc/selinux/config.
64+
GetSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg) (imagecustomizerapi.SELinuxMode, error)
65+
6166
// ReadGrub2ConfigFile reads the distro-appropriate grub.cfg file from the chroot.
6267
ReadGrub2ConfigFile(imageChroot safechroot.ChrootInterface) (string, error)
6368

toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ func (d *aclDistroHandler) SELinuxSupported() bool {
8282
return true
8383
}
8484

85+
func (d *aclDistroHandler) GetSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg,
86+
) (imagecustomizerapi.SELinuxMode, error) {
87+
return getSELinuxModeFromLinuxArgs(args)
88+
}
89+
8590
func (d *aclDistroHandler) ReadGrub2ConfigFile(imageChroot safechroot.ChrootInterface) (string, error) {
8691
// ACL does not use GRUB. Return empty string with ErrNotExist so callers
8792
// that tolerate a missing grub.cfg can proceed without error.

toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ func (d *azureLinuxDistroHandler) SELinuxSupported() bool {
103103
return true
104104
}
105105

106+
func (d *azureLinuxDistroHandler) GetSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg,
107+
) (imagecustomizerapi.SELinuxMode, error) {
108+
if d.version == "2.0" {
109+
return getSELinuxModeFromLinuxArgs(args)
110+
}
111+
112+
return getSELinuxModeFromLinuxArgsDeferIfMissing(args)
113+
}
114+
106115
func (d *azureLinuxDistroHandler) ReadGrub2ConfigFile(imageChroot safechroot.ChrootInterface) (string, error) {
107116
return readGrub2ConfigFile(imageChroot, installutils.FedoraGrubCfgFile)
108117
}

toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ func (d *fedoraDistroHandler) SELinuxSupported() bool {
9595
return true
9696
}
9797

98+
func (d *fedoraDistroHandler) GetSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg,
99+
) (imagecustomizerapi.SELinuxMode, error) {
100+
return getSELinuxModeFromLinuxArgsDeferIfMissing(args)
101+
}
102+
98103
func (d *fedoraDistroHandler) ReadGrub2ConfigFile(imageChroot safechroot.ChrootInterface) (string, error) {
99104
return readGrub2ConfigFile(imageChroot, installutils.FedoraGrubCfgFile)
100105
}

toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ func (d *ubuntuDistroHandler) SELinuxSupported() bool {
116116
return false
117117
}
118118

119+
func (d *ubuntuDistroHandler) GetSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg,
120+
) (imagecustomizerapi.SELinuxMode, error) {
121+
return getSELinuxModeFromLinuxArgs(args)
122+
}
123+
119124
func (d *ubuntuDistroHandler) ReadGrub2ConfigFile(imageChroot safechroot.ChrootInterface) (string, error) {
120125
return readGrub2ConfigFile(imageChroot, installutils.DebianGrubCfgFile)
121126
}

toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,22 @@ func getSELinuxModeFromLinuxArgs(args []grubConfigLinuxArg) (imagecustomizerapi.
750750
return imagecustomizerapi.SELinuxModeDefault, nil
751751
}
752752

753+
// getSELinuxModeFromLinuxArgsDeferIfMissing wraps getSELinuxModeFromLinuxArgs so that completely absent
754+
// SELinux cmdline args resolve to SELinuxModeDefault (defer to /etc/selinux/config) rather than SELinuxModeDisabled.
755+
// If any is present (e.g. security=apparmor, selinux=0, enforcing=1), the original Disabled result is preserved.
756+
func getSELinuxModeFromLinuxArgsDeferIfMissing(args []grubConfigLinuxArg) (imagecustomizerapi.SELinuxMode, error) {
757+
mode, err := getSELinuxModeFromLinuxArgs(args)
758+
if err != nil || mode != imagecustomizerapi.SELinuxModeDisabled {
759+
return mode, err
760+
}
761+
762+
if len(findMatchingCommandLineArgs(args, selinuxArgNames)) == 0 {
763+
return imagecustomizerapi.SELinuxModeDefault, nil
764+
}
765+
766+
return imagecustomizerapi.SELinuxModeDisabled, nil
767+
}
768+
753769
// Gets the SELinux mode set by the /etc/selinux/config file.
754770
func getSELinuxModeFromConfigFile(imageChroot safechroot.ChrootInterface) (imagecustomizerapi.SELinuxMode, error) {
755771
selinuxConfigFilePath := filepath.Join(imageChroot.RootDir(), installutils.SELinuxConfigFile)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package imagecustomizerlib
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
// selinuxArgState describes the state of a single SELinux-related kernel command-line arg.
15+
// If Present is false, the arg is not on the cmdline at all (Value is ignored).
16+
type selinuxArgState struct {
17+
Present bool
18+
Value string
19+
}
20+
21+
// TestGetSELinuxModeFromLinuxArgs_AllCombinations exhaustively covers the 3x3x3 = 27 combinations of
22+
// (security, selinux, enforcing) kernel command-line arg states.
23+
func TestGetSELinuxModeFromLinuxArgs_AllCombinations(t *testing.T) {
24+
const (
25+
Disabled = imagecustomizerapi.SELinuxModeDisabled
26+
Default = imagecustomizerapi.SELinuxModeDefault
27+
ForceEnforcing = imagecustomizerapi.SELinuxModeForceEnforcing
28+
)
29+
30+
absent := selinuxArgState{Present: false}
31+
present := func(v string) selinuxArgState { return selinuxArgState{Present: true, Value: v} }
32+
33+
testCases := []struct {
34+
Name string
35+
Security selinuxArgState
36+
Selinux selinuxArgState
37+
Enforcing selinuxArgState
38+
ExpectedBase imagecustomizerapi.SELinuxMode
39+
ExpectedDefer imagecustomizerapi.SELinuxMode
40+
}{
41+
// security=absent
42+
{"security=absent/selinux=absent/enforcing=absent", absent, absent, absent, Disabled, Default},
43+
{"security=absent/selinux=absent/enforcing=0", absent, absent, present("0"), Disabled, Disabled},
44+
{"security=absent/selinux=absent/enforcing=1", absent, absent, present("1"), Disabled, Disabled},
45+
{"security=absent/selinux=0/enforcing=absent", absent, present("0"), absent, Disabled, Disabled},
46+
{"security=absent/selinux=0/enforcing=0", absent, present("0"), present("0"), Disabled, Disabled},
47+
{"security=absent/selinux=0/enforcing=1", absent, present("0"), present("1"), Disabled, Disabled},
48+
{"security=absent/selinux=1/enforcing=absent", absent, present("1"), absent, Disabled, Disabled},
49+
{"security=absent/selinux=1/enforcing=0", absent, present("1"), present("0"), Disabled, Disabled},
50+
{"security=absent/selinux=1/enforcing=1", absent, present("1"), present("1"), Disabled, Disabled},
51+
52+
// security=selinux
53+
{"security=selinux/selinux=absent/enforcing=absent", present("selinux"), absent, absent, Disabled, Disabled},
54+
{"security=selinux/selinux=absent/enforcing=0", present("selinux"), absent, present("0"), Disabled, Disabled},
55+
{"security=selinux/selinux=absent/enforcing=1", present("selinux"), absent, present("1"), Disabled, Disabled},
56+
{"security=selinux/selinux=0/enforcing=absent", present("selinux"), present("0"), absent, Disabled, Disabled},
57+
{"security=selinux/selinux=0/enforcing=0", present("selinux"), present("0"), present("0"), Disabled, Disabled},
58+
{"security=selinux/selinux=0/enforcing=1", present("selinux"), present("0"), present("1"), Disabled, Disabled},
59+
{"security=selinux/selinux=1/enforcing=absent", present("selinux"), present("1"), absent, Default, Default},
60+
{"security=selinux/selinux=1/enforcing=0", present("selinux"), present("1"), present("0"), Default, Default},
61+
{"security=selinux/selinux=1/enforcing=1", present("selinux"), present("1"), present("1"), ForceEnforcing, ForceEnforcing},
62+
63+
// security=apparmor
64+
{"security=apparmor/selinux=absent/enforcing=absent", present("apparmor"), absent, absent, Disabled, Disabled},
65+
{"security=apparmor/selinux=absent/enforcing=0", present("apparmor"), absent, present("0"), Disabled, Disabled},
66+
{"security=apparmor/selinux=absent/enforcing=1", present("apparmor"), absent, present("1"), Disabled, Disabled},
67+
{"security=apparmor/selinux=0/enforcing=absent", present("apparmor"), present("0"), absent, Disabled, Disabled},
68+
{"security=apparmor/selinux=0/enforcing=0", present("apparmor"), present("0"), present("0"), Disabled, Disabled},
69+
{"security=apparmor/selinux=0/enforcing=1", present("apparmor"), present("0"), present("1"), Disabled, Disabled},
70+
{"security=apparmor/selinux=1/enforcing=absent", present("apparmor"), present("1"), absent, Disabled, Disabled},
71+
{"security=apparmor/selinux=1/enforcing=0", present("apparmor"), present("1"), present("0"), Disabled, Disabled},
72+
{"security=apparmor/selinux=1/enforcing=1", present("apparmor"), present("1"), present("1"), Disabled, Disabled},
73+
}
74+
75+
for _, tc := range testCases {
76+
t.Run(tc.Name, func(t *testing.T) {
77+
args := buildSELinuxArgs(tc.Security, tc.Selinux, tc.Enforcing)
78+
79+
gotBase, err := getSELinuxModeFromLinuxArgs(args)
80+
assert.NoError(t, err, "getSELinuxModeFromLinuxArgs returned unexpected error")
81+
assert.Equal(t, tc.ExpectedBase, gotBase, "getSELinuxModeFromLinuxArgs mismatch")
82+
83+
gotDefer, err := getSELinuxModeFromLinuxArgsDeferIfMissing(args)
84+
assert.NoError(t, err, "getSELinuxModeFromLinuxArgsDeferIfMissing returned unexpected error")
85+
assert.Equal(t, tc.ExpectedDefer, gotDefer, "getSELinuxModeFromLinuxArgsDeferIfMissing mismatch")
86+
})
87+
}
88+
}
89+
90+
// buildSELinuxArgs constructs a synthetic []grubConfigLinuxArg that only populates the Name and Value fields
91+
// (which are the only fields read by getSELinuxModeFromLinuxArgs / findKernelCommandLineArgValue /
92+
// findMatchingCommandLineArgs). This avoids needing to round-trip through the grub tokenizer for unit tests.
93+
func buildSELinuxArgs(security, selinux, enforcing selinuxArgState) []grubConfigLinuxArg {
94+
var args []grubConfigLinuxArg
95+
if security.Present {
96+
args = append(args, grubConfigLinuxArg{
97+
Name: "security",
98+
Value: security.Value,
99+
Arg: fmt.Sprintf("security=%s", security.Value),
100+
})
101+
}
102+
if selinux.Present {
103+
args = append(args, grubConfigLinuxArg{
104+
Name: "selinux",
105+
Value: selinux.Value,
106+
Arg: fmt.Sprintf("selinux=%s", selinux.Value),
107+
})
108+
}
109+
if enforcing.Present {
110+
args = append(args, grubConfigLinuxArg{
111+
Name: "enforcing",
112+
Value: enforcing.Value,
113+
Arg: fmt.Sprintf("enforcing=%s", enforcing.Value),
114+
})
115+
}
116+
return args
117+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
GRUB_CMDLINE_LINUX_DEFAULT="console=ttyS0 rd.shell=0"
2+
GRUB_ENABLE_BLSCFG=true
3+
GRUB_GFXMODE=auto
4+
GRUB_TERMINAL_INPUT="console"
5+
GRUB_TERMINAL_OUTPUT="gfxterm"
6+
GRUB_TIMEOUT=0
7+
GRUB_DEFAULT=saved

0 commit comments

Comments
 (0)