Source file src/internal/runtime/cgroup/cgroup_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  	"strings"
    12  	"testing"
    13  )
    14  
    15  func TestParseV1Number(t *testing.T) {
    16  	tests := []struct {
    17  		name     string
    18  		contents string
    19  		want     int64
    20  		wantErr  bool
    21  	}{
    22  		{
    23  			name:     "disabled",
    24  			contents: "-1\n",
    25  			want:     -1,
    26  		},
    27  		{
    28  			name:     "500000",
    29  			contents: "500000\n",
    30  			want:     500000,
    31  		},
    32  		{
    33  			name:     "MaxInt64",
    34  			contents: "9223372036854775807\n",
    35  			want:     9223372036854775807,
    36  		},
    37  		{
    38  			name:     "missing-newline",
    39  			contents: "500000",
    40  			wantErr:  true,
    41  		},
    42  		{
    43  			name:     "not-a-number",
    44  			contents: "123max\n",
    45  			wantErr:  true,
    46  		},
    47  		{
    48  			name:     "v2",
    49  			contents: "1000 5000\n",
    50  			wantErr:  true,
    51  		},
    52  	}
    53  
    54  	for _, tc := range tests {
    55  		t.Run(tc.name, func(t *testing.T) {
    56  			got, err := cgroup.ParseV1Number([]byte(tc.contents))
    57  			if tc.wantErr {
    58  				if err == nil {
    59  					t.Fatalf("parseV1Number got err nil want non-nil")
    60  				}
    61  				return
    62  			}
    63  			if err != nil {
    64  				t.Fatalf("parseV1Number got err %v want nil", err)
    65  			}
    66  
    67  			if got != tc.want {
    68  				t.Errorf("parseV1Number got %d want %d", got, tc.want)
    69  			}
    70  		})
    71  	}
    72  }
    73  
    74  func TestParseV2Limit(t *testing.T) {
    75  	tests := []struct {
    76  		name     string
    77  		contents string
    78  		want     float64
    79  		wantOK   bool
    80  		wantErr  bool
    81  	}{
    82  		{
    83  			name:     "disabled",
    84  			contents: "max 100000\n",
    85  			wantOK:   false,
    86  		},
    87  		{
    88  			name:     "5",
    89  			contents: "500000 100000\n",
    90  			want:     5,
    91  			wantOK:   true,
    92  		},
    93  		{
    94  			name:     "0.5",
    95  			contents: "50000 100000\n",
    96  			want:     0.5,
    97  			wantOK:   true,
    98  		},
    99  		{
   100  			name:     "2.5",
   101  			contents: "250000 100000\n",
   102  			want:     2.5,
   103  			wantOK:   true,
   104  		},
   105  		{
   106  			name:     "MaxInt64",
   107  			contents: "9223372036854775807 9223372036854775807\n",
   108  			want:     1,
   109  			wantOK:   true,
   110  		},
   111  		{
   112  			name:     "missing-newline",
   113  			contents: "500000 100000",
   114  			wantErr:  true,
   115  		},
   116  		{
   117  			name:     "v1",
   118  			contents: "500000\n",
   119  			wantErr:  true,
   120  		},
   121  		{
   122  			name:     "quota-not-a-number",
   123  			contents: "500000us 100000\n",
   124  			wantErr:  true,
   125  		},
   126  		{
   127  			name:     "period-not-a-number",
   128  			contents: "500000 100000us\n",
   129  			wantErr:  true,
   130  		},
   131  	}
   132  
   133  	for _, tc := range tests {
   134  		t.Run(tc.name, func(t *testing.T) {
   135  			got, gotOK, err := cgroup.ParseV2Limit([]byte(tc.contents))
   136  			if tc.wantErr {
   137  				if err == nil {
   138  					t.Fatalf("parseV1Limit got err nil want non-nil")
   139  				}
   140  				return
   141  			}
   142  			if err != nil {
   143  				t.Fatalf("parseV2Limit got err %v want nil", err)
   144  			}
   145  
   146  			if gotOK != tc.wantOK {
   147  				t.Errorf("parseV2Limit got ok %v want %v", gotOK, tc.wantOK)
   148  			}
   149  
   150  			if tc.wantOK && got != tc.want {
   151  				t.Errorf("parseV2Limit got %f want %f", got, tc.want)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func readString(contents string) func(fd int, b []byte) (int, uintptr) {
   158  	r := strings.NewReader(contents)
   159  	return func(fd int, b []byte) (int, uintptr) {
   160  		n, err := r.Read(b)
   161  		if err != nil && err != io.EOF {
   162  			const dummyErrno = 42
   163  			return n, dummyErrno
   164  		}
   165  		return n, 0
   166  	}
   167  }
   168  
   169  func TestParseCPUCgroup(t *testing.T) {
   170  	veryLongPathName := strings.Repeat("a", cgroup.PathSize+10)
   171  	evenLongerPathName := strings.Repeat("a", cgroup.ParseSize+10)
   172  
   173  	tests := []struct {
   174  		name     string
   175  		contents string
   176  		want     string
   177  		wantVer  cgroup.Version
   178  		wantErr  bool
   179  	}{
   180  		{
   181  			name:     "empty",
   182  			contents: "",
   183  			wantErr:  true,
   184  		},
   185  		{
   186  			name:     "too-long",
   187  			contents: "0::/" + veryLongPathName + "\n",
   188  			wantErr:  true,
   189  		},
   190  		{
   191  			name:     "too-long-line",
   192  			contents: "0::/" + evenLongerPathName + "\n",
   193  			wantErr:  true,
   194  		},
   195  		{
   196  			name: "v1",
   197  			contents: `2:cpu,cpuacct:/a/b/cpu
   198  1:blkio:/a/b/blkio
   199  `,
   200  			want:    "/a/b/cpu",
   201  			wantVer: cgroup.V1,
   202  		},
   203  		{
   204  			name:     "v2",
   205  			contents: "0::/a/b/c\n",
   206  			want:     "/a/b/c",
   207  			wantVer:  cgroup.V2,
   208  		},
   209  		{
   210  			name: "mixed",
   211  			contents: `2:cpu,cpuacct:/a/b/cpu
   212  1:blkio:/a/b/blkio
   213  0::/a/b/v2
   214  `,
   215  			want:    "/a/b/cpu",
   216  			wantVer: cgroup.V1,
   217  		},
   218  	}
   219  
   220  	for _, tc := range tests {
   221  		t.Run(tc.name, func(t *testing.T) {
   222  			var got [cgroup.PathSize]byte
   223  			var scratch [cgroup.ParseSize]byte
   224  			n, gotVer, err := cgroup.ParseCPUCgroup(0, readString(tc.contents), got[:], scratch[:])
   225  			if (err != nil) != tc.wantErr {
   226  				t.Fatalf("parseCPURelativePath got err %v want %v", err, tc.wantErr)
   227  			}
   228  
   229  			if gotVer != tc.wantVer {
   230  				t.Errorf("parseCPURelativePath got cgroup version %d want %d", gotVer, tc.wantVer)
   231  			}
   232  
   233  			if string(got[:n]) != tc.want {
   234  				t.Errorf("parseCPURelativePath got %q want %q", string(got[:n]), tc.want)
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestParseCPUCgroupMalformed(t *testing.T) {
   241  	for _, contents := range []string{
   242  		"\n",
   243  		"0\n",
   244  		"0:\n",
   245  		"0::\n",
   246  		"0::a\n",
   247  	} {
   248  		t.Run("", func(t *testing.T) {
   249  			var got [cgroup.PathSize]byte
   250  			var scratch [cgroup.ParseSize]byte
   251  			n, v, err := cgroup.ParseCPUCgroup(0, readString(contents), got[:], scratch[:])
   252  			if err != cgroup.ErrMalformedFile {
   253  				t.Errorf("ParseCPUCgroup got %q (v%d), %v, want ErrMalformedFile", string(got[:n]), v, err)
   254  			}
   255  		})
   256  	}
   257  }
   258  
   259  func TestContainsCPU(t *testing.T) {
   260  	tests := []struct {
   261  		in   string
   262  		want bool
   263  	}{
   264  		{
   265  			in:   "",
   266  			want: false,
   267  		},
   268  		{
   269  			in:   ",",
   270  			want: false,
   271  		},
   272  		{
   273  			in:   "cpu",
   274  			want: true,
   275  		},
   276  		{
   277  			in:   "memory,cpu",
   278  			want: true,
   279  		},
   280  		{
   281  			in:   "cpu,memory",
   282  			want: true,
   283  		},
   284  		{
   285  			in:   "memory,cpu,block",
   286  			want: true,
   287  		},
   288  		{
   289  			in:   "memory,cpuacct,block",
   290  			want: false,
   291  		},
   292  	}
   293  
   294  	for _, tc := range tests {
   295  		t.Run(tc.in, func(t *testing.T) {
   296  			got := cgroup.ContainsCPU([]byte(tc.in))
   297  			if got != tc.want {
   298  				t.Errorf("containsCPU(%q) got %v want %v", tc.in, got, tc.want)
   299  			}
   300  		})
   301  	}
   302  }
   303  
   304  func TestParseCPUMount(t *testing.T) {
   305  	// Used for v2-longline. We want an overlayfs mount to have an option
   306  	// so long that the entire line can't possibly fit in the scratch
   307  	// buffer.
   308  	const lowerPath = "/so/many/overlay/layers"
   309  	overlayLongLowerDir := lowerPath
   310  	for i := 0; len(overlayLongLowerDir) < cgroup.ScratchSize; i++ {
   311  		overlayLongLowerDir += fmt.Sprintf(":%s%d", lowerPath, i)
   312  	}
   313  
   314  	var longPath [4090]byte
   315  	for i := range longPath {
   316  		longPath[i] = byte(i)
   317  	}
   318  	escapedLongPath := escapePath(string(longPath[:]))
   319  	if len(escapedLongPath) <= cgroup.PathSize {
   320  		// ensure we actually support over PathSize long escaped path
   321  		t.Fatalf("escapedLongPath is too short to test")
   322  	}
   323  
   324  	tests := []struct {
   325  		name     string
   326  		contents string
   327  		cgroup   string
   328  		version  cgroup.Version
   329  		want     string
   330  		wantErr  bool
   331  	}{
   332  		{
   333  			name:     "empty",
   334  			contents: "",
   335  			wantErr:  true,
   336  		},
   337  		{
   338  			name:     "invalid-root",
   339  			contents: "56 22 0:40 /\\1 /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct\n",
   340  			cgroup:   "/",
   341  			version:  cgroup.V1,
   342  			wantErr:  true,
   343  		},
   344  		{
   345  			name:     "invalid-mount",
   346  			contents: "56 22 0:40 / /sys/fs/cgroup/\\1 rw - cgroup cgroup rw,cpu,cpuacct\n",
   347  			cgroup:   "/",
   348  			version:  cgroup.V1,
   349  			wantErr:  true,
   350  		},
   351  		{
   352  			name: "v1",
   353  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   354  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   355  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   356  49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   357  54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   358  56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   359  58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   360  59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   361  `,
   362  			cgroup:  "/",
   363  			version: cgroup.V1,
   364  			want:    "/sys/fs/cgroup/cpu",
   365  		},
   366  		{
   367  			name: "v2",
   368  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   369  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   370  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   371  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   372  `,
   373  			cgroup:  "/",
   374  			version: cgroup.V2,
   375  			want:    "/sys/fs/cgroup",
   376  		},
   377  		{
   378  			name: "mixed",
   379  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   380  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   381  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   382  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   383  49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   384  54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   385  56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   386  58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   387  59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   388  `,
   389  			cgroup:  "/",
   390  			version: cgroup.V1,
   391  			want:    "/sys/fs/cgroup/cpu",
   392  		},
   393  		{
   394  			name: "mixed-choose-v2",
   395  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   396  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   397  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   398  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   399  49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   400  54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   401  56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   402  58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   403  59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   404  `,
   405  			cgroup:  "/",
   406  			version: cgroup.V2,
   407  			want:    "/sys/fs/cgroup",
   408  		},
   409  		{
   410  			name: "v2-escaped",
   411  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   412  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   413  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   414  25 21 0:22 / /sys/fs/cgroup/tab\011tab rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   415  `,
   416  			cgroup:  "/",
   417  			version: cgroup.V2,
   418  			want:    `/sys/fs/cgroup/tab	tab`,
   419  		},
   420  		{
   421  			// Overly long line on a different mount doesn't matter.
   422  			name: "v2-longline",
   423  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   424  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   425  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   426  262 31 0:72 / /tmp/overlay2/0143e063b02f4801de9c847ad1c5ddc21fd2ead00653064d0c72ea967b248870/merged rw,relatime shared:729 - overlay overlay rw,lowerdir=` + overlayLongLowerDir + `,upperdir=/tmp/diff,workdir=/tmp/work
   427  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   428  `,
   429  			cgroup:  "/",
   430  			version: cgroup.V2,
   431  			want:    "/sys/fs/cgroup",
   432  		},
   433  		{
   434  			name: "long-escaped-path",
   435  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   436  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   437  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   438  25 21 0:22 / /sys/` + escapedLongPath + ` rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   439  `,
   440  			cgroup:  "/",
   441  			version: cgroup.V2,
   442  			want:    "/sys/" + string(longPath[:]),
   443  		},
   444  		{
   445  			name: "too-long-escaped-path",
   446  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   447  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   448  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   449  25 21 0:22 / /sys/` + escapedLongPath + ` rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   450  `,
   451  			cgroup:  "/container", // compared to above, this makes the path too long
   452  			version: cgroup.V2,
   453  			wantErr: true,
   454  		},
   455  		{
   456  			name: "non-root_mount",
   457  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   458  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   459  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   460  25 21 0:22 /sand /unrelated/cgroup1 rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   461  25 21 0:22 /stone /unrelated/cgroup2 rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   462  25 21 0:22 /sandbox/container/group /sys/fs/cgroup/mygroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   463  25 21 0:22 /sandbox /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   464  25 21 0:22 / /ignored/second/match rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   465  `,
   466  			cgroup:  "/sandbox/container",
   467  			version: cgroup.V2,
   468  			want:    "/sys/fs/cgroup/container",
   469  		},
   470  		{
   471  			name: "v2-escaped-root",
   472  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   473  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   474  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   475  25 21 0:22 /tab\011tab /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   476  `,
   477  			cgroup:  "/tab	tab/container",
   478  			version: cgroup.V2,
   479  			want:    `/sys/fs/cgroup/container`,
   480  		},
   481  		{
   482  			name: "non-root_cgroup",
   483  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   484  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   485  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   486  25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   487  `,
   488  			cgroup:  "/sandbox/container",
   489  			version: cgroup.V2,
   490  			want:    "/sys/fs/cgroup/sandbox/container",
   491  		},
   492  		{
   493  			name: "mixed_non-root",
   494  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   495  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   496  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   497  25 21 0:22 /sandbox /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw
   498  49 22 0:37 /sandbox /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory
   499  54 22 0:38 /sandbox /sys/fs/cgroup/io rw - cgroup cgroup rw,io
   500  56 22 0:40 /sand /unrelated/cgroup1 rw - cgroup cgroup rw,cpu,cpuacct
   501  56 22 0:40 /stone /unrelated/cgroup2 rw - cgroup cgroup rw,cpu,cpuacct
   502  56 22 0:40 /sandbox /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct
   503  56 22 0:40 /sandbox/container/group /sys/fs/cgroup/cpu/mygroup rw - cgroup cgroup rw,cpu,cpuacct
   504  56 22 0:40 / /ignored/second/match rw - cgroup cgroup rw,cpu,cpuacct
   505  58 22 0:42 /sandbox /sys/fs/cgroup/net rw - cgroup cgroup rw,net
   506  59 22 0:43 /sandbox /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset
   507  `,
   508  			cgroup:  "/sandbox/container",
   509  			version: cgroup.V1,
   510  			want:    "/sys/fs/cgroup/cpu/container",
   511  		},
   512  		{
   513  			// to see an example of this, for a PID in a cgroup namespace, run:
   514  			// nsenter -t <PID> -C -- cat /proc/self/cgroup
   515  			// nsenter -t <PID> -C -- grep cgroup /proc/self/mountinfo
   516  			// /mnt can be generated with `mount --bind /sys/fs/cgroup/kubepods.slice /mnt`,
   517  			// assuming PID is in cgroup /kubepods.slice
   518  			name: "out_of_namespace",
   519  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   520  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   521  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   522  1243 61 0:26 /../../.. /mnt rw,nosuid,nodev,noexec,relatime shared:4 - cgroup2 cgroup2 rw
   523  29 22 0:26 /../../../.. /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:4 - cgroup2 cgroup2 rw`,
   524  			cgroup:  "/../../../../init.scope",
   525  			version: cgroup.V2,
   526  			want:    "/sys/fs/cgroup/init.scope",
   527  		},
   528  		{
   529  			name: "out_of_namespace-root", // the process is directly in the root cgroup
   530  			contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw
   531  20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw
   532  21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw
   533  1243 61 0:26 /../../.. /mnt rw,nosuid,nodev,noexec,relatime shared:4 - cgroup2 cgroup2 rw
   534  29 22 0:26 /../../../.. /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:4 - cgroup2 cgroup2 rw`,
   535  			cgroup:  "/../../../..",
   536  			version: cgroup.V2,
   537  			want:    "/sys/fs/cgroup",
   538  		},
   539  	}
   540  
   541  	for _, tc := range tests {
   542  		t.Run(tc.name, func(t *testing.T) {
   543  			var got [cgroup.PathSize]byte
   544  			var scratch [cgroup.ParseSize]byte
   545  			n := copy(got[:], tc.cgroup)
   546  			n, err := cgroup.ParseCPUMount(0, readString(tc.contents), got[:],
   547  				got[:n], tc.version, scratch[:])
   548  			if (err != nil) != tc.wantErr {
   549  				t.Fatalf("parseCPUMount got err %v want %v", err, tc.wantErr)
   550  			}
   551  
   552  			if string(got[:n]) != tc.want {
   553  				t.Errorf("parseCPUMount got %q want %q", string(got[:n]), tc.want)
   554  			}
   555  		})
   556  	}
   557  }
   558  
   559  func TestParseCPUMountMalformed(t *testing.T) {
   560  	for _, contents := range []string{
   561  		"\n",
   562  		"22\n",
   563  		"22 1 8:1\n",
   564  		"22 1 8:1 /\n",
   565  		"22 1 8:1 / /cgroup\n",
   566  		"22 1 8:1 / /cgroup rw\n",
   567  		"22 1 8:1 / /cgroup rw -\n",
   568  		"22 1 8:1 / /cgroup rw - \n",
   569  		"22 1 8:1 / /cgroup rw - cgroup\n",
   570  		"22 1 8:1 / /cgroup rw - cgroup cgroup\n",
   571  		"22 1 8:1 a /cgroup rw - cgroup cgroup cpu\n",
   572  	} {
   573  		t.Run("", func(t *testing.T) {
   574  			var got [cgroup.PathSize]byte
   575  			var scratch [cgroup.ParseSize]byte
   576  			n, err := cgroup.ParseCPUMount(0, readString(contents), got[:], []byte("/"), cgroup.V1, scratch[:])
   577  			if err != cgroup.ErrMalformedFile {
   578  				t.Errorf("parseCPUMount got %q, %v, want ErrMalformedFile", string(got[:n]), err)
   579  			}
   580  		})
   581  	}
   582  }
   583  
   584  // escapePath performs escaping equivalent to Linux's show_path.
   585  //
   586  // That is, '\', ' ', '\t', and '\n' are converted to octal escape sequences,
   587  // like '\040' for space.
   588  func escapePath(s string) string {
   589  	out := make([]byte, 0, len(s))
   590  	for _, c := range []byte(s) {
   591  		switch c {
   592  		case '\\', ' ', '\t', '\n':
   593  			out = fmt.Appendf(out, "\\%03o", c)
   594  		default:
   595  			out = append(out, c)
   596  		}
   597  	}
   598  	return string(out)
   599  }
   600  
   601  func TestEscapePath(t *testing.T) {
   602  	tests := []struct {
   603  		name      string
   604  		unescaped string
   605  		escaped   string
   606  	}{
   607  		{
   608  			name:      "boring",
   609  			unescaped: `/a/b/c`,
   610  			escaped:   `/a/b/c`,
   611  		},
   612  		{
   613  			name:      "space",
   614  			unescaped: `/a/b b/c`,
   615  			escaped:   `/a/b\040b/c`,
   616  		},
   617  		{
   618  			name:      "tab",
   619  			unescaped: `/a/b	b/c`,
   620  			escaped:   `/a/b\011b/c`,
   621  		},
   622  		{
   623  			name: "newline",
   624  			unescaped: `/a/b
   625  b/c`,
   626  			escaped: `/a/b\012b/c`,
   627  		},
   628  		{
   629  			name:      "slash",
   630  			unescaped: `/a/b\b/c`,
   631  			escaped:   `/a/b\134b/c`,
   632  		},
   633  		{
   634  			name:      "beginning",
   635  			unescaped: `\b/c`,
   636  			escaped:   `\134b/c`,
   637  		},
   638  		{
   639  			name:      "ending",
   640  			unescaped: `/a/\`,
   641  			escaped:   `/a/\134`,
   642  		},
   643  		{
   644  			name:      "non-utf8",
   645  			unescaped: "/a/b\xff\x20/c",
   646  			escaped:   "/a/b\xff\\040/c",
   647  		},
   648  	}
   649  
   650  	t.Run("escapePath", func(t *testing.T) {
   651  		for _, tc := range tests {
   652  			t.Run(tc.name, func(t *testing.T) {
   653  				got := escapePath(tc.unescaped)
   654  				if got != tc.escaped {
   655  					t.Errorf("escapePath got %q want %q", got, tc.escaped)
   656  				}
   657  			})
   658  		}
   659  	})
   660  
   661  	t.Run("unescapePath", func(t *testing.T) {
   662  		for _, tc := range tests {
   663  			runTest := func(in, out []byte) {
   664  				n, err := cgroup.UnescapePath(out, in)
   665  				if err != nil {
   666  					t.Errorf("unescapePath got err %v want nil", err)
   667  				}
   668  				got := string(out[:n])
   669  				if got != tc.unescaped {
   670  					t.Errorf("unescapePath got %q want %q", got, tc.escaped)
   671  				}
   672  			}
   673  			t.Run(tc.name, func(t *testing.T) {
   674  				in := []byte(tc.escaped)
   675  				out := make([]byte, len(in))
   676  				runTest(in, out)
   677  			})
   678  			t.Run("inplace/"+tc.name, func(t *testing.T) {
   679  				in := []byte(tc.escaped)
   680  				runTest(in, in)
   681  			})
   682  		}
   683  	})
   684  }
   685  
   686  func TestUnescapeInvalidPath(t *testing.T) {
   687  	for _, in := range []string{
   688  		`/a/b\c`,
   689  		`/a/b\01`,
   690  		`/a/b\018`,
   691  		`/a/b\01c`,
   692  		`/a/b\777`,
   693  		`01234567890123456789`,                 // too long
   694  		`\001\002\003\004\005\006\007\010\011`, // too long
   695  	} {
   696  		out := make([]byte, 8)
   697  		t.Run(in, func(t *testing.T) {
   698  			_, err := cgroup.UnescapePath(out, []byte(in))
   699  			if err == nil {
   700  				t.Errorf("unescapePath got nil err, want non-nil")
   701  			}
   702  		})
   703  	}
   704  }
   705  

View as plain text