Skip to content

Commit e8bec1b

Browse files
authored
Merge pull request #4265 from lifubang/fix-set-RLIMIT_NOFILE-race
Fix set nofile rlimit error
2 parents 151f480 + 4ea0bf8 commit e8bec1b

File tree

6 files changed

+147
-10
lines changed

6 files changed

+147
-10
lines changed

libcontainer/init_linux.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
223223
return err
224224
}
225225

226+
// Clean the RLIMIT_NOFILE cache in go runtime.
227+
// Issue: https://github.com/opencontainers/runc/issues/4195
228+
if containsRlimit(config.Rlimits, unix.RLIMIT_NOFILE) {
229+
system.ClearRlimitNofileCache()
230+
}
231+
226232
switch t {
227233
case initSetns:
228234
i := &linuxSetnsInit{
@@ -649,6 +655,15 @@ func setupRoute(config *configs.Config) error {
649655
return nil
650656
}
651657

658+
func containsRlimit(limits []configs.Rlimit, resource int) bool {
659+
for _, rlimit := range limits {
660+
if rlimit.Type == resource {
661+
return true
662+
}
663+
}
664+
return false
665+
}
666+
652667
func setupRlimits(limits []configs.Rlimit, pid int) error {
653668
for _, rlimit := range limits {
654669
if err := unix.Prlimit(pid, rlimit.Type, &unix.Rlimit{Max: rlimit.Hard, Cur: rlimit.Soft}, nil); err != nil {

libcontainer/integration/exec_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,13 @@ func testRlimit(t *testing.T, userns bool) {
136136

137137
config := newTemplateConfig(t, &tParam{userns: userns})
138138

139-
// ensure limit is lower than what the config requests to test that in a user namespace
139+
// Ensure limit is lower than what the config requests to test that in a user namespace
140140
// the Setrlimit call happens early enough that we still have permissions to raise the limit.
141+
// Do not change the Cur value to be equal to the Max value, please see:
142+
// https://github.com/opencontainers/runc/pull/4265#discussion_r1589666444
141143
ok(t, unix.Setrlimit(unix.RLIMIT_NOFILE, &unix.Rlimit{
142144
Max: 1024,
143-
Cur: 1024,
145+
Cur: 512,
144146
}))
145147

146148
out := runContainerOk(t, config, "/bin/sh", "-c", "ulimit -n")

libcontainer/process_linux.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,20 +268,26 @@ func (p *setnsProcess) start() (retErr error) {
268268
}
269269
}
270270
}
271-
// set rlimits, this has to be done here because we lose permissions
272-
// to raise the limits once we enter a user-namespace
273-
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
274-
return fmt.Errorf("error setting rlimits for process: %w", err)
275-
}
271+
276272
if err := utils.WriteJSON(p.comm.initSockParent, p.config); err != nil {
277273
return fmt.Errorf("error writing config to pipe: %w", err)
278274
}
279275

276+
var seenProcReady bool
280277
ierr := parseSync(p.comm.syncSockParent, func(sync *syncT) error {
281278
switch sync.Type {
282279
case procReady:
283-
// This shouldn't happen.
284-
panic("unexpected procReady in setns")
280+
seenProcReady = true
281+
// Set rlimits, this has to be done here because we lose permissions
282+
// to raise the limits once we enter a user-namespace
283+
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
284+
return fmt.Errorf("error setting rlimits for ready process: %w", err)
285+
}
286+
287+
// Sync with child.
288+
if err := writeSync(p.comm.syncSockParent, procRun); err != nil {
289+
return err
290+
}
285291
case procHooks:
286292
// This shouldn't happen.
287293
panic("unexpected procHooks in setns")
@@ -340,6 +346,9 @@ func (p *setnsProcess) start() (retErr error) {
340346
if err := p.comm.syncSockParent.Shutdown(unix.SHUT_WR); err != nil && ierr == nil {
341347
return err
342348
}
349+
if !seenProcReady && ierr == nil {
350+
ierr = errors.New("procReady not received")
351+
}
343352
// Must be done after Shutdown so the child will exit and we can wait for it.
344353
if ierr != nil {
345354
_, _ = p.wait()
@@ -774,7 +783,7 @@ func (p *initProcess) start() (retErr error) {
774783
}
775784
case procReady:
776785
seenProcReady = true
777-
// set rlimits, this has to be done here because we lose permissions
786+
// Set rlimits, this has to be done here because we lose permissions
778787
// to raise the limits once we enter a user-namespace
779788
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
780789
return fmt.Errorf("error setting rlimits for ready process: %w", err)

libcontainer/setns_init_linux.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func (l *linuxSetnsInit) Init() error {
4949
}
5050
}
5151
}
52+
5253
if l.config.CreateConsole {
5354
if err := setupConsole(l.consoleSocket, l.config, false); err != nil {
5455
return err
@@ -77,6 +78,13 @@ func (l *linuxSetnsInit) Init() error {
7778
}
7879
}
7980

81+
// Tell our parent that we're ready to exec. This must be done before the
82+
// Seccomp rules have been applied, because we need to be able to read and
83+
// write to a socket.
84+
if err := syncParentReady(l.pipe); err != nil {
85+
return fmt.Errorf("sync ready: %w", err)
86+
}
87+
8088
if err := selinux.SetExecLabel(l.config.ProcessLabel); err != nil {
8189
return err
8290
}

libcontainer/system/linux.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,28 @@ import (
88
"io"
99
"os"
1010
"strconv"
11+
"sync/atomic"
1112
"syscall"
1213
"unsafe"
1314

1415
"github.com/sirupsen/logrus"
1516
"golang.org/x/sys/unix"
1617
)
1718

19+
//go:linkname syscallOrigRlimitNofile syscall.origRlimitNofile
20+
var syscallOrigRlimitNofile atomic.Pointer[syscall.Rlimit]
21+
22+
// As reported in issue #4195, the new version of go runtime(since 1.19)
23+
// will cache rlimit-nofile. Before executing execve, the rlimit-nofile
24+
// of the process will be restored with the cache. In runc, this will
25+
// cause the rlimit-nofile setting by the parent process for the container
26+
// to become invalid. It can be solved by clearing this cache. But
27+
// unfortunately, go stdlib doesn't provide such function, so we need to
28+
// link to the private var `origRlimitNofile` in package syscall to hack.
29+
func ClearRlimitNofileCache() {
30+
syscallOrigRlimitNofile.Store(nil)
31+
}
32+
1833
type ParentDeathSignal int
1934

2035
func (p ParentDeathSignal) Restore() error {

tests/integration/rlimits.bats

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bats
2+
3+
load helpers
4+
5+
function setup() {
6+
# Do not change the Cur value to be equal to the Max value
7+
# Because in some environments, the soft and hard nofile limit have the same value.
8+
[ $EUID -eq 0 ] && prlimit --nofile=1024:65536 -p $$
9+
setup_busybox
10+
}
11+
12+
function teardown() {
13+
teardown_bundle
14+
}
15+
16+
# Set and check rlimit_nofile for runc run. Arguments are:
17+
# $1: soft limit;
18+
# $2: hard limit.
19+
function run_check_nofile() {
20+
soft="$1"
21+
hard="$2"
22+
update_config ".process.rlimits = [{\"type\": \"RLIMIT_NOFILE\", \"soft\": ${soft}, \"hard\": ${hard}}]"
23+
update_config '.process.args = ["/bin/sh", "-c", "ulimit -n; ulimit -H -n"]'
24+
25+
runc run test_rlimit
26+
[ "$status" -eq 0 ]
27+
[[ "${lines[0]}" == "${soft}" ]]
28+
[[ "${lines[1]}" == "${hard}" ]]
29+
}
30+
31+
# Set and check rlimit_nofile for runc exec. Arguments are:
32+
# $1: soft limit;
33+
# $2: hard limit.
34+
function exec_check_nofile() {
35+
soft="$1"
36+
hard="$2"
37+
update_config ".process.rlimits = [{\"type\": \"RLIMIT_NOFILE\", \"soft\": ${soft}, \"hard\": ${hard}}]"
38+
39+
runc run -d --console-socket "$CONSOLE_SOCKET" test_rlimit
40+
[ "$status" -eq 0 ]
41+
42+
runc exec test_rlimit /bin/sh -c "ulimit -n; ulimit -H -n"
43+
[ "$status" -eq 0 ]
44+
[[ "${lines[0]}" == "${soft}" ]]
45+
[[ "${lines[1]}" == "${hard}" ]]
46+
}
47+
48+
@test "runc run with RLIMIT_NOFILE(The same as system's hard value)" {
49+
hard=$(ulimit -n -H)
50+
soft="$hard"
51+
run_check_nofile "$soft" "$hard"
52+
}
53+
54+
@test "runc run with RLIMIT_NOFILE(Bigger than system's hard value)" {
55+
requires root
56+
limit=$(ulimit -n -H)
57+
soft=$((limit + 1))
58+
hard=$soft
59+
run_check_nofile "$soft" "$hard"
60+
}
61+
62+
@test "runc run with RLIMIT_NOFILE(Smaller than system's hard value)" {
63+
limit=$(ulimit -n -H)
64+
soft=$((limit - 1))
65+
hard=$soft
66+
run_check_nofile "$soft" "$hard"
67+
}
68+
69+
@test "runc exec with RLIMIT_NOFILE(The same as system's hard value)" {
70+
hard=$(ulimit -n -H)
71+
soft="$hard"
72+
exec_check_nofile "$soft" "$hard"
73+
}
74+
75+
@test "runc exec with RLIMIT_NOFILE(Bigger than system's hard value)" {
76+
requires root
77+
limit=$(ulimit -n -H)
78+
soft=$((limit + 1))
79+
hard=$soft
80+
exec_check_nofile "$soft" "$hard"
81+
}
82+
83+
@test "runc exec with RLIMIT_NOFILE(Smaller than system's hard value)" {
84+
limit=$(ulimit -n -H)
85+
soft=$((limit - 1))
86+
hard=$soft
87+
exec_check_nofile "$soft" "$hard"
88+
}

0 commit comments

Comments
 (0)