Source file src/internal/runtime/cgroup/cgroup_linux_test.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cgroup_test
     6  
     7  import (
     8  	"fmt"
     9  	"internal/runtime/cgroup"
    10  	"io"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  )
    15  
    16  const _PATH_MAX = 4096
    17  
    18  func TestParseV1Number(t *testing.T) {
    19  	tests := []struct {
    20  		name     string
    21  		contents string
    22  		want     int64
    23  		wantErr  bool
    24  	}{
    25  		{
    26  			name:     "disabled",
    27  			contents: "-1\n",
    28  			want:     -1,
    29  		},
    30  		{
    31  			name:     "500000",
    32  			contents: "500000\n",
    33  			want:     500000,
    34  		},
    35  		{
    36  			name:     "MaxInt64",
    37  			contents: "9223372036854775807\n",
    38  			want:     9223372036854775807,
    39  		},
    40  		{
    41  			name:     "missing-newline",
    42  			contents: "500000",
    43  			wantErr:  true,
    44  		},
    45  		{
    46  			name:     "not-a-number",
    47  			contents: "123max\n",
    48  			wantErr:  true,
    49  		},
    50  		{
    51  			name:     "v2",
    52  			contents: "1000 5000\n",
    53  			wantErr:  true,
    54  		},
    55  	}
    56  
    57  	for _, tc := range tests {
    58  		t.Run(tc.name, func(t *testing.T) {
    59  			got, err := cgroup.ParseV1Number([]byte(tc.contents))
    60  			if tc.wantErr {
    61  				if err == nil {
    62  					t.Fatalf("parseV1Number got err nil want non-nil")
    63  				}
    64  				return
    65  			}
    66  			if err != nil {
    67  				t.Fatalf("parseV1Number got err %v want nil", err)
    68  			}
    69  
    70  			if got != tc.want {
    71  				t.Errorf("parseV1Number got %d want %d", got, tc.want)
    72  			}
    73  		})
    74  	}
    75  }
    76  
    77  func TestParseV2Limit(t *testing.T) {
    78  	tests := []struct {
    79  		name     string
    80  		contents string
    81  		want     float64
    82  		wantOK   bool
    83  		wantErr  bool
    84  	}{
    85  		{
    86  			name:     "disabled",
    87  			contents: "max 100000\n",
    88  			wantOK:   false,
    89  		},
    90  		{
    91  			name:     "5",
    92  			contents: "500000 100000\n",
    93  			want:     5,
    94  			wantOK:   true,
    95  		},
    96  		{
    97  			name:     "0.5",
    98  			contents: "50000 100000\n",
    99  			want:     0.5,
   100  			wantOK:   true,
   101  		},
   102  		{
   103  			name:     "2.5",
   104  			contents: "250000 100000\n",
   105  			want:     2.5,
   106  			wantOK:   true,
   107  		},
   108  		{
   109  			name:     "MaxInt64",
   110  			contents: "9223372036854775807 9223372036854775807\n",
   111  			want:     1,
   112  			wantOK:   true,
   113  		},
   114  		{
   115  			name:     "missing-newline",
   116  			contents: "500000 100000",
   117  			wantErr:  true,
   118  		},
   119  		{
   120  			name:     "v1",
   121  			contents: "500000\n",
   122  			wantErr:  true,
   123  		},
   124  		{
   125  			name:     "quota-not-a-number",
   126  			contents: "500000us 100000\n",
   127  			wantErr:  true,
   128  		},
   129  		{
   130  			name:     "period-not-a-number",
   131  			contents: "500000 100000us\n",
   132  			wantErr:  true,
   133  		},
   134  	}
   135  
   136  	for _, tc := range tests {
   137  		t.Run(tc.name, func(t *testing.T) {
   138  			got, gotOK, err := cgroup.ParseV2Limit([]byte(tc.contents))
   139  			if tc.wantErr {
   140  				if err == nil {
   141  					t.Fatalf("parseV1Limit got err nil want non-nil")
   142  				}
   143  				return
   144  			}
   145  			if err != nil {
   146  				t.Fatalf("parseV2Limit got err %v want nil", err)
   147  			}
   148  
   149  			if gotOK != tc.wantOK {
   150  				t.Errorf("parseV2Limit got ok %v want %v", gotOK, tc.wantOK)
   151  			}
   152  
   153  			if tc.wantOK && got != tc.want {
   154  				t.Errorf("parseV2Limit got %f want %f", got, tc.want)
   155  			}
   156  		})
   157  	}
   158  }
   159  
   160  func TestParseCPURelativePath(t *testing.T) {
   161  	tests := []struct {
   162  		name     string
   163  		contents string
   164  		want     string
   165  		wantVer  cgroup.Version
   166  		wantErr  bool
   167  	}{
   168  		{
   169  			name:     "empty",
   170  			contents: "",
   171  			wantErr:  true,
   172  		},
   173  		{
   174  			name: "v1",
   175  			contents: `2:cpu,cpuacct:/a/b/cpu
   176  1:blkio:/a/b/blkio
   177  `,
   178  			want:    "/a/b/cpu",
   179  			wantVer: cgroup.V1,
   180  		},
   181  		{
   182  			name:     "v2",
   183  			contents: "0::/a/b/c\n",
   184  			want:     "/a/b/c",
   185  			wantVer:  cgroup.V2,
   186  		},
   187  		{
   188  			name: "mixed",
   189  			contents: `2:cpu,cpuacct:/a/b/cpu
   190  1:blkio:/a/b/blkio
   191  0::/a/b/v2
   192  `,
   193  			want:    "/a/b/cpu",
   194  			wantVer: cgroup.V1,
   195  		},
   196  	}
   197  
   198  	for _, tc := range tests {
   199  		t.Run(tc.name, func(t *testing.T) {
   200  			r := strings.NewReader(tc.contents)
   201  			read := func(fd int, b []byte) (int, uintptr) {
   202  				n, err := r.Read(b)
   203  				if err != nil && err != io.EOF {
   204  					const dummyErrno = 42
   205  					return n, dummyErrno
   206  				}
   207  				return n, 0
   208  			}
   209  
   210  			var got [cgroup.PathSize]byte
   211  			var scratch [cgroup.ParseSize]byte
   212  			n, gotVer, err := cgroup.ParseCPURelativePath(0, read, got[:], scratch[:])
   213  			if (err != nil) != tc.wantErr {
   214  				t.Fatalf("parseCPURelativePath got err %v want %v", err, tc.wantErr)
   215  			}
   216  
   217  			if gotVer != tc.wantVer {
   218  				t.Errorf("parseCPURelativePath got cgroup version %d want %d", gotVer, tc.wantVer)
   219  			}
   220  
   221  			if string(got[:n]) != tc.want {
   222  				t.Errorf("parseCPURelativePath got %q want %q", string(got[:n]), tc.want)
   223  			}
   224  		})
   225  	}
   226  }
   227  
   228  func TestContainsCPU(t *testing.T) {
   229  	tests := []struct {
   230  		in   string
   231  		want bool
   232  	}{
   233  		{
   234  			in:   "",
   235  			want: false,
   236  		},
   237  		{
   238  			in:   ",",
   239  			want: false,
   240  		},
   241  		{
   242  			in:   "cpu",
   243  			want: true,
   244  		},
   245  		{
   246  			in:   "memory,cpu",
   247  			want: true,
   248  		},
   249  		{
   250  			in:   "cpu,memory",
   251  			want: true,
   252  		},
   253  		{
   254  			in:   "memory,cpu,block",
   255  			want: true,
   256  		},
   257  		{
   258  			in:   "memory,cpuacct,block",
   259  			want: false,
   260  		},
   261  	}
   262  
   263  	for _, tc := range tests {
   264  		t.Run(tc.in, func(t *testing.T) {
   265  			got := cgroup.ContainsCPU([]byte(tc.in))
   266  			if got != tc.want {
   267  				t.Errorf("containsCPU(%q) got %v want %v", tc.in, got, tc.want)
   268  			}
   269  		})
   270  	}
   271  }
   272  
   273  func TestParseCPUMount(t *testing.T) {
   274  	// Used for v2-longline. We want an overlayfs mount to have an option
   275  	// so long that the entire line can't possibly fit in the scratch
   276  	// buffer.
   277  	const lowerPath = "/so/many/overlay/layers"
   278  	overlayLongLowerDir := lowerPath
   279  	for i := 0; len(overlayLongLowerDir) < cgroup.ScratchSize; i++ {
   280  		overlayLongLowerDir += fmt.Sprintf(":%s%d", lowerPath, i)
   281  	}
   282  
   283  	tests := []struct {
   284  		name     string
   285  		contents string
   286  		want     string
   287  		wantErr  bool
   288  	}{
   289  		{
   290  			name:     "empty",
   291  			contents: "",
   292  			wantErr:  true,
   293  		},
   294  		{
   295  			name: "v1",
   296  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   297  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   298  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   299  49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   300  54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   301  56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   302  58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   303  59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   304  `,
   305  			want: "/sys/fs/cgroup/cpu",
   306  		},
   307  		{
   308  			name: "v2",
   309  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   310  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   311  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   312  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   313  `,
   314  			want: "/sys/fs/cgroup",
   315  		},
   316  		{
   317  			name: "mixed",
   318  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   319  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   320  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   321  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   322  49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   323  54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   324  56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   325  58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   326  59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   327  `,
   328  			want: "/sys/fs/cgroup/cpu",
   329  		},
   330  		{
   331  			name: "v2-escaped",
   332  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   333  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   334  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   335  25 21 0:22 / /sys/fs/cgroup/tab\011tab rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   336  `,
   337  			want: `/sys/fs/cgroup/tab	tab`,
   338  		},
   339  		{
   340  			// Overly long line on a different mount doesn't matter.
   341  			name: "v2-longline",
   342  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   343  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   344  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   345  262 31 0:72 / /tmp/overlay2/0143e063b02f4801de9c847ad1c5ddc21fd2ead00653064d0c72ea967b248870/merged rw,relatime shared:729 - overlay overlay rw,lowerdir=` + overlayLongLowerDir + `,upperdir=/tmp/diff,workdir=/tmp/work
   346  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   347  `,
   348  			want: "/sys/fs/cgroup",
   349  		},
   350  	}
   351  
   352  	for _, tc := range tests {
   353  		t.Run(tc.name, func(t *testing.T) {
   354  			r := strings.NewReader(tc.contents)
   355  			read := func(fd int, b []byte) (int, uintptr) {
   356  				n, err := r.Read(b)
   357  				if err != nil && err != io.EOF {
   358  					const dummyErrno = 42
   359  					return n, dummyErrno
   360  				}
   361  				return n, 0
   362  			}
   363  
   364  			var got [cgroup.PathSize]byte
   365  			var scratch [cgroup.ParseSize]byte
   366  			n, err := cgroup.ParseCPUMount(0, read, got[:], scratch[:])
   367  			if (err != nil) != tc.wantErr {
   368  				t.Fatalf("parseCPUMount got err %v want %v", err, tc.wantErr)
   369  			}
   370  
   371  			if string(got[:n]) != tc.want {
   372  				t.Errorf("parseCPUMount got %q want %q", string(got[:n]), tc.want)
   373  			}
   374  		})
   375  	}
   376  }
   377  
   378  // escapePath performs escaping equivalent to Linux's show_path.
   379  //
   380  // That is, '\', ' ', '\t', and '\n' are converted to octal escape sequences,
   381  // like '\040' for space.
   382  func escapePath(s string) string {
   383  	out := make([]rune, 0, len(s))
   384  	for _, c := range s {
   385  		switch c {
   386  		case '\\', ' ', '\t', '\n':
   387  			out = append(out, '\\')
   388  			cs := strconv.FormatInt(int64(c), 8)
   389  			if len(cs) <= 2 {
   390  				out = append(out, '0')
   391  			}
   392  			if len(cs) <= 1 {
   393  				out = append(out, '0')
   394  			}
   395  			for _, csc := range cs {
   396  				out = append(out, csc)
   397  			}
   398  		default:
   399  			out = append(out, c)
   400  		}
   401  	}
   402  	return string(out)
   403  }
   404  
   405  func TestEscapePath(t *testing.T) {
   406  	tests := []struct {
   407  		name      string
   408  		unescaped string
   409  		escaped   string
   410  	}{
   411  		{
   412  			name:      "boring",
   413  			unescaped: `/a/b/c`,
   414  			escaped:   `/a/b/c`,
   415  		},
   416  		{
   417  			name:      "space",
   418  			unescaped: `/a/b b/c`,
   419  			escaped:   `/a/b\040b/c`,
   420  		},
   421  		{
   422  			name:      "tab",
   423  			unescaped: `/a/b	b/c`,
   424  			escaped:   `/a/b\011b/c`,
   425  		},
   426  		{
   427  			name: "newline",
   428  			unescaped: `/a/b
   429  b/c`,
   430  			escaped: `/a/b\012b/c`,
   431  		},
   432  		{
   433  			name:      "slash",
   434  			unescaped: `/a/b\b/c`,
   435  			escaped:   `/a/b\134b/c`,
   436  		},
   437  		{
   438  			name:      "beginning",
   439  			unescaped: `\b/c`,
   440  			escaped:   `\134b/c`,
   441  		},
   442  		{
   443  			name:      "ending",
   444  			unescaped: `/a/\`,
   445  			escaped:   `/a/\134`,
   446  		},
   447  	}
   448  
   449  	t.Run("escapePath", func(t *testing.T) {
   450  		for _, tc := range tests {
   451  			t.Run(tc.name, func(t *testing.T) {
   452  				got := escapePath(tc.unescaped)
   453  				if got != tc.escaped {
   454  					t.Errorf("escapePath got %q want %q", got, tc.escaped)
   455  				}
   456  			})
   457  		}
   458  	})
   459  
   460  	t.Run("unescapePath", func(t *testing.T) {
   461  		for _, tc := range tests {
   462  			t.Run(tc.name, func(t *testing.T) {
   463  				in := []byte(tc.escaped)
   464  				out := make([]byte, len(in))
   465  				n, err := cgroup.UnescapePath(out, in)
   466  				if err != nil {
   467  					t.Errorf("unescapePath got err %v want nil", err)
   468  				}
   469  				got := string(out[:n])
   470  				if got != tc.unescaped {
   471  					t.Errorf("unescapePath got %q want %q", got, tc.escaped)
   472  				}
   473  			})
   474  		}
   475  	})
   476  }
   477  

View as plain text