cgroup_linux.mx raw

   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
   6  
   7  import (
   8  	"internal/bytealg"
   9  	"internal/runtime/strconv"
  10  	"internal/runtime/syscall"
  11  )
  12  
  13  var (
  14  	ErrNoCgroup error = stringError("not in a cgroup")
  15  
  16  	errMalformedFile error = stringError("malformed file")
  17  )
  18  
  19  const _PATH_MAX = 4096
  20  
  21  const (
  22  	// Required amount of scratch space for CPULimit.
  23  	//
  24  	// TODO(prattmic): This is shockingly large (~70KiB) due to the (very
  25  	// unlikely) combination of extremely long paths consisting mostly
  26  	// escaped characters. The scratch buffer ends up in .bss in package
  27  	// runtime, so it doesn't contribute to binary size and generally won't
  28  	// be faulted in, but it would still be nice to shrink this. A more
  29  	// complex parser that did not need to keep entire lines in memory
  30  	// could get away with much less. Alternatively, we could do a one-off
  31  	// mmap allocation for this buffer, which is only mapped larger if we
  32  	// actually need the extra space.
  33  	ScratchSize = PathSize + ParseSize
  34  
  35  	// Required space to store a path of the cgroup in the filesystem.
  36  	PathSize = _PATH_MAX
  37  
  38  	// /proc/self/mountinfo path escape sequences are 4 characters long, so
  39  	// a path consisting entirely of escaped characters could be 4 times
  40  	// larger.
  41  	escapedPathMax = 4 * _PATH_MAX
  42  
  43  	// Required space to parse /proc/self/mountinfo and /proc/self/cgroup.
  44  	// See findCPUMount and findCPURelativePath.
  45  	ParseSize = 4 * escapedPathMax
  46  )
  47  
  48  // Include explicit NUL to be sure we include it in the slice.
  49  const (
  50  	v2MaxFile    = "/cpu.max\x00"
  51  	v1QuotaFile  = "/cpu.cfs_quota_us\x00"
  52  	v1PeriodFile = "/cpu.cfs_period_us\x00"
  53  )
  54  
  55  // Version indicates the cgroup version.
  56  type Version int
  57  
  58  const (
  59  	VersionUnknown Version = iota
  60  	V1
  61  	V2
  62  )
  63  
  64  // CPU owns the FDs required to read the CPU limit from a cgroup.
  65  type CPU struct {
  66  	version Version
  67  
  68  	// For cgroup v1, this is cpu.cfs_quota_us.
  69  	// For cgroup v2, this is cpu.max.
  70  	quotaFD int
  71  
  72  	// For cgroup v1, this is cpu.cfs_period_us.
  73  	// For cgroup v2, this is unused.
  74  	periodFD int
  75  }
  76  
  77  func (c CPU) Close() {
  78  	switch c.version {
  79  	case V1:
  80  		syscall.Close(c.quotaFD)
  81  		syscall.Close(c.periodFD)
  82  	case V2:
  83  		syscall.Close(c.quotaFD)
  84  	default:
  85  		throw("impossible cgroup version")
  86  	}
  87  }
  88  
  89  func checkBufferSize(s []byte, size int) {
  90  	if len(s) != size {
  91  		println("runtime: cgroup buffer length", len(s), "want", size)
  92  		throw("runtime: cgroup invalid buffer length")
  93  	}
  94  }
  95  
  96  // OpenCPU returns a CPU for the CPU cgroup containing the current process, or
  97  // ErrNoCgroup if the process is not in a CPU cgroup.
  98  //
  99  // scratch must have length ScratchSize.
 100  func OpenCPU(scratch []byte) (CPU, error) {
 101  	checkBufferSize(scratch, ScratchSize)
 102  
 103  	base := scratch[:PathSize]
 104  	scratch2 := scratch[PathSize:]
 105  
 106  	n, version, err := FindCPU(base, scratch2)
 107  	if err != nil {
 108  		return CPU{}, err
 109  	}
 110  
 111  	switch version {
 112  	case 1:
 113  		n2 := copy(base[n:], v1QuotaFile)
 114  		path := base[:n+n2]
 115  		quotaFD, errno := syscall.Open(&path[0], syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
 116  		if errno != 0 {
 117  			// This may fail if this process was migrated out of
 118  			// the cgroup found by FindCPU and that cgroup has been
 119  			// deleted.
 120  			return CPU{}, errSyscallFailed
 121  		}
 122  
 123  		n2 = copy(base[n:], v1PeriodFile)
 124  		path = base[:n+n2]
 125  		periodFD, errno := syscall.Open(&path[0], syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
 126  		if errno != 0 {
 127  			// This may fail if this process was migrated out of
 128  			// the cgroup found by FindCPU and that cgroup has been
 129  			// deleted.
 130  			return CPU{}, errSyscallFailed
 131  		}
 132  
 133  		c := CPU{
 134  			version:  1,
 135  			quotaFD:  quotaFD,
 136  			periodFD: periodFD,
 137  		}
 138  		return c, nil
 139  	case 2:
 140  		n2 := copy(base[n:], v2MaxFile)
 141  		path := base[:n+n2]
 142  		maxFD, errno := syscall.Open(&path[0], syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
 143  		if errno != 0 {
 144  			// This may fail if this process was migrated out of
 145  			// the cgroup found by FindCPU and that cgroup has been
 146  			// deleted.
 147  			return CPU{}, errSyscallFailed
 148  		}
 149  
 150  		c := CPU{
 151  			version:  2,
 152  			quotaFD:  maxFD,
 153  			periodFD: -1,
 154  		}
 155  		return c, nil
 156  	default:
 157  		throw("impossible cgroup version")
 158  		panic("unreachable")
 159  	}
 160  }
 161  
 162  // Returns average CPU throughput limit from the cgroup, or ok false if there
 163  // is no limit.
 164  func ReadCPULimit(c CPU) (float64, bool, error) {
 165  	switch c.version {
 166  	case 1:
 167  		quota, err := readV1Number(c.quotaFD)
 168  		if err != nil {
 169  			return 0, false, errMalformedFile
 170  		}
 171  
 172  		if quota < 0 {
 173  			// No limit.
 174  			return 0, false, nil
 175  		}
 176  
 177  		period, err := readV1Number(c.periodFD)
 178  		if err != nil {
 179  			return 0, false, errMalformedFile
 180  		}
 181  
 182  		return float64(quota) / float64(period), true, nil
 183  	case 2:
 184  		// quotaFD is the cpu.max FD.
 185  		return readV2Limit(c.quotaFD)
 186  	default:
 187  		throw("impossible cgroup version")
 188  		panic("unreachable")
 189  	}
 190  }
 191  
 192  // Returns the value from the quota/period file.
 193  func readV1Number(fd int) (int64, error) {
 194  	// The format of the file is "<value>\n" where the value is in
 195  	// int64 microseconds and, if quota, may be -1 to indicate no limit.
 196  	//
 197  	// MaxInt64 requires 19 bytes to display in base 10, thus the
 198  	// conservative max size of this file is 19 + 1 (newline) = 20 bytes.
 199  	// We'll provide a bit more for good measure.
 200  	//
 201  	// Always read from the beginning of the file to get a fresh value.
 202  	var b [64]byte
 203  	n, errno := syscall.Pread(fd, b[:], 0)
 204  	if errno != 0 {
 205  		return 0, errSyscallFailed
 206  	}
 207  	if n == len(b) {
 208  		return 0, errMalformedFile
 209  	}
 210  
 211  	buf := b[:n]
 212  	return parseV1Number(buf)
 213  }
 214  
 215  func parseV1Number(buf []byte) (int64, error) {
 216  	// Ignore trailing newline.
 217  	i := bytealg.IndexByte(buf, '\n')
 218  	if i < 0 {
 219  		return 0, errMalformedFile
 220  	}
 221  	buf = buf[:i]
 222  
 223  	val, ok := strconv.Atoi64(string(buf))
 224  	if !ok {
 225  		return 0, errMalformedFile
 226  	}
 227  
 228  	return val, nil
 229  }
 230  
 231  // Returns CPU throughput limit, or ok false if there is no limit.
 232  func readV2Limit(fd int) (float64, bool, error) {
 233  	// The format of the file is "<quota> <period>\n" where quota and
 234  	// period are microseconds and quota may be "max" to indicate no limit.
 235  	//
 236  	// Note that the kernel is inconsistent about whether the values are
 237  	// uint64 or int64: values are parsed as uint64 but printed as int64.
 238  	// See kernel/sched/core.c:cpu_max_{show,write}.
 239  	//
 240  	// In practice, the kernel limits the period to 1s (1000000us) (see
 241  	// max_cfs_quota_period), and the quota to (1<<44)us (see
 242  	// max_cfs_runtime), so these values can't get large enough for the
 243  	// distinction to matter.
 244  	//
 245  	// MaxInt64 requires 19 bytes to display in base 10, thus the
 246  	// conservative max size of this file is 19 + 19 + 1 (space) + 1
 247  	// (newline) = 40 bytes. We'll provide a bit more for good measure.
 248  	//
 249  	// Always read from the beginning of the file to get a fresh value.
 250  	var b [64]byte
 251  	n, errno := syscall.Pread(fd, b[:], 0)
 252  	if errno != 0 {
 253  		return 0, false, errSyscallFailed
 254  	}
 255  	if n == len(b) {
 256  		return 0, false, errMalformedFile
 257  	}
 258  
 259  	buf := b[:n]
 260  	return parseV2Limit(buf)
 261  }
 262  
 263  func parseV2Limit(buf []byte) (float64, bool, error) {
 264  	i := bytealg.IndexByte(buf, ' ')
 265  	if i < 0 {
 266  		return 0, false, errMalformedFile
 267  	}
 268  
 269  	quotaStr := buf[:i]
 270  	if bytealg.Compare(quotaStr, []byte("max")) == 0 {
 271  		// No limit.
 272  		return 0, false, nil
 273  	}
 274  
 275  	periodStr := buf[i+1:]
 276  	// Ignore trailing newline, if any.
 277  	i = bytealg.IndexByte(periodStr, '\n')
 278  	if i < 0 {
 279  		return 0, false, errMalformedFile
 280  	}
 281  	periodStr = periodStr[:i]
 282  
 283  	quota, ok := strconv.Atoi64(string(quotaStr))
 284  	if !ok {
 285  		return 0, false, errMalformedFile
 286  	}
 287  
 288  	period, ok := strconv.Atoi64(string(periodStr))
 289  	if !ok {
 290  		return 0, false, errMalformedFile
 291  	}
 292  
 293  	return float64(quota) / float64(period), true, nil
 294  }
 295  
 296  // FindCPU finds the path to the CPU cgroup that this process is a member of
 297  // and places it in out. scratch is a scratch buffer for internal use.
 298  //
 299  // out must have length PathSize. scratch must have length ParseSize.
 300  //
 301  // Returns the number of bytes written to out and the cgroup version (1 or 2).
 302  //
 303  // Returns ErrNoCgroup if the process is not in a CPU cgroup.
 304  func FindCPU(out []byte, scratch []byte) (int, Version, error) {
 305  	checkBufferSize(out, PathSize)
 306  	checkBufferSize(scratch, ParseSize)
 307  
 308  	// The cgroup path is <cgroup mount point> + <relative path>.
 309  	//
 310  	// This is racy if our cgroup is changed while this runs. For example,
 311  	// initially there is only a cgroup v2 mount and we are not in a
 312  	// cgroup. After, there a cgroup v1 mount with a CPU controller and we
 313  	// are placed in a cgroup in this hierarchy. In that case, findCPUMount
 314  	// could pick the v2 mount, and findCPURelativePath could find the v2
 315  	// relative path.
 316  	//
 317  	// In this case we'll later fail to read the cgroup files and fall back
 318  	// to assuming no cgroup.
 319  
 320  	n, err := FindCPUMountPoint(out, scratch)
 321  	if err != nil {
 322  		return 0, 0, err
 323  	}
 324  
 325  	// The relative path always starts with /, so we can directly append it
 326  	// to the mount point.
 327  	n2, version, err := FindCPURelativePath(out[n:], scratch)
 328  	if err != nil {
 329  		return 0, 0, err
 330  	}
 331  	n += n2
 332  
 333  	return n, version, nil
 334  }
 335  
 336  // FindCPURelativePath finds the path to the CPU cgroup that this process is a member of
 337  // relative to the root of the cgroup mount and places it in out. scratch is a
 338  // scratch buffer for internal use.
 339  //
 340  // out must have length PathSize minus the size of the cgroup mount root (if
 341  // known). scratch must have length ParseSize.
 342  //
 343  // Returns the number of bytes written to out and the cgroup version (1 or 2).
 344  //
 345  // Returns ErrNoCgroup if the process is not in a CPU cgroup.
 346  func FindCPURelativePath(out []byte, scratch []byte) (int, Version, error) {
 347  	path := []byte("/proc/self/cgroup\x00")
 348  	fd, errno := syscall.Open(&path[0], syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
 349  	if errno == syscall.ENOENT {
 350  		return 0, 0, ErrNoCgroup
 351  	} else if errno != 0 {
 352  		return 0, 0, errSyscallFailed
 353  	}
 354  
 355  	// The relative path always starts with /, so we can directly append it
 356  	// to the mount point.
 357  	n, version, err := parseCPURelativePath(fd, syscall.Read, out[:], scratch)
 358  	if err != nil {
 359  		syscall.Close(fd)
 360  		return 0, 0, err
 361  	}
 362  
 363  	syscall.Close(fd)
 364  	return n, version, nil
 365  }
 366  
 367  // Finds the path of the current process's CPU cgroup relative to the cgroup
 368  // mount and writes it to out.
 369  //
 370  // Returns the number of bytes written and the cgroup version (1 or 2).
 371  func parseCPURelativePath(fd int, read func(fd int, b []byte) (int, uintptr), out []byte, scratch []byte) (int, Version, error) {
 372  	// The format of each line is
 373  	//
 374  	//   hierarchy-ID:controller-list:cgroup-path
 375  	//
 376  	// controller-list is comma-separated.
 377  	// See man 5 cgroup for more details.
 378  	//
 379  	// cgroup v2 has hierarchy-ID 0. If a v1 hierarchy contains "cpu", that
 380  	// is the CPU controller. Otherwise the v2 hierarchy (if any) is the
 381  	// CPU controller.
 382  	//
 383  	// hierarchy-ID and controller-list have relatively small maximum
 384  	// sizes, and the path can be up to _PATH_MAX, so we need a bit more
 385  	// than 1 _PATH_MAX of scratch space.
 386  
 387  	l := newLineReader(fd, scratch, read)
 388  
 389  	// Bytes written to out.
 390  	n := 0
 391  
 392  	for {
 393  		err := l.next()
 394  		if err == errIncompleteLine {
 395  			// Don't allow incomplete lines. While in theory the
 396  			// incomplete line may be for a controller we don't
 397  			// care about, in practice all lines should be of
 398  			// similar length, so we should just have a buffer big
 399  			// enough for any.
 400  			return 0, 0, err
 401  		} else if err == errEOF {
 402  			break
 403  		} else if err != nil {
 404  			return 0, 0, err
 405  		}
 406  
 407  		line := l.line()
 408  
 409  		// The format of each line is
 410  		//
 411  		//   hierarchy-ID:controller-list:cgroup-path
 412  		//
 413  		// controller-list is comma-separated.
 414  		// See man 5 cgroup for more details.
 415  		i := bytealg.IndexByte(line, ':')
 416  		if i < 0 {
 417  			return 0, 0, errMalformedFile
 418  		}
 419  
 420  		hierarchy := line[:i]
 421  		line = line[i+1:]
 422  
 423  		i = bytealg.IndexByte(line, ':')
 424  		if i < 0 {
 425  			return 0, 0, errMalformedFile
 426  		}
 427  
 428  		controllers := line[:i]
 429  		line = line[i+1:]
 430  
 431  		path := line
 432  
 433  		if string(hierarchy) == "0" {
 434  			// v2 hierarchy.
 435  			n = copy(out, path)
 436  			// Keep searching, we might find a v1 hierarchy with a
 437  			// CPU controller, which takes precedence.
 438  		} else {
 439  			// v1 hierarchy
 440  			if containsCPU(controllers) {
 441  				// Found a v1 CPU controller. This must be the
 442  				// only one, so we're done.
 443  				return copy(out, path), V1, nil
 444  			}
 445  		}
 446  	}
 447  
 448  	if n == 0 {
 449  		// Found nothing.
 450  		return 0, 0, ErrNoCgroup
 451  	}
 452  
 453  	// Must be v2, v1 returns above.
 454  	return n, V2, nil
 455  }
 456  
 457  // Returns true if comma-separated list b contains "cpu".
 458  func containsCPU(b []byte) bool {
 459  	for len(b) > 0 {
 460  		i := bytealg.IndexByte(b, ',')
 461  		if i < 0 {
 462  			// Neither cmd/compile nor gccgo allocates for these string conversions.
 463  			return string(b) == "cpu"
 464  		}
 465  
 466  		curr := b[:i]
 467  		rest := b[i+1:]
 468  
 469  		if string(curr) == "cpu" {
 470  			return true
 471  		}
 472  
 473  		b = rest
 474  	}
 475  
 476  	return false
 477  }
 478  
 479  // FindCPUMountPoint finds the root of the CPU cgroup mount places it in out.
 480  // scratch is a scratch buffer for internal use.
 481  //
 482  // out must have length PathSize. scratch must have length ParseSize.
 483  //
 484  // Returns the number of bytes written to out.
 485  //
 486  // Returns ErrNoCgroup if the process is not in a CPU cgroup.
 487  func FindCPUMountPoint(out []byte, scratch []byte) (int, error) {
 488  	checkBufferSize(out, PathSize)
 489  	checkBufferSize(scratch, ParseSize)
 490  
 491  	path := []byte("/proc/self/mountinfo\x00")
 492  	fd, errno := syscall.Open(&path[0], syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
 493  	if errno == syscall.ENOENT {
 494  		return 0, ErrNoCgroup
 495  	} else if errno != 0 {
 496  		return 0, errSyscallFailed
 497  	}
 498  
 499  	n, err := parseCPUMount(fd, syscall.Read, out, scratch)
 500  	if err != nil {
 501  		syscall.Close(fd)
 502  		return 0, err
 503  	}
 504  	syscall.Close(fd)
 505  
 506  	return n, nil
 507  }
 508  
 509  // Returns the mount point for the cpu cgroup controller (v1 or v2) from
 510  // /proc/self/mountinfo.
 511  func parseCPUMount(fd int, read func(fd int, b []byte) (int, uintptr), out []byte, scratch []byte) (int, error) {
 512  	// The format of each line is:
 513  	//
 514  	// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 515  	// (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
 516  	//
 517  	// (1) mount ID:  unique identifier of the mount (may be reused after umount)
 518  	// (2) parent ID:  ID of parent (or of self for the top of the mount tree)
 519  	// (3) major:minor:  value of st_dev for files on filesystem
 520  	// (4) root:  root of the mount within the filesystem
 521  	// (5) mount point:  mount point relative to the process's root
 522  	// (6) mount options:  per mount options
 523  	// (7) optional fields:  zero or more fields of the form "tag[:value]"
 524  	// (8) separator:  marks the end of the optional fields
 525  	// (9) filesystem type:  name of filesystem of the form "type[.subtype]"
 526  	// (10) mount source:  filesystem specific information or "none"
 527  	// (11) super options:  per super block options
 528  	//
 529  	// See man 5 proc_pid_mountinfo for more details.
 530  	//
 531  	// Note that emitted paths will not contain space, tab, newline, or
 532  	// carriage return. Those are escaped. See Linux show_mountinfo ->
 533  	// show_path. We must unescape before returning.
 534  	//
 535  	// We return the mount point (5) if the filesystem type (9) is cgroup2,
 536  	// or cgroup with "cpu" in the super options (11).
 537  	//
 538  	// (4), (5), and (10) are up to _PATH_MAX. The remaining fields have a
 539  	// small fixed maximum size, so 4*_PATH_MAX is plenty of scratch space.
 540  	// Note that non-cgroup mounts may have arbitrarily long (11), but we
 541  	// can skip those when parsing.
 542  
 543  	l := newLineReader(fd, scratch, read)
 544  
 545  	// Bytes written to out.
 546  	n := 0
 547  
 548  	for {
 549  		//incomplete := false
 550  		err := l.next()
 551  		if err == errIncompleteLine {
 552  			// An incomplete line is fine as long as it doesn't
 553  			// impede parsing the fields we need. It shouldn't be
 554  			// possible for any mount to use more than 3*PATH_MAX
 555  			// before (9) because there are two paths and all other
 556  			// earlier fields have bounded options. Only (11) has
 557  			// unbounded options.
 558  		} else if err == errEOF {
 559  			break
 560  		} else if err != nil {
 561  			return 0, err
 562  		}
 563  
 564  		line := l.line()
 565  
 566  		// Skip first four fields.
 567  		for range 4 {
 568  			i := bytealg.IndexByte(line, ' ')
 569  			if i < 0 {
 570  				return 0, errMalformedFile
 571  			}
 572  			line = line[i+1:]
 573  		}
 574  
 575  		// (5) mount point:  mount point relative to the process's root
 576  		i := bytealg.IndexByte(line, ' ')
 577  		if i < 0 {
 578  			return 0, errMalformedFile
 579  		}
 580  		mnt := line[:i]
 581  		line = line[i+1:]
 582  
 583  		// Skip ahead past optional fields, delimited by " - ".
 584  		for {
 585  			i = bytealg.IndexByte(line, ' ')
 586  			if i < 0 {
 587  				return 0, errMalformedFile
 588  			}
 589  			if i+3 >= len(line) {
 590  				return 0, errMalformedFile
 591  			}
 592  			delim := line[i : i+3]
 593  			if string(delim) == " - " {
 594  				line = line[i+3:]
 595  				break
 596  			}
 597  			line = line[i+1:]
 598  		}
 599  
 600  		// (9) filesystem type:  name of filesystem of the form "type[.subtype]"
 601  		i = bytealg.IndexByte(line, ' ')
 602  		if i < 0 {
 603  			return 0, errMalformedFile
 604  		}
 605  		ftype := line[:i]
 606  		line = line[i+1:]
 607  
 608  		if string(ftype) != "cgroup" && string(ftype) != "cgroup2" {
 609  			continue
 610  		}
 611  
 612  		// As in findCPUPath, cgroup v1 with a CPU controller takes
 613  		// precendence over cgroup v2.
 614  		if string(ftype) == "cgroup2" {
 615  			// v2 hierarchy.
 616  			n, err = unescapePath(out, mnt)
 617  			if err != nil {
 618  				// Don't keep searching on error. The kernel
 619  				// should never produce broken escaping.
 620  				return n, err
 621  			}
 622  			// Keep searching, we might find a v1 hierarchy with a
 623  			// CPU controller, which takes precedence.
 624  			continue
 625  		}
 626  
 627  		// (10) mount source:  filesystem specific information or "none"
 628  		i = bytealg.IndexByte(line, ' ')
 629  		if i < 0 {
 630  			return 0, errMalformedFile
 631  		}
 632  		// Don't care about mount source.
 633  		line = line[i+1:]
 634  
 635  		// (11) super options:  per super block options
 636  		superOpt := line
 637  
 638  		// v1 hierarchy
 639  		if containsCPU(superOpt) {
 640  			// Found a v1 CPU controller. This must be the
 641  			// only one, so we're done.
 642  			return unescapePath(out, mnt)
 643  		}
 644  	}
 645  
 646  	if n == 0 {
 647  		// Found nothing.
 648  		return 0, ErrNoCgroup
 649  	}
 650  
 651  	return n, nil
 652  }
 653  
 654  var errInvalidEscape error = stringError("invalid path escape sequence")
 655  
 656  // unescapePath copies in to out, unescaping escape sequences generated by
 657  // Linux's show_path.
 658  //
 659  // That is, '\', ' ', '\t', and '\n' are converted to octal escape sequences,
 660  // like '\040' for space.
 661  //
 662  // out must be at least as large as in.
 663  //
 664  // Returns the number of bytes written to out.
 665  //
 666  // Also see escapePath in cgroup_linux_test.go.
 667  func unescapePath(out []byte, in []byte) (int, error) {
 668  	// Not strictly necessary, but simplifies the implementation and will
 669  	// always hold in users.
 670  	if len(out) < len(in) {
 671  		throw("output too small")
 672  	}
 673  
 674  	var outi, ini int
 675  	for ini < len(in) {
 676  		c := in[ini]
 677  		if c != '\\' {
 678  			out[outi] = c
 679  			outi++
 680  			ini++
 681  			continue
 682  		}
 683  
 684  		// Start of escape sequence.
 685  
 686  		// Escape sequence is always 4 characters: one slash and three
 687  		// digits.
 688  		if ini+3 >= len(in) {
 689  			return outi, errInvalidEscape
 690  		}
 691  
 692  		var outc byte
 693  		for i := range 3 {
 694  			c := in[ini+1+i]
 695  			if c < '0' || c > '9' {
 696  				return outi, errInvalidEscape
 697  			}
 698  
 699  			outc *= 8
 700  			outc += c - '0'
 701  		}
 702  
 703  		out[outi] = outc
 704  		outi++
 705  
 706  		ini += 4
 707  	}
 708  
 709  	return outi, nil
 710  }
 711