Skip to content

Commit 4448313

Browse files
ianlancetaylorgopherbot
authored andcommitted
syscall: honor prlimit set by a different process
On Linux one process can call prlimit to change the resource limit of another process. With this change we treat that as though the current process called prlimit (or setrlimit) to set its own limit. The cost is one additional getrlimit system call per fork/exec, for cases in which the rlimit Cur and Max values differ at startup. This revealed a bug: the setrlimit (not Setrlimit) function should not change the cached rlimit. That means that it must call prlimit1, not prlimit. Fixes #66797 Change-Id: I46bfd06e09ab7273fe8dd9b5b744dffdf31d828b Reviewed-on: https://go-review.googlesource.com/c/go/+/607516 Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Aleksa Sarai <[email protected]> Reviewed-by: Kirill Kolyshkin <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]>
1 parent 7b0fdd1 commit 4448313

File tree

4 files changed

+223
-5
lines changed

4 files changed

+223
-5
lines changed

src/syscall/exec_linux.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,12 @@ func forkAndExecInChild1(argv0 *byte, argv, envv []*byte, chroot, dir *byte, att
252252
cred *Credential
253253
ngroups, groups uintptr
254254
c uintptr
255+
rlim *Rlimit
256+
lim Rlimit
255257
)
256258
pidfd = -1
257259

258-
rlim := origRlimitNofile.Load()
260+
rlim = origRlimitNofile.Load()
259261

