Skip to content

Commit 80c5945

Browse files
authored
Merge pull request #3882 from dtrudg/prep-4.3.5-ghsa
Prepare 4.3.5 release / pick GHSA-wwrx-w7c9-rf87 fixes
2 parents 19cea39 + 23bc70a commit 80c5945

File tree

10 files changed

+234
-42
lines changed

10 files changed

+234
-42
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ orbs:
66
parameters:
77
go-version:
88
type: string
9-
default: '1.25.3'
9+
default: '1.25.4'
1010

1111
executors:
1212
node:

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# SingularityCE Changelog
22

3+
## 4.3.5 \[2025-12-02\]
4+
5+
### Security Related Fixes
6+
7+
- Fix for [CVE-2025-64750 /
8+
GHSA-wwrx-w7c9-rf87](https://github.com/sylabs/singularity/security/advisories/GHSA-wwrx-w7c9-rf87)
9+
Ineffective application of selinux / apparmor LSM process labels via the
10+
`--security` flag.
11+
- Dependencies updated.
12+
313
## 4.3.4 \[2025-10-14\]
414

515
### Security Related Fixes

INSTALL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,11 @@ git submodule update --init
227227
By default your clone will be on the `main` branch which is where development
228228
of SingularityCE happens. To build a specific version of SingularityCE, check
229229
out a [release tag](https://github.com/sylabs/singularity/tags) before
230-
compiling. E.g. to build the 4.3.3 release, checkout the
231-
`v4.3.3` tag:
230+
compiling. E.g. to build the 4.3.5 release, checkout the
231+
`v4.3.5` tag:
232232

233233
```sh
234-
git checkout --recurse-submodules v4.3.3
234+
git checkout --recurse-submodules v4.3.5
235235
```
236236

237237
## Compiling SingularityCE

e2e/security/security.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package security
88
import (
99
"fmt"
1010
"os"
11+
"os/exec"
1112
"testing"
1213

1314
"github.com/sylabs/singularity/v4/e2e/internal/e2e"
@@ -231,6 +232,124 @@ func (c ctx) testSecurityConfOwnership(t *testing.T) {
231232
)
232233
}
233234

235+
// testApparmor tests the apparmor security flag.
236+
func (c ctx) testApparmor(t *testing.T) {
237+
require.Apparmor(t)
238+
e2e.EnsureImage(t, c.env)
239+
240+
tests := []struct {
241+
name string
242+
image string
243+
argv []string
244+
opts []string
245+
expectOp e2e.SingularityCmdResultOp
246+
expectExit int
247+
}{
248+
// apparmor
249+
// Uses the profile for /usr/bin/man which will block `ls` in container root.
250+
{
251+
name: "apparmor applied",
252+
argv: []string{"cat", "/proc/self/attr/current"},
253+
opts: []string{"--security", "apparmor:/usr/bin/man"},
254+
expectOp: e2e.ExpectOutput(e2e.ExactMatch, "/usr/bin/man (enforce)"),
255+
},
256+
{
257+
name: "apparmor denial",
258+
argv: []string{"ls", "/"},
259+
opts: []string{"--security", "apparmor:/usr/bin/man"},
260+
expectExit: 1,
261+
expectOp: e2e.ExpectError(e2e.ContainMatch, "Permission denied"),
262+
},
263+
}
264+
265+
for _, profile := range e2e.NativeProfiles {
266+
t.Run(profile.String(), func(t *testing.T) {
267+
for _, tt := range tests {
268+
optArgs := []string{}
269+
optArgs = append(optArgs, tt.opts...)
270+
optArgs = append(optArgs, c.env.ImagePath)
271+
optArgs = append(optArgs, tt.argv...)
272+
273+
c.env.RunSingularity(
274+
t,
275+
e2e.AsSubtest(tt.name),
276+
e2e.WithProfile(profile),
277+
e2e.WithCommand("exec"),
278+
e2e.WithArgs(optArgs...),
279+
e2e.ExpectExit(tt.expectExit, tt.expectOp),
280+
)
281+
}
282+
})
283+
}
284+
}
285+
286+
// testSELinux tests the selinux security flag.
287+
func (c ctx) testSELinux(t *testing.T) {
288+
require.Selinux(t)
289+
require.Command(t, "chcon")
290+
e2e.EnsureImage(t, c.env)
291+
292+
sandbox, cleanup := e2e.MakeTempDir(t, c.env.TestDir, "sandbox-", "")
293+
defer cleanup(t)
294+
// convert test image to sandbox
295+
c.env.RunSingularity(
296+
t,
297+
e2e.WithProfile(e2e.UserProfile),
298+
e2e.WithCommand("build"),
299+
e2e.WithArgs("--force", "--sandbox", sandbox, c.env.ImagePath),
300+
e2e.ExpectExit(0),
301+
)
302+
// label as `container_ro_t`
303+
cmd := exec.Command("chcon", "-R", "-t", "container_ro_file_t", sandbox)
304+
if err := cmd.Run(); err != nil {
305+
t.Fatalf("while labeling sandbox: %v", err)
306+
}
307+
308+
tests := []struct {
309+
name string
310+
image string
311+
argv []string
312+
opts []string
313+
expectOp e2e.SingularityCmdResultOp
314+
expectExit int
315+
}{
316+
// selinux
317+
// Uses the container_t label which will block `ls` in /tmp bind from host.
318+
{
319+
name: "selinux applied",
320+
argv: []string{"cat", "/proc/self/attr/current"},
321+
opts: []string{"--security", "selinux:unconfined_u:unconfined_r:container_t:s0"},
322+
expectOp: e2e.ExpectOutput(e2e.ContainMatch, "unconfined_u:unconfined_r:container_t:s0"),
323+
},
324+
{
325+
name: "selinux denial",
326+
argv: []string{"ls", "/tmp"},
327+
opts: []string{"--security", "selinux:unconfined_u:unconfined_r:container_t:s0"},
328+
expectExit: 1,
329+
expectOp: e2e.ExpectError(e2e.ContainMatch, "Permission denied"),
330+
},
331+
}
332+
for _, profile := range e2e.NativeProfiles {
333+
t.Run(profile.String(), func(t *testing.T) {
334+
for _, tt := range tests {
335+
optArgs := []string{}
336+
optArgs = append(optArgs, tt.opts...)
337+
optArgs = append(optArgs, sandbox)
338+
optArgs = append(optArgs, tt.argv...)
339+
340+
c.env.RunSingularity(
341+
t,
342+
e2e.AsSubtest(tt.name),
343+
e2e.WithProfile(profile),
344+
e2e.WithCommand("exec"),
345+
e2e.WithArgs(optArgs...),
346+
e2e.ExpectExit(tt.expectExit, tt.expectOp),
347+
)
348+
}
349+
})
350+
}
351+
}
352+
234353
// E2ETests is the main func to trigger the test suite
235354
func E2ETests(env e2e.TestEnv) testhelper.Tests {
236355
c := ctx{
@@ -243,6 +362,8 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
243362
"singularitySecurityUnpriv": c.testSecurityUnpriv,
244363
"singularitySecurityPriv": c.testSecurityPriv,
245364
"testSecurityConfOwnership": np(c.testSecurityConfOwnership),
365+
"testApparmor": c.testApparmor,
366+
"testSELinux": c.testSELinux,
246367
// OCI-Mode
247368
"ociCapabilities": c.ociCapabilities,
248369
}

internal/pkg/runtime/engine/singularity/container_linux.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,25 @@ func (c *container) addKernelMount(system *mount.System) error {
11461146
} else {
11471147
sylog.Verbosef("Skipping /sys mount")
11481148
}
1149+
1150+
// /sys/fs/selinux must be available for opencontainers/selinux.GetEnabled() to detect SELinux.
1151+
if c.engine.EngineConfig.OciConfig.Process.SelinuxLabel != "" {
1152+
sylog.Debugf("SELinux requested, ensuring /sys/fs/selinux mount available.")
1153+
1154+
if !c.engine.EngineConfig.File.MountSys || c.engine.EngineConfig.GetNoSys() {
1155+
return fmt.Errorf("SELinux requested, but /sys mount disabled by configuration")
1156+
}
1157+
1158+
if c.userNS {
1159+
sylog.Debugf("/sys/fs/selinux available via /sys bind")
1160+
} else {
1161+
if err := system.Points.AddFS(mount.KernelTag, "/sys/fs/selinux", "selinuxfs", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC, ""); err != nil {
1162+
return fmt.Errorf("unable to add sys/fs/selinux to mount list: %s", err)
1163+
}
1164+
sylog.Verbosef("Default mount: /sys/fs/selinux")
1165+
}
1166+
}
1167+
11491168
return nil
11501169
}
11511170

internal/pkg/security/apparmor/apparmor_supported.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
1+
// Copyright (c) 2018-2025, Sylabs Inc. All rights reserved.
22
// This software is licensed under a 3-clause BSD license. Please consult the
33
// LICENSE.md file distributed with the sources of this project regarding your
44
// rights to use or distribute this software.
@@ -10,6 +10,10 @@ package apparmor
1010
import (
1111
"fmt"
1212
"os"
13+
14+
"github.com/cyphar/filepath-securejoin/pathrs-lite"
15+
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
16+
"golang.org/x/sys/unix"
1317
)
1418

1519
// Enabled returns whether AppArmor is enabled.
@@ -23,7 +27,23 @@ func Enabled() bool {
2327

2428
// LoadProfile loads the specified AppArmor profile.
2529
func LoadProfile(profile string) error {
26-
f, err := os.OpenFile("/proc/self/attr/exec", os.O_WRONLY, 0)
30+
// We must make sure we are actually opening and writing to a real attr/exec
31+
// in a real procfs so that the profile takes effect. Using
32+
// pathrs-lite/procfs as below accomplishes this.
33+
proc, err := procfs.OpenProcRoot()
34+
if err != nil {
35+
return err
36+
}
37+
defer proc.Close()
38+
39+
attrExec, closer, err := proc.OpenThreadSelf("attr/exec")
40+
if err != nil {
41+
return err
42+
}
43+
defer closer()
44+
defer attrExec.Close()
45+
46+
f, err := pathrs.Reopen(attrExec, unix.O_WRONLY|unix.O_CLOEXEC)
2747
if err != nil {
2848
return err
2949
}

internal/pkg/security/security.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2018-2020, Sylabs Inc. All rights reserved.
1+
// Copyright (c) 2018-2025, Sylabs Inc. All rights reserved.
22
// This software is licensed under a 3-clause BSD license. Please consult the
33
// LICENSE.md file distributed with the sources of this project regarding your
44
// rights to use or distribute this software.
@@ -28,15 +28,15 @@ func Configure(config *specs.Spec) error {
2828
return err
2929
}
3030
} else {
31-
sylog.Warningf("selinux is not enabled or supported on this system")
31+
return fmt.Errorf("selinux requested, but is not enabled or supported on this system")
3232
}
3333
} else if config.Process.ApparmorProfile != "" {
3434
if apparmor.Enabled() {
3535
if err := apparmor.LoadProfile(config.Process.ApparmorProfile); err != nil {
3636
return err
3737
}
3838
} else {
39-
sylog.Warningf("apparmor is not enabled or supported on this system")
39+
return fmt.Errorf("apparmor requested, but is not enabled or supported on this system")
4040
}
4141
}
4242
}
@@ -46,7 +46,7 @@ func Configure(config *specs.Spec) error {
4646
return err
4747
}
4848
} else {
49-
sylog.Warningf("seccomp requested but not enabled, seccomp library is missing or too old")
49+
return fmt.Errorf("seccomp requested but not enabled, seccomp library is missing or too old")
5050
}
5151
}
5252
return nil

internal/pkg/security/security_test.go

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
// Copyright (c) 2019, Sylabs Inc. All rights reserved.
1+
// Copyright (c) 2019-2025, Sylabs Inc. All rights reserved.
22
// This software is licensed under a 3-clause BSD license. Please consult the
33
// LICENSE.md file distributed with the sources of this project regarding your
44
// rights to use or distribute this software.
55

66
package security
77

88
import (
9-
"os"
109
"runtime"
1110
"testing"
1211

1312
specs "github.com/opencontainers/runtime-spec/specs-go"
1413
"github.com/sylabs/singularity/v4/internal/pkg/security/apparmor"
1514
"github.com/sylabs/singularity/v4/internal/pkg/security/selinux"
1615
"github.com/sylabs/singularity/v4/internal/pkg/test"
17-
"github.com/sylabs/singularity/v4/internal/pkg/util/mainthread"
1816
)
1917

2018
func TestGetParam(t *testing.T) {
@@ -89,6 +87,16 @@ func TestConfigure(t *testing.T) {
8987
},
9088
disabled: !selinux.Enabled(),
9189
},
90+
{
91+
desc: "SELinux when not available",
92+
spec: specs.Spec{
93+
Process: &specs.Process{
94+
SelinuxLabel: "unconfined_u:unconfined_r:unconfined_t:s0",
95+
},
96+
},
97+
expectFailure: true,
98+
disabled: selinux.Enabled(),
99+
},
92100
{
93101
desc: "with bad apparmor profile",
94102
spec: specs.Spec{
@@ -108,6 +116,16 @@ func TestConfigure(t *testing.T) {
108116
},
109117
disabled: !apparmor.Enabled(),
110118
},
119+
{
120+
desc: "apparmor when not available",
121+
spec: specs.Spec{
122+
Process: &specs.Process{
123+
ApparmorProfile: "unconfined",
124+
},
125+
},
126+
expectFailure: true,
127+
disabled: apparmor.Enabled(),
128+
},
111129
}
112130

113131
for _, s := range specs {
@@ -118,9 +136,9 @@ func TestConfigure(t *testing.T) {
118136

119137
var err error
120138

121-
mainthread.Execute(func() {
122-
err = Configure(&s.spec)
123-
})
139+
runtime.LockOSThread()
140+
defer runtime.UnlockOSThread()
141+
err = Configure(&s.spec)
124142

125143
if err != nil && !s.expectFailure {
126144
t.Errorf("unexpected failure %s: %s", s.desc, err)
@@ -130,18 +148,3 @@ func TestConfigure(t *testing.T) {
130148
})
131149
}
132150
}
133-
134-
func init() {
135-
runtime.LockOSThread()
136-
}
137-
138-
func TestMain(m *testing.M) {
139-
go func() {
140-
os.Exit(m.Run())
141-
}()
142-
143-
// run functions requiring execution in main thread
144-
for f := range mainthread.FuncChannel {
145-
f()
146-
}
147-
}

0 commit comments

Comments
 (0)