260262
if sys.UidMappings != nil {
261263
puid = []byte("/proc/self/uid_map\000")
@@ -632,7 +634,20 @@ func forkAndExecInChild1(argv0 *byte, argv, envv []*byte, chroot, dir *byte, att
632634

633635
// Restore original rlimit.
634636
if rlim != nil {
635-
RawSyscall6(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, uintptr(unsafe.Pointer(rlim)), 0, 0, 0)
637+
// Some other process may have changed our rlimit by
638+
// calling prlimit. We can check for that case because
639+
// our current rlimit will not be the value we set when
640+
// caching the rlimit in the init function in rlimit.go.
641+
//
642+
// Note that this test is imperfect, since it won't catch
643+
// the case in which some other process used prlimit to
644+
// set our rlimits to max-1/max. In that case we will fall
645+
// back to the original cur/max when starting the child.
646+
// We hope that setting to max-1/max is unlikely.
647+
_, _, err1 = RawSyscall6(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, 0, uintptr(unsafe.Pointer(&lim)), 0, 0)
648+
if err1 != 0 || (lim.Cur == rlim.Max-1 && lim.Max == rlim.Max) {
649+
RawSyscall6(SYS_PRLIMIT64, 0, RLIMIT_NOFILE, uintptr(unsafe.Pointer(rlim)), 0, 0, 0)
650+
}
636651
}
637652

638653
// Enable tracing if requested.

src/syscall/rlimit.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,18 @@ var origRlimitNofile atomic.Pointer[Rlimit]
2929
// which Go of course has no choice but to respect.
3030
func init() {
3131
var lim Rlimit
32-
if err := Getrlimit(RLIMIT_NOFILE, &lim); err == nil && lim.Cur != lim.Max {
32+
if err := Getrlimit(RLIMIT_NOFILE, &lim); err == nil && lim.Max > 0 && lim.Cur < lim.Max-1 {
3333
origRlimitNofile.Store(&lim)
3434
nlim := lim
35-
nlim.Cur = nlim.Max
35+
36+
// We set Cur to Max - 1 so that we are more likely to
37+
// detect cases where another process uses prlimit
38+
// to change our resource limits. The theory is that
39+
// using prlimit to change to Cur == Max is more likely
40+
// than using prlimit to change to Cur == Max - 1.
41+
// The place we check for this is in exec_linux.go.
42+
nlim.Cur = nlim.Max - 1
43+
3644
adjustFileLimit(&nlim)
3745
setrlimit(RLIMIT_NOFILE, &nlim)
3846
}

src/syscall/syscall_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,7 @@ func Getrlimit(resource int, rlim *Rlimit) (err error) {
12961296
// setrlimit sets a resource limit.
12971297
// The Setrlimit function is in rlimit.go, and calls this one.
12981298
func setrlimit(resource int, rlim *Rlimit) (err error) {
1299-
return prlimit(0, resource, rlim, nil)
1299+
return prlimit1(0, resource, rlim, nil)
13001300
}
13011301

13021302
// prlimit changes a resource limit. We use a single definition so that

src/syscall/syscall_linux_test.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package syscall_test
66

77
import (
8+
"context"
89
"fmt"
910
"internal/testenv"
1011
"io"
@@ -720,3 +721,197 @@ func TestPrlimitOtherProcess(t *testing.T) {
720721
t.Fatalf("origRlimitNofile got=%v, want=%v", rlimLater, rlimOrig)
721722
}
722723
}
724+
725+
const magicRlimitValue = 42
726+
727+
// TestPrlimitFileLimit tests that we can start a Go program, use
728+
// prlimit to change its NOFILE limit, and have that updated limit be
729+
// seen by children. See issue #66797.
730+
func TestPrlimitFileLimit(t *testing.T) {
731+
switch os.Getenv("GO_WANT_HELPER_PROCESS") {
732+
case "prlimit1":
733+
testPrlimitFileLimitHelper1(t)
734+
return
735+
case "prlimit2":
736+
testPrlimitFileLimitHelper2(t)
737+
return
738+
}
739+
740+
origRlimitNofile := syscall.GetInternalOrigRlimitNofile()
741+
defer origRlimitNofile.Store(origRlimitNofile.Load())
742+
743+
// Set our rlimit to magic+1/max.
744+
// That will also become the rlimit of the child.
745+
746+
var lim syscall.Rlimit
747+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
748+
t.Fatal(err)
749+
}
750+
max := lim.Max
751+
752+
lim = syscall.Rlimit{
753+
Cur: magicRlimitValue + 1,
754+
Max: max,
755+
}
756+
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
757+
t.Fatal(err)
758+
}
759+
760+
ctx, cancel := context.WithCancel(context.Background())
761+
defer cancel()
762+
763+
exe, err := os.Executable()
764+
if err != nil {
765+
t.Fatal(err)
766+
}
767+
768+
r1, w1, err := os.Pipe()
769+
if err != nil {
770+
t.Fatal(err)
771+
}
772+
defer r1.Close()
773+
defer w1.Close()
774+
775+
r2, w2, err := os.Pipe()
776+
if err != nil {
777+
t.Fatal(err)
778+
}
779+
defer r2.Close()
780+
defer w2.Close()
781+
782+
var output strings.Builder
783+
784+
const arg = "-test.run=^TestPrlimitFileLimit$"
785+
cmd := testenv.CommandContext(t, ctx, exe, arg, "-test.v")
786+
cmd = testenv.CleanCmdEnv(cmd)
787+
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=prlimit1")
788+
cmd.ExtraFiles = []*os.File{r1, w2}
789+
cmd.Stdout = &output
790+
cmd.Stderr = &output
791+
792+
t.Logf("running %s %s", exe, arg)
793+
794+
if err := cmd.Start(); err != nil {
795+
t.Fatal(err)
796+
}
797+
798+
// Wait for the child to start.
799+
b := make([]byte, 1)
800+
if n, err := r2.Read(b); err != nil {
801+
t.Fatal(err)
802+
} else if n != 1 {
803+
t.Fatalf("read %d bytes, want 1", n)
804+
}
805+
806+
// Set the child's prlimit.
807+
lim = syscall.Rlimit{
808+
Cur: magicRlimitValue,
809+
Max: max,
810+
}
811+
if err := syscall.Prlimit(cmd.Process.Pid, syscall.RLIMIT_NOFILE, &lim, nil); err != nil {
812+
t.Fatalf("Prlimit failed: %v", err)
813+
}
814+
815+
// Tell the child to continue.
816+
if n, err := w1.Write(b); err != nil {
817+
t.Fatal(err)
818+
} else if n != 1 {
819+
t.Fatalf("wrote %d bytes, want 1", n)
820+
}
821+
822+
err = cmd.Wait()
823+
if output.Len() > 0 {
824+
t.Logf("%s", output.String())
825+
}
826+
827+
if err != nil {
828+
t.Errorf("child failed: %v", err)
829+
}
830+
}
831+
832+
// testPrlimitFileLimitHelper1 is run by TestPrlimitFileLimit.
833+
func testPrlimitFileLimitHelper1(t *testing.T) {
834+
var lim syscall.Rlimit
835+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
836+
t.Fatal(err)
837+
}
838+
t.Logf("helper1 rlimit is %v", lim)
839+
t.Logf("helper1 cached rlimit is %v", syscall.OrigRlimitNofile())
840+
841+
// Tell the parent that we are ready.
842+
b := []byte{0}
843+
if n, err := syscall.Write(4, b); err != nil {
844+
t.Fatal(err)
845+
} else if n != 1 {
846+
t.Fatalf("wrote %d bytes, want 1", n)
847+
}
848+
849+
// Wait for the parent to tell us that prlimit was used.
850+
if n, err := syscall.Read(3, b); err != nil {
851+
t.Fatal(err)
852+
} else if n != 1 {
853+
t.Fatalf("read %d bytes, want 1", n)
854+
}
855+
856+
if err := syscall.Close(3); err != nil {
857+
t.Errorf("Close(3): %v", err)
858+
}
859+
if err := syscall.Close(4); err != nil {
860+
t.Errorf("Close(4): %v", err)
861+
}
862+
863+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
864+
t.Fatal(err)
865+
}
866+
t.Logf("after prlimit helper1 rlimit is %v", lim)
867+
t.Logf("after prlimit helper1 cached rlimit is %v", syscall.OrigRlimitNofile())
868+
869+
// Start the grandchild, which should see the rlimit
870+
// set by the prlimit called by the parent.
871+
872+
ctx, cancel := context.WithCancel(context.Background())
873+
defer cancel()
874+
875+
exe, err := os.Executable()
876+
if err != nil {
877+
t.Fatal(err)
878+
}
879+
880+
const arg = "-test.run=^TestPrlimitFileLimit$"
881+
cmd := testenv.CommandContext(t, ctx, exe, arg, "-test.v")
882+
cmd = testenv.CleanCmdEnv(cmd)
883+
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=prlimit2")
884+
t.Logf("running %s %s", exe, arg)
885+
out, err := cmd.CombinedOutput()
886+
if len(out) > 0 {
887+
t.Logf("%s", out)
888+
}
889+
if err != nil {
890+
t.Errorf("grandchild failed: %v", err)
891+
} else {
892+
fmt.Println("OK")
893+
}
894+
}
895+
896+
// testPrlimitFileLimitHelper2 is run by testPrlimitFileLimit1.
897+
func testPrlimitFileLimitHelper2(t *testing.T) {
898+
var lim syscall.Rlimit
899+
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
900+
t.Fatal(err)
901+
}
902+
903+
t.Logf("helper2 rlimit is %v", lim)
904+
cached := syscall.OrigRlimitNofile()
905+
t.Logf("helper2 cached rlimit is %v", cached)
906+
907+
// The value return by Getrlimit will have been adjusted.
908+
// We should have cached the value set by prlimit called by the parent.
909+
910+
if cached == nil {
911+
t.Fatal("no cached rlimit")
912+
} else if cached.Cur != magicRlimitValue {
913+
t.Fatalf("cached rlimit is %d, want %d", cached.Cur, magicRlimitValue)
914+
}
915+
916+
fmt.Println("OK")
917+
}

0 commit comments

Comments
 (0)