├── go.work ├── bpf ├── handler-bpfeb.o ├── handler-bpfel.o ├── update_vmlinux.sh ├── vmlinux_core.h ├── update_bpf.sh ├── handler.c └── bpf_helpers.h ├── ci ├── images │ └── clang-13 │ │ ├── llvm.gpg │ │ ├── llvm.list │ │ └── Dockerfile └── scripts │ ├── clang_image.sh │ ├── shellcheck.sh │ ├── check_unstaged.sh │ ├── clang.sh │ ├── build_handler.sh │ └── version.sh ├── .clang-tidy ├── .editorconfig ├── .gitattributes ├── .prettierrc ├── tracer_other.go ├── .gitignore ├── bench ├── go.mod ├── bench.sh └── tracer_linux_test.go ├── bpf_eb.go ├── bpf_el.go ├── .github └── workflows │ ├── gen.yaml │ ├── enterprise.yaml │ ├── enterprise-release.yaml │ └── quality.yaml ├── go.mod ├── enterprise ├── Dockerfile ├── go.mod ├── scripts │ └── image_tag.sh ├── README.md ├── cmd │ └── exectrace │ │ └── main.go ├── templates │ ├── kubernetes │ │ ├── README.md │ │ └── main.tf │ └── kubernetes-envbox │ │ ├── README.md │ │ └── main.tf ├── exectrace.go └── exectrace_test.go ├── LICENSE.MIT ├── LICENSE.enterprise ├── Makefile ├── cmd └── exectrace │ └── main.go ├── tracer.go ├── bpf.go ├── go.sum ├── Makefile.enterprise ├── README.md ├── .golangci.yaml ├── tracer_linux_test.go ├── tracer_linux.go ├── LICENSE.GPL └── LICENSE /go.work: -------------------------------------------------------------------------------- 1 | go 1.24 2 | 3 | use ( 4 | ./ 5 | ./bench 6 | ./enterprise 7 | ) 8 | -------------------------------------------------------------------------------- /bpf/handler-bpfeb.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/exectrace/HEAD/bpf/handler-bpfeb.o -------------------------------------------------------------------------------- /bpf/handler-bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/exectrace/HEAD/bpf/handler-bpfel.o -------------------------------------------------------------------------------- /ci/images/clang-13/llvm.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/exectrace/HEAD/ci/images/clang-13/llvm.gpg -------------------------------------------------------------------------------- /ci/images/clang-13/llvm.list: -------------------------------------------------------------------------------- 1 | deb [signed-by=/usr/share/keyrings/llvm.gpg] http://apt.llvm.org/focal llvm-toolchain-focal-13 main 2 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: "clang-diagnostic-*,clang-analyzer-*,-*,cert-*,linuxkernel-*,clang-analyzer-*,llvm-*,performance-*,portability-*,readability-*" 2 | WarningsAsErrors: "*" 3 | -------------------------------------------------------------------------------- /ci/scripts/clang_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script builds the clang builder image used by clang.sh 4 | 5 | set -euo pipefail 6 | cd "$(dirname "$0")/../images/clang-13" 7 | 8 | docker build -t exectrace-clang-13 . 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [{*.go,*.c,*.h,Makefile}] 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [{*.ya?ml,*.json,.prettierrc] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /ci/scripts/shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs shellcheck on all `.sh` files in the repository. 4 | 5 | set -euo pipefail 6 | cd "$(dirname "$0")" 7 | cd "$(git rev-parse --show-toplevel)" 8 | 9 | shopt -s globstar nullglob 10 | shellcheck ./**/*.sh 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | bpf/bpf_core_read.h linguist-generated=true 2 | bpf/bpf_helper_defs.h linguist-generated=true 3 | bpf/bpf_helpers.h linguist-generated=true 4 | bpf/handler-bpfeb.o linguist-generated=true 5 | bpf/handler-bpfel.o linguist-generated=true 6 | bpf/vmlinux.h linguist-generated=true 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "overrides": [ 6 | { 7 | "files": ["./*.md", "./**/*.md"], 8 | "options": { 9 | "printWidth": 80, 10 | "proseWrap": "always" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tracer_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package exectrace 5 | 6 | import ( 7 | "runtime" 8 | 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | // New is not supported on OSes other than Linux. 13 | func New(_ *TracerOpts) (Tracer, error) { 14 | return nil, xerrors.Errorf(`%q is an unsupported OS, only "linux" is supported`, runtime.GOOS) 15 | } 16 | -------------------------------------------------------------------------------- /ci/images/clang-13/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN set -eux && \ 6 | apt update && \ 7 | apt install -y \ 8 | apt-transport-https \ 9 | ca-certificates \ 10 | gnupg 11 | 12 | # Install clang-13 13 | COPY llvm.gpg /usr/share/keyrings/llvm.gpg 14 | COPY llvm.list /etc/apt/sources.list.d/llvm.list 15 | RUN set -eux && \ 16 | apt update && \ 17 | apt install -y \ 18 | clang-13 \ 19 | clang-tools-13 \ 20 | clang-tidy-13 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Prerequisites 15 | *.d 16 | 17 | # Object files 18 | *.o 19 | !bpf/handler-*.o 20 | *.obj 21 | *.elf 22 | 23 | # Editor folders 24 | .vscode 25 | 26 | # Make outputs 27 | .clang-image 28 | 29 | build 30 | 31 | *.tfstate 32 | *.tfstate.backup 33 | *.tfplan 34 | *.lock.hcl 35 | .terraform/ 36 | 37 | /exectrace 38 | -------------------------------------------------------------------------------- /bench/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coder/exectrace/bench 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/DataDog/ebpfbench v0.0.0-20230731170724-14eabe1f3e9a 7 | github.com/coder/exectrace v0.2.4 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/cilium/ebpf v0.10.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/hashicorp/errwrap v1.1.0 // indirect 15 | github.com/hashicorp/go-multierror v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/sys v0.4.0 // indirect 18 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /bpf_eb.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && arm64be) || (linux && armbe) || (linux && mips) || (linux && mips64) || (linux && mips64p32) || (linux && ppc64) || (linux && s390) || (linux && s390x) || (linux && sparc) || (linux && sparc64) 2 | // +build linux,arm64be linux,armbe linux,mips linux,mips64 linux,mips64p32 linux,ppc64 linux,s390 linux,s390x linux,sparc linux,sparc64 3 | 4 | package exectrace 5 | 6 | import ( 7 | _ "embed" 8 | "encoding/binary" 9 | ) 10 | 11 | // The native endian of the processor this program was compiled for. 12 | var NativeEndian = binary.BigEndian 13 | 14 | // The compiled BPF program on big endian processors. 15 | // 16 | //go:embed bpf/handler-bpfeb.o 17 | var bpfProgram []byte 18 | -------------------------------------------------------------------------------- /bpf_el.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && 386) || (linux && amd64) || (linux && amd64p32) || (linux && arm) || (linux && arm64) || (linux && mips64le) || (linux && mips64p32le) || (linux && mipsle) || (linux && ppc64le) || (linux && riscv64) 2 | // +build linux,386 linux,amd64 linux,amd64p32 linux,arm linux,arm64 linux,mips64le linux,mips64p32le linux,mipsle linux,ppc64le linux,riscv64 3 | 4 | package exectrace 5 | 6 | import ( 7 | _ "embed" 8 | "encoding/binary" 9 | ) 10 | 11 | // The native endian of the processor this program was compiled for. 12 | var NativeEndian = binary.LittleEndian 13 | 14 | // The compiled BPF program on little endian processors. 15 | // 16 | //go:embed bpf/handler-bpfel.o 17 | var bpfProgram []byte 18 | -------------------------------------------------------------------------------- /bpf/update_vmlinux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file updates the vmlinux.h file in this directory to match the current 4 | # kernel. This is OK to ship for many different kernel versions because we use 5 | # CO:RE. 6 | # 7 | # Depends on bpftool compiled against the current kernel. 8 | 9 | set -euo pipefail 10 | cd "$(dirname "$0")" 11 | 12 | vmlinux="./vmlinux.h" 13 | 14 | # Attach the license header and source URL. 15 | cat < "$vmlinux" 16 | // This file was generated on the following system: 17 | // $(uname -a) 18 | // On $(date). 19 | // 20 | // Kernel headers licensed under GPL-2.0. 21 | 22 | EOF 23 | 24 | bpftool btf dump file /sys/kernel/btf/vmlinux format c >> "$vmlinux" 25 | 26 | # Remove extra trailing newlines. 27 | sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$vmlinux" 28 | -------------------------------------------------------------------------------- /bench/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Runs go benchmarks in a new PID namespace. Depends on `go` and `unshare`. 4 | # 5 | # Usage: COUNT=10000 ./bench.sh 6 | # Runs `go test -bench=. -run="^#" -count 1 -benchtime "${COUNT:-1000}x"` ./ 7 | # 8 | # Usage: ./bench.sh -bench=... ... 9 | # Runs `go test "$@"`. COUNT is ignored. 10 | 11 | set -euo pipefail 12 | 13 | cd "$(dirname "$0")" 14 | 15 | args=("$@") 16 | if [[ "${#args[@]}" -eq 0 ]]; then 17 | args=('-bench=.' '-run="^#"' '-count=1' "-benchtime=${COUNT:-1000}x" ./) 18 | fi 19 | 20 | # Start the go test process in a new PID namespace and exec with sudo. 21 | uid=$(id -u) 22 | gid=$(id -g) 23 | go_binary=$(command -v go) 24 | set -x 25 | exec sudo -E unshare --pid --fork --setuid "$uid" --setgid "$gid" -- "$go_binary" test -exec sudo "${args[@]}" 26 | -------------------------------------------------------------------------------- /.github/workflows/gen.yaml: -------------------------------------------------------------------------------- 1 | name: gen 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*" 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | workflow_dispatch: 15 | 16 | permissions: 17 | actions: none 18 | checks: none 19 | contents: read 20 | deployments: none 21 | issues: none 22 | packages: none 23 | pull-requests: none 24 | repository-projects: none 25 | security-events: none 26 | statuses: none 27 | 28 | jobs: 29 | handler-elf: 30 | name: handler-elf 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Run make 37 | run: make 38 | 39 | - name: Check for unstaged files 40 | run: ./ci/scripts/check_unstaged.sh 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coder/exectrace 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.14.0 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 9 | github.com/spf13/cobra v1.8.0 10 | github.com/stretchr/testify v1.9.0 11 | golang.org/x/sys v0.19.0 12 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/hashicorp/errwrap v1.1.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/spf13/pflag v1.0.5 // indirect 21 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /ci/scripts/check_unstaged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script returns 1 and a bunch of log output if there are unstaged files in 4 | # the repository. This is mostly used to make sure there are no changes after 5 | # running fmt or gen steps in CI. 6 | 7 | set -euo pipefail 8 | cd "$(dirname "$0")" 9 | cd "$(git rev-parse --show-toplevel)" 10 | 11 | FILES="$(git ls-files --other --modified --exclude-standard)" 12 | if [[ "$FILES" != "" ]]; then 13 | mapfile -t files <<< "$FILES" 14 | 15 | echo "The following files contain unstaged changes:" 16 | echo 17 | for file in "${files[@]}" 18 | do 19 | echo " - $file" 20 | done 21 | echo 22 | 23 | echo "These are the changes:" 24 | echo 25 | for file in "${files[@]}" 26 | do 27 | git --no-pager diff "$file" 28 | done 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /bpf/vmlinux_core.h: -------------------------------------------------------------------------------- 1 | // This is file is based on the Linux Kernel headers and is licensed under 2 | // GPL-2.0. 3 | // 4 | // Contains CO-RE structs that are adapted and shrunk down from vmlinux.h. 5 | 6 | #ifndef __VMLINUX_CORE_H__ 7 | #define __VMLINUX_CORE_H__ 8 | 9 | struct task_struct___exectrace { 10 | struct nsproxy___exectrace *nsproxy; 11 | } __attribute__((preserve_access_index)); 12 | 13 | struct nsproxy___exectrace { 14 | struct pid_namespace___exectrace *pid_ns_for_children; 15 | } __attribute__((preserve_access_index)); 16 | 17 | struct ns_common___exectrace { 18 | __u32 inum; 19 | } __attribute__((preserve_access_index)); 20 | 21 | struct pid_namespace___exectrace { 22 | struct pid_namespace___exectrace *parent; 23 | struct ns_common___exectrace ns; 24 | } __attribute__((preserve_access_index)); 25 | 26 | #endif /* __VMLINUX_CORE_H__ */ 27 | -------------------------------------------------------------------------------- /ci/scripts/clang.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs the given program with the given arguments in a Docker 4 | # container. The container starts inside the `bpf` directory and runs as the 5 | # same user on the host to avoid permissions problems. 6 | # 7 | # You must call ./clang_image.sh first (Make handles this for you). 8 | # 9 | # Usage: clang.sh clang-13 ... -c in.c -o out.o 10 | 11 | set -euo pipefail 12 | cd "$(dirname "$0")" 13 | 14 | # Only use the "-t" flag if we're in a TTY. This is useful for development as 15 | # clang outputs colors in a terminal. 16 | terminal_flags="-i" 17 | if [ -t 0 ]; then 18 | terminal_flags="-it" 19 | fi 20 | 21 | docker run \ 22 | "$terminal_flags" \ 23 | --rm \ 24 | --hostname exectrace \ 25 | --name "exectrace_build_$RANDOM" \ 26 | --user "$(id -u):$(id -g)" \ 27 | --volume "$(git rev-parse --show-toplevel):/repo" \ 28 | --net none \ 29 | --workdir "/repo/bpf" \ 30 | exectrace-clang-13 \ 31 | "$@" 32 | -------------------------------------------------------------------------------- /enterprise/Dockerfile: -------------------------------------------------------------------------------- 1 | # This is a multi-arch Dockerfile, so it cannot have ANY "RUN" commands as it's 2 | # cross-compiled. All binaries are cross-compiled using the host's Go toolchain 3 | # and then copied into the build context. 4 | # 5 | # This uses Ubuntu instead of Alpine Linux because the binaries are compiled 6 | # with cgo due to dependencies requiring cgo. 7 | FROM ubuntu:latest 8 | 9 | # LABEL doesn't add any real layers so it's fine (and easier) to do it here than 10 | # in the build script. 11 | ARG CODER_VERSION 12 | LABEL \ 13 | org.opencontainers.image.title="Coder v2 Exectrace" \ 14 | org.opencontainers.image.description="A tool for tracing launched processes inside Coder workspaces." \ 15 | org.opencontainers.image.url="https://github.com/coder/exectrace/enterprise" \ 16 | org.opencontainers.image.source="https://github.com/coder/exectrace/enterprise" \ 17 | org.opencontainers.image.version="$CODER_VERSION" 18 | 19 | COPY exectrace /opt/exectrace 20 | 21 | USER 0:0 22 | WORKDIR / 23 | 24 | ENTRYPOINT [ "/opt/exectrace", "run" ] 25 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2022 Coder Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bpf/update_bpf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file updates the libbpf header files in this directory. 4 | 5 | set -euo pipefail 6 | cd "$(dirname "$0")" 7 | 8 | # Version of libbpf to fetch headers from. 9 | LIBBPF_VERSION="${LIBBPF_VERSION:-0.8.2}" 10 | 11 | # The headers we want to download from the repo. These files are found in the 12 | # src/ directory in the repo. 13 | header_files=( 14 | "bpf_core_read.h" 15 | "bpf_helper_defs.h" 16 | "bpf_helpers.h" 17 | ) 18 | 19 | for f in "${header_files[@]}"; do 20 | # Attach the license header and source URL. 21 | cat < "$f" 22 | // This file is taken from libbpf v$LIBBPF_VERSION. 23 | // https://github.com/libbpf/libbpf/blob/v$LIBBPF_VERSION/src/$f 24 | // 25 | // Licensed under LGPL 2.1 or the BSD 2 Clause. 26 | 27 | EOF 28 | 29 | echo "+ Downloading $f" >&2 30 | curl -sSL "https://raw.githubusercontent.com/libbpf/libbpf/v$LIBBPF_VERSION/src/$f" >> "$f" 31 | 32 | # Remove extra trailing newlines. 33 | sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$f" 34 | 35 | # Change any <...> includes to "..." and remove bpf/ prefix. 36 | sed -i -E -e 's/#include <(.*)>/#include "\1"/g' -e 's/#include "bpf\//#include "/g' "$f" 37 | done 38 | -------------------------------------------------------------------------------- /enterprise/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coder/exectrace/enterprise 2 | 3 | go 1.24 4 | 5 | replace github.com/coder/exectrace => ../ 6 | 7 | require ( 8 | cdr.dev/slog v1.4.1 9 | github.com/coder/exectrace v0.2.4 10 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 11 | github.com/stretchr/testify v1.9.0 12 | github.com/urfave/cli/v2 v2.23.7 13 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 14 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 15 | ) 16 | 17 | require ( 18 | github.com/cilium/ebpf v0.14.0 // indirect 19 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/go-logr/logr v1.4.1 // indirect 22 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 23 | github.com/google/go-cmp v0.6.0 // indirect 24 | github.com/hashicorp/errwrap v1.1.0 // indirect 25 | github.com/hashicorp/go-multierror v1.1.1 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 28 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 29 | go.opencensus.io v0.24.0 // indirect 30 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect 31 | golang.org/x/sys v0.19.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | k8s.io/klog/v2 v2.120.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE.enterprise: -------------------------------------------------------------------------------- 1 | ## Acceptance 2 | 3 | By using any software and associated documentation files under Coder 4 | Technologies Inc.’s ("Coder") directory named "enterprise" ("Enterprise 5 | Software"), you agree to all of the terms and conditions below. 6 | 7 | ## Copyright License 8 | 9 | The licensor grants you a non-exclusive, royalty-free, worldwide, 10 | non-sublicensable, non-transferable license to use, copy, distribute, make 11 | available, modify and prepare derivative works of the Enterprise Software, in 12 | each case subject to the limitations and conditions below. 13 | 14 | ## Limitations 15 | 16 | You may not move, change, disable, or circumvent the license key functionality 17 | in the software, and you may not remove or obscure any functionality in the 18 | software that is protected by the license key. 19 | 20 | You may not alter, remove, or obscure any licensing, copyright, or other notices 21 | of the licensor in the software. 22 | 23 | You agree that Coder and/or its licensors (as applicable) retain all right, 24 | title and interest in and to all such modifications and/or patches. 25 | 26 | ## Additional Terms 27 | 28 | This Enterprise Software may only be used in production, if you (and any entity 29 | that you represent) have agreed to, and are in compliance with, the Coder’s 30 | Terms of Service, available at https://coder.com/legal/terms-of-service, or 31 | other agreement governing the use of the Software, as agreed by you and Coder. 32 | -------------------------------------------------------------------------------- /ci/scripts/build_handler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script builds the given eBPF handler binary in a Docker container. The 4 | # output file is put into the `bpf` directory. 5 | # 6 | # Usage: build_handler.sh handler-bpfel.o 7 | 8 | set -euo pipefail 9 | cd "$(dirname "$0")" 10 | 11 | output="$(basename "$1")" 12 | target="${output#handler-}" 13 | target="${target%.o}" 14 | 15 | if [[ "$target" != "bpfeb" ]] && [[ "$target" != "bpfel" ]]; then 16 | echo "Sniffed build target '$target' from input '$output' is invalid" 17 | exit 1 18 | fi 19 | 20 | # Run clang with the following options: 21 | # -O2: 22 | # Optimize the code so the BTF verifier can understand it properly. 23 | # -mcpu=v1: 24 | # Clang defaults to mcpu=probe which checks the kernel that we are 25 | # compiling on. This isn't appropriate for ahead of time compiled code so 26 | # force the most compatible version. 27 | # -g: 28 | # We always want BTF to be generated, so enforce debug symbols. 29 | # -Wall -Wextra -Werror: 30 | # Enable lots of warnings, and treat all warnings as fatal build errors. 31 | # -fno-ident: 32 | # Don't include the clang version. 33 | # -fdebug-compilation-dir .: 34 | # Don't output the current directory into debug info. 35 | # -target 36 | # This is set to bpfeb or bpfel based on the build target. 37 | ./clang.sh clang-13 \ 38 | -O2 \ 39 | -mcpu=v1 \ 40 | -g \ 41 | -Wall -Wextra -Werror \ 42 | -fno-ident \ 43 | -fdebug-compilation-dir . \ 44 | -target "$target" \ 45 | -c ./handler.c \ 46 | -o "$output" 47 | -------------------------------------------------------------------------------- /enterprise/scripts/image_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script prints the image tag to use for the given arch and version 4 | # combination. 5 | # 6 | # Usage: ./image_tag.sh [--arch amd64] [--version 1.2.3] 7 | # 8 | # The --arch parameter accepts a Golang arch specification. If not specified, 9 | # the image tag for the multi-arch image will be returned instead. 10 | # 11 | # If no version is specified, defaults to the version from ./version.sh. If the 12 | # supplied version is "latest", no `v` prefix will be added to the tag. 13 | # 14 | # The returned tag will be sanitized to remove invalid characters like the plus 15 | # sign. 16 | 17 | set -euo pipefail 18 | cd "$(dirname "$0")/.." 19 | 20 | arch="" 21 | version="" 22 | 23 | args="$(getopt -o "" -l arch:,version: -- "$@")" 24 | eval set -- "$args" 25 | while true; do 26 | case "$1" in 27 | --arch) 28 | arch="$2" 29 | shift 2 30 | ;; 31 | --version) 32 | version="$2" 33 | shift 2 34 | ;; 35 | --) 36 | shift 37 | break 38 | ;; 39 | *) 40 | echo "Unrecognized option: $1" 41 | exit 1 42 | ;; 43 | esac 44 | done 45 | 46 | # Remove the "v" prefix because we don't want to add it twice. 47 | version="${version#v}" 48 | if [[ "$version" == "" ]]; then 49 | version="$(../ci/scripts/version.sh)" 50 | fi 51 | 52 | image="${CODER_IMAGE_BASE:-ghcr.io/coder/exectrace}" 53 | tag="v$version" 54 | if [[ "$version" == "latest" ]]; then 55 | tag="latest" 56 | fi 57 | if [[ "$arch" != "" ]]; then 58 | tag+="-$arch" 59 | fi 60 | 61 | # Dev versions contain plus signs which are illegal characters in Docker tags. 62 | tag="${tag//+/-}" 63 | echo "$image:$tag" 64 | -------------------------------------------------------------------------------- /ci/scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script generates the version string used by exectrace v2, including for 4 | # dev versions. Note: the version returned by this script will NOT include the 5 | # "v" prefix that is included in the Git tag. 6 | # 7 | # If $CODER_RELEASE is set to "true", the returned version will equal the 8 | # current git tag. If the current commit is not tagged, this will fail. 9 | # 10 | # If $CODER_RELEASE is not set, the returned version will always be a dev 11 | # version. 12 | 13 | set -euo pipefail 14 | cd "$(dirname "$0")/.." 15 | 16 | if [[ "${CODER_FORCE_VERSION:-}" != "" ]]; then 17 | echo "$CODER_FORCE_VERSION" 18 | exit 0 19 | fi 20 | 21 | last_tag="$(git describe --tags --abbrev=0)" 22 | version="$last_tag" 23 | 24 | # If the HEAD has extra commits since the last tag then we are in a dev version. 25 | # 26 | # Dev versions are denoted by the "-devel+" suffix with a trailing commit short 27 | # SHA. 28 | if [[ "${CODER_RELEASE:-}" == *t* ]]; then 29 | # $last_tag will equal `git describe --always` if we currently have the tag 30 | # checked out. 31 | if [[ "$last_tag" != "$(git describe --always)" ]]; then 32 | # make won't exit on $(shell cmd) failures, so we have to kill it :( 33 | if [[ "$(ps -o comm= "$PPID" || true)" == *make* ]]; then 34 | echo "ERROR: version.sh: the current commit is not tagged with an annotated tag" 35 | kill "$PPID" || true 36 | exit 1 37 | fi 38 | 39 | echo "version.sh: the current commit is not tagged with an annotated tag" 40 | exit 1 41 | fi 42 | else 43 | version+="-devel+$(git rev-parse --short HEAD)" 44 | fi 45 | 46 | # Remove the "v" prefix. 47 | echo "${version#v}" 48 | -------------------------------------------------------------------------------- /.github/workflows/enterprise.yaml: -------------------------------------------------------------------------------- 1 | # This workflow file is adapted from coder/coder. 2 | name: enterprise 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | pull_request: 10 | 11 | workflow_dispatch: 12 | 13 | permissions: 14 | actions: none 15 | checks: none 16 | contents: read 17 | deployments: none 18 | issues: none 19 | packages: none 20 | pull-requests: none 21 | repository-projects: none 22 | security-events: none 23 | statuses: none 24 | 25 | # Cancel in-progress runs for pull requests when developers push additional 26 | # changes 27 | concurrency: 28 | group: ${{ github.workflow }}-${{ github.ref }} 29 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 30 | 31 | jobs: 32 | test-go-enterprise: 33 | name: "test/go-enterprise" 34 | runs-on: ubuntu-latest 35 | timeout-minutes: 20 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Install Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version: "^1.24.5" 44 | 45 | - name: Echo Go Cache Paths 46 | id: go-cache-paths 47 | run: | 48 | echo "::set-output name=go-build::$(go env GOCACHE)" 49 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 50 | 51 | - name: Go Build Cache 52 | uses: actions/cache@v4 53 | with: 54 | path: ${{ steps.go-cache-paths.outputs.go-build }} 55 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.**', '**.go') }} 56 | 57 | - name: Go Mod Cache 58 | uses: actions/cache@v4 59 | with: 60 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 61 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 62 | 63 | - name: Test 64 | run: make test/go-enterprise 65 | -------------------------------------------------------------------------------- /enterprise/README.md: -------------------------------------------------------------------------------- 1 | # Exectrace for Coder v2 2 | 3 | A wrapper around Coder's open source 4 | [exectrace](https://github.com/coder/exectrace) library for providing workspace 5 | process logging in Kubernetes on Linux for 6 | [Coder v2](https://github.com/coder/coder). 7 | 8 | > Note: The enterprise directory of this repo, including the exectrace binary 9 | > and image, are enterprise-licensed. Workspace process logging is an enterprise 10 | > feature in Coder v2. 11 | 12 | > If you are looking for documentation on how to use workspace process logging 13 | > in Coder v1, please refer to the 14 | > [documentation](https://coder.com/docs/coder/latest/admin/workspace-management/process-logging) 15 | > and reach out to us if you need any assistance. 16 | 17 | This works by creating a sidecar inside the same Linux process namespace and 18 | logging all processes created inside the namespace, even processes in nested 19 | namespaces (i.e. from Docker containers). 20 | 21 | ## Usage in Kubernetes 22 | 23 | Use the Kubernetes template in the [templates/kubernetes](templates/kubernetes) 24 | directory as your starting point. This template is similar to the 25 | [kubernetes](./templates/kubernetes) template shipped with Coder. 26 | 27 | The main changes are: 28 | 29 | - Adds some shell code before starting the Coder agent to submit the process ID 30 | namespace inum to the sibling container, which ensures that workspace startup 31 | waits for the exectrace container to be running. 32 | - Adds the exectrace container to the pod. 33 | 34 | ## Usage in Kubernetes with Envbox 35 | 36 | Same as above, but use the [kubernetes-envbox](./templates/kubernetes-envbox) 37 | template instead. 38 | 39 | ## Usage outside of Kubernetes 40 | 41 | This binary/image only supports Kubernetes, although technically it can be made 42 | to work outside of Kubernetes by ensuring the workspace and the sidecar are 43 | inside the same process ID namespace. Support for usage outside of Kubernetes is 44 | not offered, but please reach out to us if you need this outside of Kubernetes 45 | and we will see what we can do. 46 | 47 | ## License 48 | 49 | Coder Enterprise license. See [LICENSE.enterprise](../LICENSE.enterprise). 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := handlers 2 | 3 | # Use a single bash shell for each job, and immediately exit on failure 4 | SHELL := bash 5 | .SHELLFLAGS := -ceu 6 | .ONESHELL: 7 | 8 | # This doesn't work on directories. 9 | # See https://stackoverflow.com/questions/25752543/make-delete-on-error-for-directory-targets 10 | .DELETE_ON_ERROR: 11 | 12 | # Don't print the commands in the file unless you specify VERBOSE. This is 13 | # essentially the same as putting "@" at the start of each line. 14 | ifndef VERBOSE 15 | .SILENT: 16 | endif 17 | 18 | CXX = clang-13 19 | 20 | .PHONY: handlers 21 | handlers: bpf/handler-bpfeb.o bpf/handler-bpfel.o 22 | 23 | .PHONY: clean 24 | clean: clean-enterprise 25 | rm -rf bpf/handler-bpfeb.o bpf/handler-bpfel.o 26 | 27 | ci/.clang-image: ci/images/clang-13/Dockerfile ci/scripts/clang_image.sh 28 | ./ci/scripts/clang_image.sh 29 | touch ci/.clang-image 30 | 31 | # bpfeb is big endian, bpfel is little endian. 32 | bpf/handler-bpfeb.o bpf/handler-bpfel.o: bpf/*.h bpf/*.c ci/.clang-image ci/scripts/build_handler.sh 33 | ./ci/scripts/build_handler.sh "$(@F)" 34 | 35 | .PHONY: fmt 36 | fmt: fmt/go fmt/prettier 37 | 38 | .PHONY: fmt/go 39 | fmt/go: 40 | go fmt ./... 41 | cd enterprise 42 | go fmt ./... 43 | 44 | .PHONY: fmt/prettier 45 | fmt/prettier: 46 | # Config file: .prettierrc 47 | prettier -w . 48 | 49 | .PHONY: lint 50 | lint: lint/go lint/c lint/shellcheck 51 | 52 | .PHONY: lint/go 53 | lint/go: lint/go/linux lint/go/other 54 | 55 | .PHONY: lint/go/linux 56 | lint/go/linux: 57 | # Config file: .golangci.yml 58 | golangci-lint run ./... 59 | cd enterprise 60 | golangci-lint run ./... 61 | 62 | .PHONY: lint/go/other 63 | lint/go/other: 64 | # The windows and darwin builds include the same files. 65 | # Config file: .golangci.yml 66 | GOOS=windows golangci-lint run ./... 67 | # Enterprise dir does not support Windows or Darwin. 68 | 69 | .PHONY: lint/c 70 | lint/c: ci/.clang-image 71 | # Config file: .clang-tidy 72 | ./ci/scripts/clang.sh clang-tidy-13 --config-file ../.clang-tidy ./handler.c 73 | 74 | .PHONY: lint/shellcheck 75 | lint/shellcheck: 76 | ./ci/scripts/shellcheck.sh 77 | 78 | .PHONY: test 79 | test: test/go test/go-enterprise 80 | 81 | .PHONY: test/go 82 | test/go: 83 | go test -exec sudo -v -count 1 ./... 84 | 85 | .PHONY: test/go-enterprise 86 | test/go-enterprise: 87 | cd enterprise 88 | go test -exec sudo -v -count 1 ./... 89 | 90 | .PHONY: bench 91 | bench: 92 | go clean -testcache 93 | COUNT=10000 ./bench/bench.sh 94 | 95 | include Makefile.enterprise 96 | -------------------------------------------------------------------------------- /.github/workflows/enterprise-release.yaml: -------------------------------------------------------------------------------- 1 | name: enterprise-release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | workflow_dispatch: 7 | inputs: 8 | snapshot: 9 | description: Force a dev version to be generated, implies dry_run. 10 | type: boolean 11 | required: true 12 | dry_run: 13 | description: Perform a dry-run release. 14 | type: boolean 15 | required: true 16 | 17 | permissions: 18 | # Required to publish a release 19 | contents: write 20 | # Necessary to push docker images to ghcr.io. 21 | packages: write 22 | 23 | env: 24 | CODER_RELEASE: ${{ github.event.inputs.snapshot && 'false' || 'true' }} 25 | 26 | jobs: 27 | release: 28 | runs-on: ubuntu-latest 29 | env: 30 | # Necessary for Docker manifest 31 | DOCKER_CLI_EXPERIMENTAL: "enabled" 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | 38 | # If the event that triggered the build was an annotated tag (which our 39 | # tags are supposed to be), actions/checkout has a bug where the tag in 40 | # question is only a lightweight tag and not a full annotated tag. This 41 | # command seems to fix it. 42 | # https://github.com/actions/checkout/issues/290 43 | - name: Fetch git tags 44 | run: git fetch --tags --force 45 | 46 | - name: Docker Login 47 | uses: docker/login-action@v3 48 | with: 49 | registry: ghcr.io 50 | username: ${{ github.actor }} 51 | password: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Install Go 54 | uses: actions/setup-go@v5 55 | with: 56 | go-version: "^1.24.5" 57 | 58 | - name: Build binaries 59 | run: | 60 | set -euxo pipefail 61 | go mod download 62 | 63 | version=$(./ci/scripts/version.sh) 64 | make -j \ 65 | enterprise/build/exectrace_"$version"_linux_{amd64,arm64,armv7} 66 | 67 | - name: Build Docker images 68 | run: | 69 | set -euxo pipefail 70 | 71 | version=$(./ci/scripts/version.sh) 72 | make -j \ 73 | enterprise/build/exectrace_"$version"_linux_{amd64,arm64,armv7}.tag 74 | 75 | - name: Push Docker images 76 | if: ${{ !github.event.inputs.snapshot && !github.event.inputs.dry_run }} 77 | run: | 78 | set -euxo pipefail 79 | 80 | version=$(./ci/scripts/version.sh) 81 | make -j \ 82 | enterprise/build/exectrace_"$version"_linux_{amd64,arm64,armv7}.tag.pushed 83 | 84 | - name: Build and push multi-arch Docker image 85 | if: ${{ !github.event.inputs.snapshot && !github.event.inputs.dry_run }} 86 | run: | 87 | set -euxo pipefail 88 | 89 | version=$(./ci/scripts/version.sh) 90 | make -j \ 91 | enterprise/build/exectrace_{"$version",latest}_linux.tag.pushed 92 | -------------------------------------------------------------------------------- /cmd/exectrace/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | 13 | "github.com/kballard/go-shellquote" 14 | "github.com/spf13/cobra" 15 | "golang.org/x/xerrors" 16 | 17 | "github.com/coder/exectrace" 18 | ) 19 | 20 | func main() { 21 | err := rootCmd().Execute() 22 | if err != nil { 23 | log.Fatalf("failed to run command: %+v", err) 24 | } 25 | } 26 | 27 | func rootCmd() *cobra.Command { 28 | var ( 29 | pidNS uint32 30 | outputFormat string 31 | ) 32 | 33 | var cmd = &cobra.Command{ 34 | Use: "exectrace", 35 | Short: "exectrace logs all exec calls on the system.", 36 | Run: func(_ *cobra.Command, _ []string) { 37 | if outputFormat != "text" && outputFormat != "json" { 38 | //nolint:revive 39 | log.Fatalf(`output format must be "text" or "json", got %q`, outputFormat) 40 | } 41 | 42 | err := run(pidNS, outputFormat) 43 | if err != nil { 44 | //nolint:revive 45 | log.Fatalf("run exectrace: %+v", err) 46 | } 47 | }, 48 | } 49 | 50 | cmd.Flags().Uint32VarP(&pidNS, "pid-ns", "p", 0, "PID NS ID to filter events from, you can get this by doing `readlink /proc/self/ns/pid`") 51 | cmd.Flags().StringVarP(&outputFormat, "output", "f", "text", "Output format, text or json") 52 | 53 | return cmd 54 | } 55 | 56 | func run(pidNS uint32, outputFormat string) error { 57 | t, err := exectrace.New(&exectrace.TracerOpts{ 58 | PidNS: pidNS, 59 | // We use the default LogFn since it logs all the details to stderr. 60 | }) 61 | if err != nil { 62 | return xerrors.Errorf("start tracer: %w", err) 63 | } 64 | defer t.Close() 65 | 66 | // When we get a SIGTERM we should close the tracer so the loop exits. 67 | signals := make(chan os.Signal, 1) 68 | signal.Notify(signals, os.Interrupt, syscall.SIGTERM) 69 | go func() { 70 | <-signals 71 | 72 | log.Print("signal received, closing tracer") 73 | err := t.Close() 74 | if err != nil { 75 | //nolint:revive 76 | log.Fatalf("error closing tracer: %+v", err) 77 | } 78 | }() 79 | 80 | enc := json.NewEncoder(os.Stdout) 81 | 82 | log.Println("Waiting for events..") 83 | for { 84 | event, err := t.Read() 85 | if err != nil { 86 | if errors.Is(err, io.EOF) { 87 | return nil 88 | } 89 | log.Printf("error reading from reader: %+v", err) 90 | continue 91 | } 92 | 93 | if outputFormat == "text" { 94 | ellipsis := "" 95 | if event.Truncated { 96 | ellipsis = "..." 97 | } 98 | 99 | _, _ = fmt.Printf( 100 | "[%v, comm=%q, uid=%v, gid=%v, filename=%v] %v%v\n", 101 | event.PID, event.Comm, event.UID, event.GID, event.Filename, 102 | shellquote.Join(event.Argv...), ellipsis, 103 | ) 104 | continue 105 | } 106 | err = enc.Encode(event) 107 | if err != nil { 108 | log.Printf("error writing event as JSON: %+v", err) 109 | continue 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tracer.go: -------------------------------------------------------------------------------- 1 | package exectrace 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | const pidNSPath = "/proc/self/ns/pid" 13 | 14 | var nonNumericRegex = regexp.MustCompile(`[^\d]`) 15 | 16 | // TracerOpts contains all of the configuration options for the tracer. All are 17 | // optional. 18 | type TracerOpts struct { 19 | // PidNS filters all processes that are in the given PID namespace or in the 20 | // child namespace tree of this given namespace. This is very useful for 21 | // Docker containers, as you can read all processes in a container (or in 22 | // child containers). 23 | // 24 | // You can read the PID namespace ID for a given process by running 25 | // `readlink /proc/x/ns/pid`. 26 | // 27 | // This filter runs in the kernel for high performance. 28 | PidNS uint32 29 | 30 | // LogFn is called for each log line that is read from the kernel. All logs 31 | // are considered error logs unless running a debug version of the eBPF 32 | // program. 33 | // 34 | // If unspecified, a default log function is used that logs to stderr. 35 | LogFn func(uid, gid, pid uint32, logLine string) 36 | } 37 | 38 | // Tracer allows consumers to read exec events from the kernel via an eBPF 39 | // program. `execve()` syscalls are traced in the kernel, and details about the 40 | // event are sent back to this Go interface. 41 | type Tracer interface { 42 | io.Closer 43 | 44 | // Read blocks until an exec event is available, then returns it. 45 | Read() (*Event, error) 46 | 47 | // FD returns the FD of the loaded eBPF program. This is useful for 48 | // benchmarking. 49 | FD() int 50 | } 51 | 52 | // Event contains data about each exec event with many fields for easy 53 | // filtering and logging. 54 | type Event struct { 55 | Filename string `json:"filename"` 56 | // Argv contains the raw argv supplied to the process, including argv[0] 57 | // (which is equal to `filepath.Base(e.Filename)` in most circumstances). 58 | Argv []string `json:"argv"` 59 | // Truncated is true if we were unable to read all process arguments into 60 | // Argv because there were more than 32 arguments, or if one of the 61 | // arguments was greater than or equal to 1023 bytes in length. 62 | // 63 | // It may indicate that the user or process is trying to hide arguments from 64 | // the tracer. 65 | Truncated bool `json:"truncated"` 66 | 67 | // These values are of the new process. Keep in mind that the exec call may 68 | // fail and the PID will be released in such a case. 69 | PID uint32 `json:"pid"` 70 | UID uint32 `json:"uid"` 71 | GID uint32 `json:"gid"` 72 | 73 | // Comm is the "name" of the parent process, usually the filename of the 74 | // executable (but not always). 75 | Comm string `json:"comm"` 76 | } 77 | 78 | // GetPidNS returns the inum of the PidNS used by the current process. 79 | func GetPidNS() (uint32, error) { 80 | rawPidNS, err := os.Readlink(pidNSPath) 81 | if err != nil { 82 | return 0, xerrors.Errorf("readlink %v: %w", pidNSPath, err) 83 | } 84 | 85 | rawPidNS = nonNumericRegex.ReplaceAllString(rawPidNS, "") 86 | pidNS, err := strconv.ParseUint(rawPidNS, 10, 32) 87 | if err != nil { 88 | return 0, xerrors.Errorf("parse PidNS %v to uint32: %w", rawPidNS, err) 89 | } 90 | 91 | return uint32(pidNS), nil 92 | } 93 | -------------------------------------------------------------------------------- /enterprise/cmd/exectrace/main.go: -------------------------------------------------------------------------------- 1 | // This file is licensed under the Coder Enterprise License. Please see 2 | // ../../../LICENSE.enterprise. 3 | package main 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/urfave/cli/v2" 11 | "golang.org/x/xerrors" 12 | 13 | "cdr.dev/slog" 14 | "cdr.dev/slog/sloggers/slogjson" 15 | exectracewrapper "github.com/coder/exectrace/enterprise" 16 | ) 17 | 18 | func main() { 19 | app := root() 20 | 21 | err := app.Run(os.Args) 22 | if err != nil { 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func root() *cli.App { 28 | return &cli.App{ 29 | Name: "exectrace", 30 | Usage: "Run exectrace tracing, printing a log line for each process " + 31 | "launched in the workspace.", 32 | Description: "Waits for the PidNS to be sent from the sibling " + 33 | "container to http://, then logs all exec calls in the " + 34 | "specified PidNS to stderr using the exectrace library.", 35 | Flags: []cli.Flag{ 36 | &cli.StringFlag{ 37 | Name: "init-address", 38 | Usage: "The HTTP listen address that the binary listens on " + 39 | "temporarily at startup for the PidNS. Once a PidNS is " + 40 | "received, this server is automatically closed.", 41 | // We use this port because it's uncommon. 42 | Value: "0.0.0.0:56123", 43 | }, 44 | &cli.DurationFlag{ 45 | Name: "startup-timeout", 46 | Usage: "The maximum duration that the process will wait for " + 47 | "PidNS to be received during startup before timing out.", 48 | // We use 15 minutes as the default as Kubernetes starts 49 | // containers out of order, so it might start this container 50 | // long before the workspace container is ready to start. 51 | Value: 15 * time.Minute, 52 | }, 53 | &cli.BoolFlag{ 54 | Name: "use-local-pidns", 55 | Usage: "Use the local PidNS instead of waiting for one over " + 56 | "HTTP. This should only be used if it's difficult to " + 57 | "send the PidNS over HTTP (e.g. due to network policies) " + 58 | "and requires the workspace and exectrace container to " + 59 | "share a PidNS.", 60 | }, 61 | &cli.StringSliceFlag{ 62 | Name: "label", 63 | Usage: "Add these labels to all logged events. Labels are in " + 64 | "the form of key=value.", 65 | }, 66 | }, 67 | Action: func(ctx *cli.Context) error { 68 | // TODO: more flags for controlling logging 69 | log := slog.Make(slogjson.Sink(os.Stderr)).Leveled(slog.LevelDebug) 70 | 71 | // Add labels from flags. 72 | rawLabels := ctx.StringSlice("label") 73 | labels := make(map[string]string, len(rawLabels)) 74 | for _, l := range rawLabels { 75 | vals := strings.SplitN(l, "=", 2) 76 | if len(vals) != 2 { 77 | return xerrors.Errorf("invalid label %q", l) 78 | } 79 | 80 | labels[strings.TrimSpace(vals[0])] = strings.TrimSpace(vals[1]) 81 | } 82 | log = log.With(slog.F("labels", labels)) 83 | 84 | log.Debug(ctx.Context, "starting exectrace") 85 | err := exectracewrapper.Run(ctx.Context, log, exectracewrapper.Options{ 86 | UseLocalPidNS: ctx.Bool("use-local-pidns"), 87 | InitListenAddress: ctx.String("init-address"), 88 | StartupTimeout: ctx.Duration("startup-timeout"), 89 | }) 90 | if err != nil { 91 | return xerrors.Errorf("run exectrace: %w", err) 92 | } 93 | 94 | return nil 95 | }, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /bpf.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package exectrace 5 | 6 | import ( 7 | "bytes" 8 | "runtime" 9 | "sync" 10 | 11 | "github.com/cilium/ebpf" 12 | "github.com/cilium/ebpf/rlimit" 13 | "github.com/hashicorp/go-multierror" 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | var ( 18 | errObjectsClosed = xerrors.New("objects are closed") 19 | removeMemlockOnce sync.Once 20 | 21 | // collectionOpts used for loading the BPF objects. 22 | collectionOpts = &ebpf.CollectionOptions{ 23 | Programs: ebpf.ProgramOptions{ 24 | // While debugging, it may be helpful to set this value to be much 25 | // higher (i.e. * 1000). 26 | LogSize: ebpf.DefaultVerifierLogSize, 27 | }, 28 | } 29 | ) 30 | 31 | // loadBPFObjects reads and parses the programs and maps out of the embedded 32 | // BPF program. 33 | func loadBPFObjects() (*bpfObjects, error) { 34 | // Allow the current process to lock memory for eBPF resources. This does 35 | // nothing on 5.11+ kernels which don't need this. 36 | var err error 37 | removeMemlockOnce.Do(func() { 38 | err = rlimit.RemoveMemlock() 39 | }) 40 | if err != nil { 41 | return nil, xerrors.Errorf("remove kernel memlock: %w", err) 42 | } 43 | 44 | r := bytes.NewReader(bpfProgram) 45 | spec, err := ebpf.LoadCollectionSpecFromReader(r) 46 | if err != nil { 47 | return nil, xerrors.Errorf("load collection from reader: %w", err) 48 | } 49 | 50 | objs := &bpfObjects{ 51 | closeLock: sync.Mutex{}, 52 | closed: make(chan struct{}), 53 | } 54 | err = spec.LoadAndAssign(objs, collectionOpts) 55 | if err != nil { 56 | var ve *ebpf.VerifierError 57 | if xerrors.As(err, &ve) { 58 | // It's better to use %+v for this as it forces the error to contain 59 | // all lines from the verifier log. 60 | return nil, xerrors.Errorf("load and assign specs: verifier error: %+v", ve) 61 | } 62 | return nil, xerrors.Errorf("load and assign specs: %w", err) 63 | } 64 | 65 | return objs, nil 66 | } 67 | 68 | type bpfObjects struct { 69 | EnterExecveProg *ebpf.Program `ebpf:"enter_execve"` 70 | EventsMap *ebpf.Map `ebpf:"events"` 71 | LogsMap *ebpf.Map `ebpf:"logs"` 72 | FiltersMap *ebpf.Map `ebpf:"filters"` 73 | 74 | closeLock sync.Mutex 75 | closed chan struct{} 76 | } 77 | 78 | func (o *bpfObjects) Close() error { 79 | o.closeLock.Lock() 80 | defer o.closeLock.Unlock() 81 | select { 82 | case <-o.closed: 83 | return errObjectsClosed 84 | default: 85 | } 86 | close(o.closed) 87 | runtime.SetFinalizer(o, nil) 88 | 89 | var merr error 90 | if o.EnterExecveProg != nil { 91 | err := o.EnterExecveProg.Close() 92 | if err != nil { 93 | merr = multierror.Append(merr, xerrors.Errorf(`close BPF program "enter_execve": %w`, err)) 94 | } 95 | } 96 | if o.EventsMap != nil { 97 | err := o.EventsMap.Close() 98 | if err != nil { 99 | merr = multierror.Append(merr, xerrors.Errorf(`close BPF map "events": %w`, err)) 100 | } 101 | } 102 | if o.LogsMap != nil { 103 | err := o.LogsMap.Close() 104 | if err != nil { 105 | merr = multierror.Append(merr, xerrors.Errorf(`close BPF map "logs": %w`, err)) 106 | } 107 | } 108 | if o.FiltersMap != nil { 109 | err := o.FiltersMap.Close() 110 | if err != nil { 111 | merr = multierror.Append(merr, xerrors.Errorf(`close BPF map "filters": %w`, err)) 112 | } 113 | } 114 | 115 | return merr 116 | } 117 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.14.0 h1:0PsxAjO6EjI1rcT+rkp6WcCnE0ZvfkXBYiMedJtrSUs= 2 | github.com/cilium/ebpf v0.14.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 7 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 8 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 9 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 11 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 12 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 13 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 14 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 15 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 16 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 17 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 18 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 26 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 27 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 28 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 29 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 30 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 31 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 32 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 33 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 34 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= 35 | golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 36 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 37 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 39 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /Makefile.enterprise: -------------------------------------------------------------------------------- 1 | # Create the output directory if it does not exist. 2 | ENTERPRISE_BUILD := enterprise/build 3 | $(shell mkdir -p "$(ENTERPRISE_BUILD)") 4 | 5 | VERSION := $(shell ./ci/scripts/version.sh) 6 | 7 | ARCHES := amd64 arm64 armv7 8 | 9 | BINARIES := $(addprefix $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_,$(ARCHES)) 10 | IMAGES := $(foreach arch, $(ARCHES), $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_$(arch).tag) 11 | IMAGES_PUSHED := $(foreach image, $(IMAGES), $(image).pushed) 12 | 13 | BINARIES_VERSIONLESS := $(addprefix $(ENTERPRISE_BUILD)/exectrace_linux_,$(ARCHES)) 14 | IMAGES_VERSIONLESS := $(foreach arch, $(ARCHES), $(ENTERPRISE_BUILD)/exectrace_linux_$(arch).tag) 15 | 16 | MAIN_IMAGE := $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux.tag 17 | MAIN_IMAGE_PUSHED := $(MAIN_IMAGE).pushed 18 | 19 | MAIN_IMAGE_LATEST := $(ENTERPRISE_BUILD)/exectrace_latest_linux.tag 20 | MAIN_IMAGE_LATEST_PUSHED := $(MAIN_IMAGE_LATEST).pushed 21 | 22 | .PHONY: clean-enterprise 23 | clean-enterprise: 24 | rm -rf "$(ENTERPRISE_BUILD)" 25 | mkdir -p "$(ENTERPRISE_BUILD)" 26 | 27 | .PHONY: build 28 | build: $(BINARIES) 29 | 30 | $(BINARIES): $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_%: \ 31 | $(shell find . -type f -name '*.go') \ 32 | bpf/handler-bpfeb.o \ 33 | bpf/handler-bpfel.o 34 | 35 | goarch="$*" 36 | goarm="" 37 | if [[ "$${goarch}" == "arm" ]]; then 38 | goarm="7" 39 | elif [[ "$${goarch}" == "armv"* ]] || [[ "$${goarch}" == "arm64v"* ]]; then 40 | goarm="$${goarch//*v/}" 41 | # Remove the v* suffix. 42 | goarch="$${goarch//v*/}" 43 | fi 44 | 45 | # TODO: version details 46 | GOOS=linux GOARCH="$${goarch}" GOARM="$${goarm}" go build \ 47 | -o "$@" \ 48 | ./enterprise/cmd/exectrace 49 | 50 | $(BINARIES_VERSIONLESS): $(ENTERPRISE_BUILD)/exectrace_linux_%: $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_% 51 | rm -f "$@" 52 | ln "$<" "$@" 53 | 54 | $(IMAGES): $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_%.tag: $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_% enterprise/Dockerfile 55 | arch="$*" 56 | image_tag="$$(./enterprise/scripts/image_tag.sh --arch "$${arch}" --version "$(VERSION)")" 57 | 58 | # Remap the arch from Golang to Docker. 59 | declare -A arch_map=( 60 | [amd64]="linux/amd64" 61 | [arm64]="linux/arm64" 62 | [arm]="linux/arm/v7" 63 | [armv7]="linux/arm/v7" 64 | ) 65 | if [[ "$${arch_map[$${arch}]+exists}" != "" ]]; then 66 | arch="$${arch_map[$${arch}]}" 67 | else 68 | echo "Unknown arch: $${arch}" 69 | exit 1 70 | fi 71 | 72 | temp_dir="$$(mktemp -d)" 73 | cp enterprise/Dockerfile "$${temp_dir}/Dockerfile" 74 | cp "$<" "$${temp_dir}/exectrace" 75 | 76 | docker build \ 77 | --platform "$${arch}" \ 78 | --tag "$${image_tag}" \ 79 | --no-cache \ 80 | --build-arg "CODER_VERSION=$(VERSION)" \ 81 | "$${temp_dir}" 82 | 83 | rm -rf "$${temp_dir}" 84 | echo "$${image_tag}" > "$@" 85 | 86 | $(IMAGES_VERSIONLESS): $(ENTERPRISE_BUILD)/exectrace_linux_%.tag: $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_%.tag 87 | rm -f "$@" 88 | ln "$<" "$@" 89 | 90 | $(IMAGES_PUSHED): $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_%.tag.pushed: $(ENTERPRISE_BUILD)/exectrace_$(VERSION)_linux_%.tag 91 | docker push "$$(cat "$<")" 92 | cat "$<" > "$@" 93 | 94 | # Creating a docker manifest requires the images to be pushed, so this job 95 | # depends on IMAGES_PUSHED instead of IMAGES. 96 | $(MAIN_IMAGE) $(MAIN_IMAGE_LATEST): $(IMAGES_PUSHED) 97 | version="$(VERSION)" 98 | if [[ "$@" == "$(MAIN_IMAGE_LATEST)" ]]; then 99 | version="latest" 100 | fi 101 | image_tag="$$(./enterprise/scripts/image_tag.sh --version "$${version}")" 102 | docker manifest create \ 103 | "$${image_tag}" \ 104 | $(foreach image, $(IMAGES), "$$(cat "$(image)")") 105 | 106 | echo "$${image_tag}" > "$@" 107 | 108 | $(MAIN_IMAGE_PUSHED): $(MAIN_IMAGE) 109 | docker manifest push "$$(cat "$<")" 110 | cat "$<" > "$@" 111 | 112 | $(MAIN_IMAGE_LATEST_PUSHED): $(MAIN_IMAGE_LATEST) 113 | docker manifest push "$$(cat "$<")" 114 | cat "$<" > "$@" 115 | -------------------------------------------------------------------------------- /bench/tracer_linux_test.go: -------------------------------------------------------------------------------- 1 | package exectrace 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/DataDog/ebpfbench" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/coder/exectrace" 12 | ) 13 | 14 | func BenchmarkExectraceBase(b *testing.B) { 15 | // This test must be run as root so we can start exectrace. 16 | if os.Geteuid() != 0 { 17 | b.Fatal("must be run as root") 18 | } 19 | 20 | // Start tracer. 21 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 22 | LogFn: func(uid, gid, pid uint32, logLine string) { 23 | b.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 24 | }, 25 | }) 26 | require.NoError(b, err) 27 | defer tracer.Close() 28 | 29 | eb := ebpfbench.NewEBPFBenchmark(b) 30 | defer eb.Close() 31 | 32 | eb.ProfileProgram(tracer.FD(), "enter_execve") 33 | eb.Run(func(b *testing.B) { 34 | // NOTE: the actual iteration count in the final logs might be higher 35 | // than b.N because the program runs on every execve syscall (even ones 36 | // that don't originate from the benchmarked code). 37 | // 38 | // ebpfbench will still report the correct number of iterations and the 39 | // average time per iteration. 40 | for i := 0; i < b.N; i++ { 41 | cmd := exec.Command("true") 42 | err := cmd.Run() 43 | require.NoError(b, err) 44 | 45 | _, err = tracer.Read() 46 | require.NoError(b, err) 47 | // Can't verify the process here because we are not filtering and 48 | // other processes on the system will cause any checks to fail. 49 | } 50 | }) 51 | } 52 | 53 | // NOTE: you should probably run this benchmark with ./bench.sh or `make bench`. 54 | func BenchmarkExectracePIDNSFilter(b *testing.B) { 55 | // This test must be run as root so we can start exectrace. 56 | if os.Geteuid() != 0 { 57 | b.Fatal("must be run as root") 58 | } 59 | 60 | pidNS, err := exectrace.GetPidNS() 61 | require.NoError(b, err) 62 | 63 | // Start tracer. 64 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 65 | PidNS: pidNS, 66 | LogFn: func(uid, gid, pid uint32, logLine string) { 67 | b.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 68 | }, 69 | }) 70 | require.NoError(b, err) 71 | defer tracer.Close() 72 | 73 | eb := ebpfbench.NewEBPFBenchmark(b) 74 | defer eb.Close() 75 | 76 | eb.ProfileProgram(tracer.FD(), "enter_execve") 77 | eb.Run(func(b *testing.B) { 78 | // NOTE: iteration count can end up higher than b.N, see above. 79 | // 80 | // Because filtered events take less time to process in the kernel, this 81 | // does impact the benchmark results. The average time per iteration 82 | // reported will be lower than it should be. If you run with a high 83 | // iteration count the effect should be mostly invisible, however. 84 | for i := 0; i < b.N; i++ { 85 | cmd := exec.Command("true") 86 | err := cmd.Run() 87 | require.NoError(b, err) 88 | 89 | event, err := tracer.Read() 90 | require.NoError(b, err) 91 | require.Equal(b, "true", event.Argv[0]) 92 | } 93 | }) 94 | } 95 | 96 | func BenchmarkExectracePIDNSFilterNoHit(b *testing.B) { 97 | // This test must be run as root so we can start exectrace. 98 | if os.Geteuid() != 0 { 99 | b.Fatal("must be run as root") 100 | } 101 | 102 | pidNS, err := exectrace.GetPidNS() 103 | require.NoError(b, err) 104 | 105 | // Start tracer. 106 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 107 | // Use a nonsense PID NS so we don't match any processes. 108 | PidNS: pidNS + 1, 109 | LogFn: func(uid, gid, pid uint32, logLine string) { 110 | b.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 111 | }, 112 | }) 113 | require.NoError(b, err) 114 | defer tracer.Close() 115 | 116 | eb := ebpfbench.NewEBPFBenchmark(b) 117 | defer eb.Close() 118 | 119 | eb.ProfileProgram(tracer.FD(), "enter_execve") 120 | eb.Run(func(b *testing.B) { 121 | // NOTE: iteration count can end up higher than b.N, see above. 122 | // 123 | // Since every event is a no hit because the filter doesn't match 124 | // anything, results should not be impacted. 125 | for i := 0; i < b.N; i++ { 126 | cmd := exec.Command("true") 127 | err := cmd.Run() 128 | require.NoError(b, err) 129 | } 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /enterprise/templates/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Develop in Kubernetes 3 | description: Get started with Kubernetes development. 4 | tags: [cloud, kubernetes] 5 | icon: /icon/k8s.png 6 | --- 7 | 8 | # Getting started 9 | 10 | This template creates a pod running the `codercom/enterprise-base:ubuntu` image, 11 | with an [exectrace](https://github.com/coder/exectrace/tree/main/enterprise) 12 | sidecar container to log all processes started in the workspace. 13 | 14 | ## RBAC 15 | 16 | The Coder provisioner requires permission to administer pods to use this 17 | template. The template creates workspaces in a single Kubernetes namespace, 18 | using the `workspaces_namespace` parameter set while creating the template. 19 | 20 | Create a role as follows and bind it to the user or service account that runs 21 | the coder host. 22 | 23 | ```yaml 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | kind: Role 26 | metadata: 27 | name: coder 28 | rules: 29 | - apiGroups: [""] 30 | resources: ["pods"] 31 | verbs: ["*"] 32 | ``` 33 | 34 | ## Authentication 35 | 36 | This template can authenticate using in-cluster authentication, or using a 37 | kubeconfig local to the Coder host. For additional authentication options, 38 | consult the 39 | [Kubernetes provider documentation](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs). 40 | 41 | ### kubeconfig on Coder host 42 | 43 | If the Coder host has a local `~/.kube/config`, you can use this to authenticate 44 | with Coder. Make sure this is done with same user that's running the `coder` 45 | service. 46 | 47 | To use this authentication, set the parameter `use_kubeconfig` to true. 48 | 49 | ### In-cluster authentication 50 | 51 | If the Coder host runs in a Pod on the same Kubernetes cluster as you are 52 | creating workspaces in, you can use in-cluster authentication. 53 | 54 | To use this authentication, set the parameter `use_kubeconfig` to false. 55 | 56 | The Terraform provisioner will automatically use the service account associated 57 | with the pod to authenticate to Kubernetes. Be sure to bind a 58 | [role with appropriate permission](#rbac) to the service account. For example, 59 | assuming the Coder host runs in the same namespace as you intend to create 60 | workspaces: 61 | 62 | ```yaml 63 | apiVersion: v1 64 | kind: ServiceAccount 65 | metadata: 66 | name: coder 67 | 68 | --- 69 | apiVersion: rbac.authorization.k8s.io/v1 70 | kind: RoleBinding 71 | metadata: 72 | name: coder 73 | subjects: 74 | - kind: ServiceAccount 75 | name: coder 76 | roleRef: 77 | kind: Role 78 | name: coder 79 | apiGroup: rbac.authorization.k8s.io 80 | ``` 81 | 82 | Then start the Coder host with `serviceAccountName: coder` in the pod spec. 83 | 84 | ## Namespace 85 | 86 | The target namespace in which the pod will be deployed is defined via the 87 | `coder_workspace` variable. The namespace must exist prior to creating 88 | workspaces. 89 | 90 | ## Persistence 91 | 92 | The `/home/coder` directory in this example is persisted via the attached 93 | PersistentVolumeClaim. Any data saved outside of this directory will be wiped 94 | when the workspace stops. 95 | 96 | Since most binary installations and environment configurations live outside of 97 | the `/home` directory, we suggest including these in the `startup_script` 98 | argument of the `coder_agent` resource block, which will run each time the 99 | workspace starts up. 100 | 101 | For example, when installing the `aws` CLI, the install script will place the 102 | `aws` binary in `/usr/local/bin/aws`. To ensure the `aws` CLI is persisted 103 | across workspace starts/stops, include the following code in the `coder_agent` 104 | resource block of your workspace template: 105 | 106 | ```terraform 107 | resource "coder_agent" "main" { 108 | startup_script = < Use `go run -exec sudo ./cmd/program` to compile a program and start it with 50 | > `sudo` 51 | 52 | ```console 53 | $ go install -u github.com/coder/exectrace/cmd/exectrace 54 | $ exectrace --help 55 | ... 56 | 57 | $ sudo exectrace 58 | 2021/12/01 16:42:02 Waiting for events.. 59 | [1188921, comm="node", uid=1002, gid=1003, filename=/bin/sh] /bin/sh -c 'which ps' 60 | [1188922, comm="sh", uid=1002, gid=1003, filename=/usr/bin/which] which ps 61 | ``` 62 | 63 | ## Usage 64 | 65 | exectrace exposes a minimal API surface. Call `exectrace.New(nil)` and then you 66 | can start reading events from the returned `Tracer`. 67 | 68 | It is important that you close the tracer to avoid leaking kernel resources, so 69 | we recommend implementing a simple signal handler like the one in this example: 70 | 71 | ```go 72 | package main 73 | 74 | import ( 75 | "fmt" 76 | "os" 77 | "os/signal" 78 | "syscall" 79 | 80 | "github.com/coder/exectrace" 81 | ) 82 | 83 | func main() { 84 | tracer, err := exectrace.New(nil) 85 | if err != nil { 86 | panic(err) 87 | } 88 | defer tracer.Close() 89 | 90 | go func() { 91 | sigs := make(chan os.Signal, 1) 92 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 93 | <-sigs 94 | tracer.Close() 95 | }() 96 | 97 | for { 98 | event, err := tracer.Read() 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | fmt.Printf("%+v\n", event) 104 | } 105 | } 106 | ``` 107 | 108 | > For a full usage example, refer to this 109 | > [comprehensive program](./cmd/exectrace/main.go) that uses the library. 110 | 111 | ## Development 112 | 113 | You will need the following: 114 | 115 | - Docker (the Makefile runs clang within a Docker container for reproducibility) 116 | - Golang 1.20+ 117 | - `golangci-lint` 118 | - `prettier` 119 | - `shellcheck` 120 | 121 | Since the eBPF program is packaged using `go:embed`, you will need to compile 122 | the program and include it in the repo. 123 | 124 | If you change the files in the `bpf` directory, run `make` and ensure that you 125 | include the `.o` files you changed in your commit (CI will verify that you've 126 | done this correctly). 127 | 128 | ## Status: stable 129 | 130 | This library is ready to use as-is. It has been used in production for years and 131 | has received minimal maintenance over that time period. 132 | 133 | In April 2024, a system to send logs from the kernel to userspace was added 134 | which can make discovering potential issues in production/development much 135 | easier. 136 | 137 | The API will likely not be further modified as we have no need for additional 138 | fields/features. We will continue to maintain the library as needed. 139 | 140 | ## See also 141 | 142 | - [`canonical/etrace`](https://github.com/canonical/etrace) - Go binary that 143 | uses ptrace and tracks the processes that a command launches for debugging and 144 | analysis 145 | - [`shirou/gopsutil`](https://github.com/shirou/gopsutil) - Go library that has 146 | methods for listing process details and getting information about the system 147 | 148 | --- 149 | 150 | Dual licensed under the MIT and GPL 2.0 licenses. See [LICENSE](LICENSE). 151 | 152 | Code in the enterprise directory has a different license. See 153 | [LICENSE.enterprise](LICENSE.enterprise). 154 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # This is copied from github.com/coder/coder. 2 | # 3 | # Changes: 4 | # - removed ruleguard 5 | # - removed testpackage 6 | 7 | linters-settings: 8 | dupl: 9 | # goal: 100 10 | threshold: 412 11 | 12 | exhaustruct: 13 | include: 14 | # Gradually extend to cover more of the codebase. 15 | - 'httpmw\.\w+' 16 | # We want to enforce all values are specified when inserting or updating 17 | # a database row. Ref: #993 18 | - 'github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params' 19 | gocognit: 20 | min-complexity: 300 21 | 22 | goconst: 23 | min-len: 4 # Min length of string consts (def 3). 24 | min-occurrences: 3 # Min number of const occurrences (def 3). 25 | 26 | gocritic: 27 | enabled-checks: 28 | # - appendAssign 29 | # - appendCombine 30 | # - assignOp 31 | # - badCall 32 | - badLock 33 | - badRegexp 34 | - boolExprSimplify 35 | # - builtinShadow 36 | - builtinShadowDecl 37 | # - commentedOutCode 38 | - commentedOutImport 39 | - deferUnlambda 40 | # - deprecatedComment 41 | # - docStub 42 | - dupImport 43 | # - elseif 44 | - emptyFallthrough 45 | # - emptyStringTest 46 | # - equalFold 47 | # - evalOrder 48 | # - exitAfterDefer 49 | # - exposedSyncMutex 50 | # - filepathJoin 51 | - hexLiteral 52 | # - httpNoBody 53 | # - hugeParam 54 | # - ifElseChain 55 | # - importShadow 56 | - indexAlloc 57 | - initClause 58 | - methodExprCall 59 | # - nestingReduce 60 | - nilValReturn 61 | # - octalLiteral 62 | # - paramTypeCombine 63 | # - preferStringWriter 64 | # - preferWriteByte 65 | # - ptrToRefParam 66 | # - rangeExprCopy 67 | # - rangeValCopy 68 | - regexpPattern 69 | # - regexpSimplify 70 | # - ruleguard 71 | # - sloppyReassign 72 | - sortSlice 73 | - sprintfQuotedString 74 | - sqlQuery 75 | # - stringConcatSimplify 76 | # - stringXbytes 77 | # - suspiciousSorting 78 | - truncateCmp 79 | - typeAssertChain 80 | # - typeDefFirst 81 | # - typeUnparen 82 | # - unlabelStmt 83 | # - unlambda 84 | # - unnamedResult 85 | # - unnecessaryBlock 86 | # - unnecessaryDefer 87 | # - unslice 88 | - weakCond 89 | # - whyNoLint 90 | # - wrapperFunc 91 | # - yodaStyleExpr 92 | 93 | staticcheck: 94 | # https://staticcheck.io/docs/options#checks 95 | # We disable SA1019 because it gets angry about our usage of xerrors. We 96 | # intentionally xerrors because stack frame support didn't make it into the 97 | # stdlib port. 98 | checks: ["all", "-SA1019"] 99 | 100 | goimports: 101 | local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder 102 | 103 | importas: 104 | no-unaliased: true 105 | 106 | misspell: 107 | locale: US 108 | ignore-words: 109 | - trialer 110 | 111 | nestif: 112 | # goal: 10 113 | min-complexity: 20 114 | 115 | revive: 116 | # see https://github.com/mgechev/revive#available-rules for details. 117 | ignore-generated-header: true 118 | severity: warning 119 | rules: 120 | - name: atomic 121 | - name: bare-return 122 | - name: blank-imports 123 | - name: bool-literal-in-expr 124 | - name: call-to-gc 125 | - name: confusing-naming 126 | - name: confusing-results 127 | - name: constant-logical-expr 128 | - name: context-as-argument 129 | - name: context-keys-type 130 | - name: deep-exit 131 | - name: defer 132 | - name: dot-imports 133 | - name: duplicated-imports 134 | - name: early-return 135 | - name: empty-block 136 | - name: empty-lines 137 | - name: error-naming 138 | - name: error-return 139 | - name: error-strings 140 | - name: errorf 141 | - name: exported 142 | - name: flag-parameter 143 | - name: get-return 144 | - name: identical-branches 145 | - name: if-return 146 | - name: import-shadowing 147 | - name: increment-decrement 148 | - name: indent-error-flow 149 | # - name: modifies-parameter 150 | - name: modifies-value-receiver 151 | - name: package-comments 152 | - name: range 153 | - name: receiver-naming 154 | - name: redefines-builtin-id 155 | - name: string-of-int 156 | - name: struct-tag 157 | - name: superfluous-else 158 | - name: time-naming 159 | - name: unconditional-recursion 160 | - name: unexported-naming 161 | - name: unexported-return 162 | - name: unhandled-error 163 | - name: unnecessary-stmt 164 | - name: unreachable-code 165 | - name: unused-parameter 166 | exclude: "**/*_test.go" 167 | - name: unused-receiver 168 | - name: var-declaration 169 | - name: var-naming 170 | - name: waitgroup-by-value 171 | 172 | # irrelevant as of Go v1.22: https://go.dev/blog/loopvar-preview 173 | govet: 174 | disable: 175 | - loopclosure 176 | gosec: 177 | excludes: 178 | # Implicit memory aliasing of items from a range statement (irrelevant as of Go v1.22) 179 | - G601 180 | 181 | issues: 182 | # Rules listed here: https://github.com/securego/gosec#available-rules 183 | exclude-rules: 184 | - path: _test\.go 185 | linters: 186 | # We use assertions rather than explicitly checking errors in tests 187 | - errcheck 188 | - forcetypeassert 189 | - exhaustruct # This is unhelpful in tests. 190 | 191 | fix: true 192 | max-issues-per-linter: 0 193 | max-same-issues: 0 194 | 195 | run: 196 | timeout: 10m 197 | 198 | # Over time, add more and more linters from 199 | # https://golangci-lint.run/usage/linters/ as the code improves. 200 | linters: 201 | disable-all: true 202 | enable: 203 | - asciicheck 204 | - bidichk 205 | - bodyclose 206 | - dogsled 207 | - errcheck 208 | - errname 209 | - errorlint 210 | - exhaustruct 211 | - forcetypeassert 212 | - gocritic 213 | # gocyclo is may be useful in the future when we start caring 214 | # about testing complexity, but for the time being we should 215 | # create a good culture around cognitive complexity. 216 | # - gocyclo 217 | - gocognit 218 | - nestif 219 | - goimports 220 | - gomodguard 221 | - gosec 222 | - gosimple 223 | - govet 224 | - importas 225 | - ineffassign 226 | - makezero 227 | - misspell 228 | - nilnil 229 | - noctx 230 | - paralleltest 231 | - revive 232 | 233 | # These don't work until the following issue is solved. 234 | # https://github.com/golangci/golangci-lint/issues/2649 235 | # - rowserrcheck 236 | # - sqlclosecheck 237 | # - structcheck 238 | # - wastedassign 239 | 240 | - staticcheck 241 | - tenv 242 | # - testpackage 243 | - tparallel 244 | - typecheck 245 | - unconvert 246 | - unused 247 | - dupl 248 | -------------------------------------------------------------------------------- /enterprise/exectrace.go: -------------------------------------------------------------------------------- 1 | // This file is licensed under the Coder Enterprise License. Please see 2 | // ../LICENSE.enterprise. 3 | package exectracewrapper 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/kballard/go-shellquote" 18 | "golang.org/x/xerrors" 19 | "k8s.io/utils/mount" 20 | 21 | "cdr.dev/slog" 22 | "github.com/coder/exectrace" 23 | ) 24 | 25 | const ( 26 | debugFS = "debugfs" 27 | debugFSMountpoint = "/sys/kernel/debug" 28 | traceFS = "tracefs" 29 | traceFSMountpoint = debugFSMountpoint + "/tracing" 30 | ) 31 | 32 | type Options struct { 33 | UseLocalPidNS bool 34 | InitListenAddress string 35 | StartupTimeout time.Duration 36 | } 37 | 38 | func Run(ctx context.Context, log slog.Logger, opts Options) error { 39 | var ( 40 | err error 41 | pidNS uint32 42 | ) 43 | if opts.UseLocalPidNS { 44 | log.Debug(ctx, "using local PidNS") 45 | pidNS, err = exectrace.GetPidNS() 46 | if err != nil { 47 | return xerrors.Errorf("get current pidns: %w", err) 48 | } 49 | } else { 50 | waitCtx, waitCancel := context.WithTimeout(ctx, opts.StartupTimeout) 51 | defer waitCancel() 52 | 53 | // Wait for the PidNS to be sent to us from the workspace container. 54 | log.Debug(ctx, "waiting for PidNS", slog.F("addr", opts.InitListenAddress), slog.F("timeout", opts.StartupTimeout)) 55 | pidNS, err = waitForExectracePidNS(waitCtx, opts.InitListenAddress) 56 | if err != nil { 57 | return xerrors.Errorf("wait for PidNS at %q: %w", opts.InitListenAddress, err) 58 | } 59 | } 60 | log.Debug(ctx, "got PidNS", slog.F("pid_ns", pidNS)) 61 | 62 | // We need to make sure that debugfs is mounted at /sys/kernel/debug and 63 | // tracefs is mounted at /sys/kernel/debug/tracing. 64 | log.Debug(ctx, "preparing environment for eBPF tracing") 65 | err = ensureVirtualMountpoint(debugFS, debugFSMountpoint, nil) 66 | if err != nil { 67 | return xerrors.Errorf("ensure debugfs mounted: %w", err) 68 | } 69 | err = ensureVirtualMountpoint(traceFS, traceFSMountpoint, nil) 70 | if err != nil { 71 | return xerrors.Errorf("ensure tracefs mounted: %w", err) 72 | } 73 | 74 | log.Debug(ctx, "starting tracer") 75 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 76 | PidNS: pidNS, 77 | LogFn: func(uid, gid, pid uint32, logLine string) { 78 | log.Error(ctx, "tracer error log: "+logLine, slog.F("uid", uid), slog.F("gid", gid), slog.F("pid", pid)) 79 | }, 80 | }) 81 | if err != nil { 82 | return xerrors.Errorf("create tracer: %w", err) 83 | } 84 | defer func() { 85 | err := tracer.Close() 86 | if err != nil && !xerrors.Is(err, io.EOF) { 87 | log.Error(ctx, "failed to close tracer on exit", slog.Error(err)) 88 | } 89 | }() 90 | 91 | events := make(chan *exectrace.Event, 1) 92 | errCh := make(chan error, 1) 93 | go func() { 94 | const attempts = 10 95 | for { 96 | // Fatal after 10 consecutive read errors. 97 | for i := 1; ; i++ { 98 | event, err := tracer.Read() 99 | if err != nil { 100 | log.Warn(ctx, "failed to read event from tracer", slog.Error(err)) 101 | if i == attempts { 102 | log.Error(ctx, "failed to read event after many attempts", slog.F("attempts", attempts)) 103 | errCh <- err 104 | return 105 | } 106 | continue 107 | } 108 | 109 | events <- event 110 | break 111 | } 112 | } 113 | }() 114 | 115 | // Setup a signal handler so we can gracefully exit. 116 | signals := make(chan os.Signal, 1) 117 | signal.Notify(signals, os.Interrupt) 118 | signal.Notify(signals, syscall.SIGTERM) 119 | 120 | for { 121 | select { 122 | case <-ctx.Done(): 123 | return ctx.Err() 124 | case <-signals: 125 | log.Warn(ctx, "received signal, exiting") 126 | return nil 127 | case err := <-errCh: 128 | log.Error(ctx, "closing tracer due to error", slog.Error(err)) 129 | return err 130 | case event := <-events: 131 | log.Info(ctx, "exec", 132 | // Construct a simple string field so people don't need to write 133 | // queries against the argv array. 134 | slog.F("cmdline", shellquote.Join(event.Argv...)), 135 | slog.F("event", event), 136 | ) 137 | } 138 | } 139 | } 140 | 141 | // waitForExectracePidNS starts a HTTP server on listenAddr and waits until it 142 | // gets POSTed a uint32, then closes the server and returns it. 143 | func waitForExectracePidNS(ctx context.Context, listenAddr string) (uint32, error) { 144 | l, err := net.Listen("tcp", listenAddr) 145 | if err != nil { 146 | return 0, xerrors.Errorf("listen %q on tcp: %w", listenAddr, err) 147 | } 148 | defer l.Close() 149 | 150 | return waitForExectracePidNSListener(ctx, l) 151 | } 152 | 153 | func waitForExectracePidNSListener(ctx context.Context, l net.Listener) (uint32, error) { 154 | ctx, cancel := context.WithCancel(ctx) 155 | defer cancel() 156 | 157 | var ( 158 | pidNS uint32 159 | valid bool 160 | srv = &http.Server{ 161 | ReadHeaderTimeout: 5 * time.Second, 162 | ReadTimeout: 5 * time.Second, 163 | WriteTimeout: 5 * time.Second, 164 | IdleTimeout: 5 * time.Second, 165 | Handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 166 | writeError := func(rw http.ResponseWriter, status int, msg string) { 167 | rw.Header().Set("Content-Type", "text/plain") 168 | rw.WriteHeader(status) 169 | _, _ = rw.Write([]byte(msg)) 170 | } 171 | if r.Method != "POST" || (r.URL.Path != "" && r.URL.Path != "/") { 172 | writeError(rw, http.StatusBadRequest, "This server only accepts POST requests at /") 173 | return 174 | } 175 | 176 | ct := r.Header.Get("Content-Type") 177 | if ct != "" && ct != "text/plain" { 178 | writeError(rw, http.StatusBadRequest, "This server only accepts text/plain requests") 179 | return 180 | } 181 | 182 | // Read 16 bytes from the body, max uint32 is 10 chars so this 183 | // is plenty. 184 | body, err := io.ReadAll(io.LimitReader(r.Body, 16)) 185 | if err != nil { 186 | writeError(rw, http.StatusInternalServerError, "Failed to read request body: "+err.Error()) 187 | return 188 | } 189 | 190 | // Parse the body as a uint32. 191 | val, err := strconv.ParseUint(strings.TrimSpace(string(body)), 10, 32) 192 | if err != nil { 193 | writeError(rw, http.StatusBadRequest, "Failed to parse request body as uint32 string: "+err.Error()) 194 | return 195 | } 196 | 197 | pidNS = uint32(val) 198 | valid = true 199 | rw.WriteHeader(http.StatusNoContent) 200 | cancel() 201 | }), 202 | } 203 | ) 204 | defer srv.Close() 205 | 206 | go func() { 207 | <-ctx.Done() 208 | _ = srv.Close() 209 | }() 210 | 211 | err := srv.Serve(l) 212 | if !xerrors.Is(err, http.ErrServerClosed) { 213 | return 0, xerrors.Errorf("start server: %w", err) 214 | } 215 | if !valid { 216 | return 0, xerrors.Errorf("did not receive a PidNS from the workspace in time: %w", ctx.Err()) 217 | } 218 | 219 | return pidNS, nil 220 | } 221 | 222 | func ensureVirtualMountpoint(mountType, dest string, opts []string) error { 223 | mounter := &mount.Mounter{} 224 | if len(opts) == 0 { 225 | // The default on the server I tested this on. 226 | opts = []string{"rw", "nosuid", "nodev", "noexec", "relatime"} 227 | } 228 | 229 | // Find an existing mount. 230 | mounts, err := mounter.List() 231 | if err != nil { 232 | return xerrors.Errorf("list mounts: %w", err) 233 | } 234 | for _, m := range mounts { 235 | // NOTE: We don't check the device (i.e. source) because it doesn't 236 | // matter for virtual filesystems. Sometimes it's mounted as "none", 237 | // sometimes it's the same as the mount type. 238 | 239 | if m.Path == dest { 240 | if m.Type != mountType { 241 | return xerrors.Errorf("mount already exists at %q with incorrect type %q", m.Path, m.Type) 242 | } 243 | 244 | return nil 245 | } 246 | } 247 | 248 | // Create the new mount. 249 | err = os.MkdirAll(dest, 0o744) 250 | if err != nil { 251 | return xerrors.Errorf("mkdir -p %q: %w", dest, err) 252 | } 253 | err = mounter.Mount(mountType, dest, mountType, opts) 254 | if err != nil { 255 | return xerrors.Errorf("mount -t %q -o %q %q %q: %w", mountType, strings.Join(opts, ","), mountType, dest, err) 256 | } 257 | 258 | return nil 259 | } 260 | -------------------------------------------------------------------------------- /enterprise/exectrace_test.go: -------------------------------------------------------------------------------- 1 | // This file is licensed under the Coder Enterprise License. Please see 2 | // ../LICENSE.enterprise. 3 | package exectracewrapper 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/json" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "os/exec" 15 | "strconv" 16 | "strings" 17 | "testing" 18 | "time" 19 | 20 | "github.com/stretchr/testify/assert" 21 | "github.com/stretchr/testify/require" 22 | 23 | "github.com/coder/exectrace" 24 | 25 | "cdr.dev/slog" 26 | "cdr.dev/slog/sloggers/slogjson" 27 | ) 28 | 29 | func TestExectrace(t *testing.T) { 30 | t.Parallel() 31 | 32 | // This test must be run as root so we can start exectrace. 33 | if os.Geteuid() != 0 { 34 | t.Fatal("must be run as root") 35 | } 36 | 37 | currentPidNS, err := exectrace.GetPidNS() 38 | require.NoError(t, err) 39 | 40 | //nolint:paralleltest // Reserves a port 41 | t.Run("OK", func(t *testing.T) { 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | defer cancel() 44 | 45 | logBuf := bytes.NewBuffer(nil) 46 | log := slog.Make(slogjson.Sink(logBuf)).Leveled(slog.LevelDebug) 47 | 48 | l := getRandListener(t) 49 | _ = l.Close() 50 | addr := l.Addr().String() 51 | 52 | done := make(chan struct{}) 53 | go func() { 54 | defer close(done) 55 | 56 | err := Run(ctx, log, Options{ 57 | UseLocalPidNS: false, 58 | InitListenAddress: addr, 59 | StartupTimeout: 5 * time.Second, 60 | }) 61 | assert.Error(t, err) 62 | assert.ErrorIs(t, err, context.Canceled) 63 | }() 64 | 65 | // Post the PidNS to the listener. 66 | require.Eventually(t, func() bool { 67 | res, err := makeRequest(t, http.MethodPost, "http://"+addr, "text/plain", currentPidNS) 68 | if err != nil { 69 | return false 70 | } 71 | _ = res.Body.Close() 72 | 73 | return assert.Equal(t, http.StatusNoContent, res.StatusCode) 74 | }, 5*time.Second, 10*time.Millisecond) 75 | 76 | // Launch a process and wait for it to show up in the logs. 77 | require.Eventually(t, func() bool { 78 | const expected = "hello exectrace test 1" 79 | _, err := exec.CommandContext(ctx, "/bin/sh", "-c", "echo '"+expected+"'").CombinedOutput() 80 | if !assert.NoError(t, err) { 81 | return false 82 | } 83 | 84 | return strings.Contains(logBuf.String(), expected) 85 | }, 5*time.Second, 100*time.Millisecond) 86 | 87 | cancel() 88 | <-done 89 | }) 90 | 91 | t.Run("UseCurrentPidNS", func(t *testing.T) { 92 | t.Parallel() 93 | 94 | ctx, cancel := context.WithCancel(context.Background()) 95 | defer cancel() 96 | 97 | logBuf := bytes.NewBuffer(nil) 98 | log := slog.Make(slogjson.Sink(logBuf)).Leveled(slog.LevelDebug) 99 | 100 | done := make(chan struct{}) 101 | go func() { 102 | defer close(done) 103 | 104 | err := Run(ctx, log, Options{ 105 | UseLocalPidNS: true, 106 | StartupTimeout: 5 * time.Second, 107 | }) 108 | assert.Error(t, err) 109 | assert.ErrorIs(t, err, context.Canceled) 110 | }() 111 | 112 | // Launch a process and wait for it to show up in the logs. 113 | require.Eventually(t, func() bool { 114 | const expected = "hello exectrace test 2" 115 | _, err := exec.CommandContext(ctx, "/bin/sh", "-c", "echo '"+expected+"'").CombinedOutput() 116 | if !assert.NoError(t, err) { 117 | return false 118 | } 119 | 120 | return strings.Contains(logBuf.String(), expected) 121 | }, 5*time.Second, 100*time.Millisecond) 122 | 123 | cancel() 124 | <-done 125 | }) 126 | } 127 | 128 | func TestWaitForExectracePidNS(t *testing.T) { 129 | t.Parallel() 130 | 131 | t.Run("OK", func(t *testing.T) { 132 | t.Parallel() 133 | var ( 134 | ctx = context.Background() 135 | l = getRandListener(t) 136 | uri = (&url.URL{ 137 | Scheme: "http", 138 | Host: l.Addr().String(), 139 | Path: "/", 140 | }).String() 141 | done = make(chan struct{}, 1) 142 | expected uint32 = 12345 143 | ) 144 | 145 | go func() { 146 | defer close(done) 147 | res, err := makeRequest(t, http.MethodPost, uri, "text/plain", expected) 148 | assert.NoError(t, err) 149 | _ = res.Body.Close() 150 | assert.Equal(t, http.StatusNoContent, res.StatusCode) 151 | done <- struct{}{} 152 | }() 153 | 154 | got, err := waitForExectracePidNSListener(ctx, l) 155 | require.NoError(t, err) 156 | 157 | require.Equal(t, expected, got) 158 | 159 | <-done 160 | }) 161 | 162 | t.Run("BadRequest", func(t *testing.T) { 163 | t.Parallel() 164 | var ( 165 | ctx = context.Background() 166 | l = getRandListener(t) 167 | uri = (&url.URL{ 168 | Scheme: "http", 169 | Host: l.Addr().String(), 170 | Path: "/", 171 | }).String() 172 | ) 173 | 174 | var ( 175 | expected uint32 = 54321 176 | done = make(chan struct{}, 1) 177 | ) 178 | go func() { 179 | defer close(done) 180 | 181 | // Bad method. 182 | res, err := makeRequest(t, http.MethodGet, uri, "", nil) 183 | assert.NoError(t, err) 184 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 185 | assert.Contains(t, readResBody(t, res), "only accepts POST") 186 | 187 | // Bad path. 188 | res, err = makeRequest(t, http.MethodPost, uri+"path", "", nil) 189 | assert.NoError(t, err) 190 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 191 | assert.Contains(t, readResBody(t, res), "only accepts POST requests at /") 192 | 193 | // Bad Content-Type. 194 | res, err = makeRequest(t, http.MethodPost, uri, "application/json", nil) 195 | assert.NoError(t, err) 196 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 197 | assert.Contains(t, readResBody(t, res), "only accepts text/plain") 198 | 199 | // No body. 200 | res, err = makeRequest(t, http.MethodPost, uri, "text/plain", nil) 201 | assert.NoError(t, err) 202 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 203 | assert.Contains(t, readResBody(t, res), "Failed to parse request body") 204 | 205 | // Invalid uint32. 206 | res, err = makeRequest(t, http.MethodPost, uri, "text/plain", "yo") 207 | assert.NoError(t, err) 208 | assert.Equal(t, http.StatusBadRequest, res.StatusCode) 209 | assert.Contains(t, readResBody(t, res), "Failed to parse request body") 210 | 211 | // Real (with missing CT). 212 | res, err = makeRequest(t, http.MethodPost, uri, "", expected) 213 | assert.NoError(t, err) 214 | _ = res.Body.Close() 215 | assert.Equal(t, http.StatusNoContent, res.StatusCode) 216 | 217 | // Second post should fail since the server is closed. 218 | res, err = makeRequest(t, http.MethodPost, uri, "text/plain", expected) 219 | assert.Error(t, err) 220 | if err == nil { 221 | _ = res.Body.Close() 222 | } 223 | }() 224 | 225 | got, err := waitForExectracePidNSListener(ctx, l) 226 | require.NoError(t, err) 227 | require.Equal(t, expected, got) 228 | <-done 229 | }) 230 | 231 | t.Run("AlreadyListening", func(t *testing.T) { 232 | t.Parallel() 233 | 234 | l := getRandListener(t) 235 | _, err := waitForExectracePidNS(context.Background(), l.Addr().String()) 236 | require.Error(t, err) 237 | if err != nil { 238 | require.Contains(t, err.Error(), "address already in use") 239 | } 240 | }) 241 | } 242 | 243 | func getRandListener(t *testing.T) net.Listener { 244 | l, err := net.Listen("tcp", "localhost:0") 245 | require.NoError(t, err, "listen on available port") 246 | t.Cleanup(func() { 247 | _ = l.Close() 248 | }) 249 | return l 250 | } 251 | 252 | func makeRequest(t *testing.T, method, u string, ct string, body any) (*http.Response, error) { 253 | t.Helper() 254 | 255 | var b io.Reader 256 | if body != nil { 257 | switch v := body.(type) { 258 | case string: 259 | b = strings.NewReader(v) 260 | case uint32: 261 | b = strings.NewReader(strconv.Itoa(int(v))) 262 | case []byte: 263 | b = bytes.NewReader(v) 264 | case io.Reader: 265 | b = v 266 | default: 267 | x, err := json.Marshal(body) 268 | require.NoError(t, err) 269 | b = bytes.NewReader(x) 270 | } 271 | } 272 | 273 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 274 | defer cancel() 275 | 276 | req, err := http.NewRequestWithContext(ctx, method, u, b) 277 | require.NoError(t, err) 278 | 279 | if ct != "" { 280 | req.Header.Set("Content-Type", ct) 281 | } 282 | 283 | return http.DefaultClient.Do(req) 284 | } 285 | 286 | func readResBody(t *testing.T, res *http.Response) string { 287 | t.Helper() 288 | b, err := io.ReadAll(res.Body) 289 | assert.NoError(t, err) 290 | _ = res.Body.Close() 291 | 292 | return string(b) 293 | } 294 | -------------------------------------------------------------------------------- /enterprise/templates/kubernetes-envbox/README.md: -------------------------------------------------------------------------------- 1 | # envbox 2 | 3 | ## Introduction 4 | 5 | `envbox` is an image that enables creating non-privileged containers capable of 6 | running system-level software (e.g. `dockerd`, `systemd`, etc) in Kubernetes. 7 | 8 | It mainly acts as a wrapper for the excellent 9 | [sysbox runtime](https://github.com/nestybox/sysbox/) developed by 10 | [Nestybox](https://www.nestybox.com/). For more details on the security of 11 | `sysbox` containers see sysbox's 12 | [official documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/security.md). 13 | 14 | This template runs an 15 | [exectrace](https://github.com/coder/exectrace/tree/main/enterprise) sidecar 16 | container to log all processes started in the workspace. 17 | 18 | ## Envbox Configuration 19 | 20 | The following environment variables can be used to configure various aspects of 21 | the inner and outer container. 22 | 23 | | env | usage | required | 24 | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | 25 | | `CODER_INNER_IMAGE` | The image to use for the inner container. | True | 26 | | `CODER_INNER_USERNAME` | The username to use for the inner container. | True | 27 | | `CODER_AGENT_TOKEN` | The [Coder Agent](https://coder.com/docs/v2/latest/about/architecture#agents) token to pass to the inner container. | True | 28 | | `CODER_INNER_ENVS` | The environment variables to pass to the inner container. A wildcard can be used to match a prefix. Ex: `CODER_INNER_ENVS=KUBERNETES_*,MY_ENV,MY_OTHER_ENV` | false | 29 | | `CODER_INNER_HOSTNAME` | The hostname to use for the inner container. | false | 30 | | `CODER_IMAGE_PULL_SECRET` | The docker credentials to use when pulling the inner container. The recommended way to do this is to create an [Image Pull Secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials) and then reference the secret using an [environment variable](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data). | false | 31 | | `CODER_DOCKER_BRIDGE_CIDR` | The bridge CIDR to start the Docker daemon with. | false | 32 | | `CODER_MOUNTS` | A list of mounts to mount into the inner container. Mounts default to `rw`. Ex: `CODER_MOUNTS=/home/coder:/home/coder,/var/run/mysecret:/var/run/mysecret:ro` | false | 33 | | `CODER_USR_LIB_DIR` | The mountpoint of the host `/usr/lib` directory. Only required when using GPUs. | false | 34 | | `CODER_ADD_TUN` | If `CODER_ADD_TUN=true` add a TUN device to the inner container. | false | 35 | | `CODER_ADD_FUSE` | If `CODER_ADD_FUSE=true` add a FUSE device to the inner container. | false | 36 | | `CODER_ADD_GPU` | If `CODER_ADD_GPU=true` add detected GPUs and related files to the inner container. Requires setting `CODER_USR_LIB_DIR` and mounting in the hosts `/usr/lib/` directory. | false | 37 | | `CODER_CPUS` | Dictates the number of CPUs to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false | 38 | | `CODER_MEMORY` | Dictates the max memory (in bytes) to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false | 39 | 40 | # Migrating Existing Envbox Templates 41 | 42 | Due to the 43 | [deprecation and removal of legacy parameters](https://coder.com/docs/v2/latest/templates/parameters#legacy) 44 | it may be necessary to migrate existing envbox templates on newer versions of 45 | Coder. Consult the 46 | [migration](https://coder.com/docs/v2/latest/templates/parameters#migration) 47 | documentation for details on how to do so. 48 | 49 | To supply values to existing existing Terraform variables you can specify the 50 | `--variable` flag. For example 51 | 52 | ```bash 53 | coder templates create envbox --variable namespace="mynamespace" --variable max_cpus=2 --variable min_cpus=1 --variable max_memory=4 --variable min_memory=1 54 | ``` 55 | 56 | ## Contributions 57 | 58 | Contributions are welcome and can be made against the 59 | [envbox repo](https://github.com/coder/envbox). 60 | -------------------------------------------------------------------------------- /bpf/handler.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "bpf_core_read.h" 3 | #include "bpf_helpers.h" 4 | #include "vmlinux_core.h" 5 | 6 | // This license needs to be GPL-compatible because the BTF verifier won't let us 7 | // use many BPF helpers (including `bpf_probe_read_*`). 8 | u8 __license[] SEC("license") = "Dual MIT/GPL"; // NOLINT 9 | 10 | // Adds some extra log entries that are usually spam when deployed in the real 11 | // world. 12 | //#define DEBUG 13 | 14 | // These constants must be kept in sync with Go. 15 | #define ARGLEN 32 // maximum amount of args in argv we'll copy 16 | #define ARGSIZE 1024 // maximum byte length of each arg in argv we'll copy 17 | #define LOGFMTSIZE 1024 // maximum length of log fmt str sent back to userspace 18 | #define LOGARGLEN 3 // maximum amount of fmt arguments to a log entry 19 | 20 | // Maximum levels of PID namespace nesting. PID namespaces have a hierarchy 21 | // limit of 32 since kernel 3.7. 22 | #define MAX_PIDNS_HIERARCHY 32 23 | 24 | // This struct is defined according to 25 | // /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format 26 | struct exec_info { 27 | u16 common_type; // offset=0, size=2 28 | u8 common_flags; // offset=2, size=1 29 | u8 common_preempt_count; // offset=3, size=1 30 | s32 common_pid; // offset=4, size=4 31 | 32 | s32 syscall_nr; // offset=8, size=4 33 | u32 pad; // offset=12, size=4 (pad) 34 | const u8 *filename; // offset=16, size=8 (ptr) 35 | const u8 *const *argv; // offset=24, size=8 (ptr) 36 | const u8 *const *envp; // offset=32, size=8 (ptr) 37 | }; 38 | 39 | // The event struct. This struct must be kept in sync with the Golang 40 | // counterpart. 41 | struct event_t { 42 | // Details about the process being launched. 43 | u8 filename[ARGSIZE]; 44 | u8 argv[ARGLEN][ARGSIZE]; 45 | u32 argc; // set to ARGLEN + 1 if there were more than ARGLEN arguments 46 | u32 uid; 47 | u32 gid; 48 | u32 pid; 49 | 50 | // Name of the calling process. 51 | u8 comm[ARGSIZE]; 52 | }; 53 | 54 | static struct event_t zero_event SEC(".rodata") = { 55 | .filename = {0}, 56 | .argv = {}, 57 | .argc = 0, 58 | .uid = 0, 59 | .gid = 0, 60 | .pid = 0, 61 | .comm = {0}, 62 | }; 63 | 64 | // Log entry from eBPF to userspace. This struct must be kept in sync with the 65 | // Golang counterpart. 66 | struct log_entry_t { 67 | u32 uid; 68 | u32 gid; 69 | u32 pid; 70 | // fmt contains a format string that only contains "%d" and "%u" directives. 71 | // In userspace we will replace these with the arguments in `args`. 72 | u8 fmt[LOGFMTSIZE]; 73 | // These are communicated back to userspace as unsigned 32-bit integers, but 74 | // depending on the format string, they could be treated as signed or 75 | // unsigned. 76 | u32 args[LOGARGLEN]; 77 | }; 78 | 79 | static struct log_entry_t zero_log SEC(".rodata") = { 80 | .fmt = {0}, 81 | .args = {}, 82 | }; 83 | 84 | // This is the ring buffer we'll output events data to. The Go program reads 85 | // from this ring buffer and reads the data into a Go struct for easy usage. 86 | struct { 87 | __uint(type, BPF_MAP_TYPE_RINGBUF); 88 | __uint(max_entries, 1 << 24); 89 | } events SEC(".maps"); 90 | 91 | // The ring buffer we will output log entries to. 92 | struct { 93 | __uint(type, BPF_MAP_TYPE_RINGBUF); 94 | __uint(max_entries, 1 << 24); 95 | } logs SEC(".maps"); 96 | 97 | // The map we'll use to retrieve the configuration about the given filters. 98 | struct { 99 | __uint(type, BPF_MAP_TYPE_ARRAY); 100 | __uint(key_size, sizeof(u32)); 101 | __uint(value_size, sizeof(u32)); 102 | __uint(max_entries, 1); 103 | } filters SEC(".maps"); 104 | 105 | // Indexes in the `filters` map for each configuration option. 106 | static u32 filter_pidns_idx SEC(".rodata") = 0; 107 | 108 | // LOG[N] calls log() with the unused parameters zeroed out. `N` is the amount 109 | // of fmt args you want to use. 110 | #define LOG0(fmt) LOG3(fmt, 0, 0, 0) 111 | #define LOG1(fmt, arg0) LOG3(fmt, arg0, 0, 0) 112 | #define LOG2(fmt, arg0, arg1) LOG3(fmt, arg0, arg1, 0) 113 | #define LOG3(fmt, arg0, arg1, arg2) log(fmt, sizeof(fmt), arg0, arg1, arg2) 114 | 115 | // log logs to bpf_trace_printk() and sends the formatted log string to the logs 116 | // ringbuf. Call LOG[N]() instead of calling this directly. 117 | static void log(const char *fmt, u32 fmt_size, u32 arg0, u32 arg1, u32 arg2) { 118 | bpf_trace_printk(fmt, fmt_size, arg0, arg1, arg2); 119 | 120 | struct log_entry_t *entry; 121 | entry = bpf_ringbuf_reserve(&logs, sizeof(struct log_entry_t), 0); 122 | if (!entry) { 123 | bpf_printk("could not reserve logs ringbuf memory"); 124 | return; 125 | } 126 | 127 | // Zero out the log entry for safety. If we don't do this, we risk sending 128 | // random kernel memory back to userspace. 129 | s32 ret = bpf_probe_read_kernel(entry, sizeof(struct log_entry_t), &zero_log); 130 | if (ret < 0) { 131 | bpf_printk("zero out log: %d", ret); 132 | bpf_ringbuf_discard(entry, 0); 133 | return; 134 | } 135 | 136 | // Copy the fmt string into the log entry. 137 | // NOTE: bpf_snprintf is not supported in some of the lower kernel versions 138 | // we claim to support, so we have to do it this way. 139 | ret = bpf_probe_read_kernel_str(&entry->fmt, sizeof(entry->fmt), fmt); 140 | if (ret < 0) { 141 | bpf_printk("could not read fmt into log struct: %d", ret); 142 | bpf_ringbuf_discard(entry, 0); 143 | return; 144 | } 145 | 146 | entry->uid = bpf_get_current_uid_gid(); 147 | entry->gid = bpf_get_current_uid_gid() >> 32; // NOLINT(readability-magic-numbers) 148 | entry->pid = bpf_get_current_pid_tgid(); 149 | entry->args[0] = arg0; 150 | entry->args[1] = arg1; 151 | entry->args[2] = arg2; 152 | 153 | bpf_ringbuf_submit(entry, 0); 154 | } 155 | 156 | // filter_pidns checks if the current task is in a PID namespace equal to or 157 | // under the given target_pidns. Returns a 0 if successful, or a negative error 158 | // on failure. 159 | s32 filter_pidns(u32 target_pidns) { 160 | struct task_struct___exectrace *task = (void *)bpf_get_current_task(); // NOLINT(performance-no-int-to-ptr) 161 | 162 | struct pid_namespace___exectrace *pidns; 163 | s32 ret = BPF_CORE_READ_INTO(&pidns, task, nsproxy, pid_ns_for_children); 164 | if (ret) { 165 | LOG1("could not read current task pidns: %d", ret); 166 | return ret; 167 | } 168 | 169 | // Iterate up the PID NS tree until we either find the net namespace we're 170 | // filtering for, or until there are no more parent namespaces. 171 | u32 inum; 172 | u32 i = 0; 173 | for (; i < MAX_PIDNS_HIERARCHY; i++) { 174 | if (i != 0) { 175 | ret = BPF_CORE_READ_INTO(&pidns, pidns, parent); 176 | if (ret) { 177 | LOG2("could not read parent pidns on iteration %u: %d", i, ret); 178 | return ret; 179 | } 180 | } 181 | if (!pidns) { 182 | #ifdef DEBUG 183 | LOG1("no more pidns after %u iterations", i); 184 | #endif 185 | return -1; 186 | } 187 | 188 | ret = BPF_CORE_READ_INTO(&inum, pidns, ns.inum); 189 | if (ret) { 190 | LOG2("could not read pidns common on iteration %u: %d", i, ret); 191 | return ret; 192 | } 193 | 194 | #ifdef DEBUG 195 | LOG3("got pidns on iteration %u: %u (target=%u)", i, inum, target_pidns); 196 | #endif 197 | 198 | if (inum == target_pidns) { 199 | // One of the parent PID namespaces was the target PID namespace. 200 | return 0; 201 | } 202 | } 203 | 204 | // Iterated through all 32 parent PID namespaces and couldn't find what we 205 | // were looking for. 206 | #ifdef DEBUG 207 | LOG1("does not match pidns filter after %u iterations", i); 208 | #endif 209 | return -1; 210 | } 211 | 212 | // Tracepoint at the top of execve() syscall. 213 | SEC("tracepoint/syscalls/sys_enter_execve") 214 | s32 enter_execve(struct exec_info *ctx) { 215 | u32 *target_pidns = bpf_map_lookup_elem(&filters, &filter_pidns_idx); 216 | if (target_pidns && *target_pidns && filter_pidns(*target_pidns)) { 217 | return 1; 218 | } 219 | 220 | // Reserve memory for our event on the `events` ring buffer defined above. 221 | struct event_t *event; 222 | event = bpf_ringbuf_reserve(&events, sizeof(struct event_t), 0); 223 | if (!event) { 224 | LOG0("could not reserve events ringbuf memory"); 225 | return 1; 226 | } 227 | 228 | // Zero out the event for safety. If we don't do this, we risk sending 229 | // random kernel memory back to userspace. 230 | s32 ret = bpf_probe_read_kernel(event, sizeof(event), &zero_event); 231 | if (ret) { 232 | LOG1("zero out event: %d", ret); 233 | bpf_ringbuf_discard(event, 0); 234 | return 1; 235 | } 236 | 237 | // Store process/calling process details. 238 | event->uid = bpf_get_current_uid_gid(); 239 | event->gid = bpf_get_current_uid_gid() >> 32; // NOLINT(readability-magic-numbers) 240 | event->pid = bpf_get_current_pid_tgid(); 241 | ret = bpf_get_current_comm(&event->comm, sizeof(event->comm)); 242 | if (ret) { 243 | LOG1("could not get current comm: %d", ret); 244 | bpf_ringbuf_discard(event, 0); 245 | return 1; 246 | } 247 | 248 | // Write the filename in addition to argv[0] because the filename contains 249 | // the full path to the file which could be more useful in some situations. 250 | ret = bpf_probe_read_user_str(&event->filename, sizeof(event->filename), ctx->filename); 251 | if (ret < 0) { 252 | LOG1("could not read filename into event struct: %d", ret); 253 | bpf_ringbuf_discard(event, 0); 254 | return 1; 255 | } 256 | 257 | // Copy everything from ctx->argv to event->argv, incrementing event->argc 258 | // as we go. 259 | for (u32 i = 0; i < ARGLEN; i++) { 260 | if (!(&ctx->argv[i])) { 261 | goto out; 262 | } 263 | 264 | // Copying the arg into it's own variable before copying it into 265 | // event->argv[i] prevents memory corruption. 266 | const u8 *argp = NULL; 267 | ret = bpf_probe_read_user(&argp, sizeof(argp), &ctx->argv[i]); 268 | if (ret || !argp) { 269 | goto out; 270 | } 271 | 272 | // Copy argp to event->argv[i]. 273 | ret = bpf_probe_read_user_str(event->argv[i], sizeof(event->argv[i]), argp); 274 | if (ret < 0) { 275 | LOG2("read argv %u: %d", i, ret); 276 | goto out; 277 | } 278 | 279 | event->argc++; 280 | } 281 | 282 | // This won't get hit if we `goto out` in the loop above. This is to signify 283 | // to userspace that we couldn't copy all of the arguments because it 284 | // exceeded ARGLEN. 285 | event->argc++; 286 | 287 | out: 288 | // Write the event to the ring buffer and notify userspace. This will cause 289 | // the `Read()` call in userspace to return if it was blocked. 290 | bpf_ringbuf_submit(event, 0); 291 | 292 | return 0; 293 | } 294 | -------------------------------------------------------------------------------- /tracer_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package exectrace_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "strings" 14 | "syscall" 15 | "testing" 16 | "time" 17 | 18 | "github.com/stretchr/testify/require" 19 | "golang.org/x/xerrors" 20 | 21 | "github.com/coder/exectrace" 22 | ) 23 | 24 | //nolint:paralleltest 25 | func TestExectrace(t *testing.T) { 26 | // This test must be run as root so we can start exectrace. 27 | if os.Geteuid() != 0 { 28 | t.Fatal("must be run as root") 29 | } 30 | 31 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 32 | defer cancel() 33 | 34 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 35 | LogFn: func(uid, gid, pid uint32, logLine string) { 36 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 37 | }, 38 | }) 39 | require.NoError(t, err) 40 | defer tracer.Close() 41 | 42 | // Launch processes. 43 | const ( 44 | expected = "hello exectrace basic test" 45 | uid = 1000 46 | gid = 2000 47 | ) 48 | args := []string{"sh", "-c", "# " + expected} 49 | filename, err := exec.LookPath(args[0]) 50 | require.NoError(t, err) 51 | processDone := spamProcess(ctx, t, args, func(cmd *exec.Cmd) { 52 | cmd.SysProcAttr = &syscall.SysProcAttr{ 53 | Credential: &syscall.Credential{ 54 | Uid: uid, 55 | Gid: gid, 56 | }, 57 | } 58 | }) 59 | 60 | event := getLogEntry(ctx, t, tracer, expected) 61 | require.Equal(t, filename, event.Filename, "event.Filename") 62 | require.Equal(t, args, event.Argv, "event.Argv") 63 | require.False(t, event.Truncated, "event.Truncated is true") 64 | require.NotEqualValues(t, event.PID, 0, "event.PID should not be 0") 65 | require.NotEqual(t, event.PID, os.Getpid(), "event.PID should not be the parent PID") 66 | require.EqualValues(t, event.UID, uid, "event.UID should match custom UID") 67 | require.EqualValues(t, event.GID, gid, "event.GID should match custom GID") 68 | 69 | // Comm can either be parent.Argv[0] or the parent full binary path. 70 | executable, err := os.Executable() 71 | require.NoError(t, err) 72 | if event.Comm != executable { 73 | require.Equalf(t, filepath.Base(os.Args[0]), event.Comm, "event.Comm should match parent argv[0] (does not match executable %q)", executable) 74 | } 75 | 76 | cancel() 77 | <-processDone 78 | } 79 | 80 | //nolint:paralleltest 81 | func TestExectraceTruncatedArgs(t *testing.T) { 82 | // This test must be run as root so we can start exectrace. 83 | if os.Geteuid() != 0 { 84 | t.Fatal("must be run as root") 85 | } 86 | 87 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 88 | defer cancel() 89 | 90 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 91 | LogFn: func(uid, gid, pid uint32, logLine string) { 92 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 93 | }, 94 | }) 95 | require.NoError(t, err) 96 | defer tracer.Close() 97 | 98 | const expected = "hello exectrace overflow test" 99 | args := []string{"echo", expected} 100 | 101 | // Exectrace only captures the first 32 arguments of each process. 102 | for i := 0; i < 30; i++ { 103 | args = append(args, fmt.Sprint(i)) 104 | } 105 | args = append(args, "final") 106 | require.Len(t, args, 33) 107 | 108 | // Launch processes. 109 | processDone := spamProcess(ctx, t, args, nil) 110 | event := getLogEntry(ctx, t, tracer, expected) 111 | 112 | // Should only hold the first 32 args, and truncated should be true. 113 | require.Len(t, event.Argv, 32) 114 | require.Equal(t, args[:32], event.Argv, "event.Argv") 115 | require.True(t, event.Truncated, "event.Truncated is false") 116 | 117 | cancel() 118 | <-processDone 119 | } 120 | 121 | //nolint:paralleltest 122 | func TestExectraceTruncatedLongArg(t *testing.T) { 123 | // This test must be run as root so we can start exectrace. 124 | if os.Geteuid() != 0 { 125 | t.Fatal("must be run as root") 126 | } 127 | 128 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 129 | defer cancel() 130 | 131 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 132 | LogFn: func(uid, gid, pid uint32, logLine string) { 133 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 134 | }, 135 | }) 136 | require.NoError(t, err) 137 | defer tracer.Close() 138 | 139 | // We only record the first 1024 bytes of each argument, so use an arg 140 | // that's longer. 141 | const expected = "hello exectrace arg length test" 142 | args := []string{"echo", expected, strings.Repeat("a", 1025), "final"} 143 | 144 | // Launch processes. 145 | processDone := spamProcess(ctx, t, args, nil) 146 | event := getLogEntry(ctx, t, tracer, expected) 147 | 148 | // Should only hold the first 1021 chars of the long arg with a trailing 149 | // "...". 150 | args[2] = args[2][:1021] + "..." 151 | require.Equal(t, args, event.Argv, "event.Argv") 152 | require.True(t, event.Truncated, "event.Truncated is false") 153 | 154 | cancel() 155 | <-processDone 156 | } 157 | 158 | //nolint:paralleltest 159 | func TestExectracePIDNS(t *testing.T) { 160 | // This test must be run as root so we can start exectrace. 161 | if os.Geteuid() != 0 { 162 | t.Fatal("must be run as root") 163 | } 164 | 165 | //nolint:paralleltest 166 | t.Run("Same", func(t *testing.T) { 167 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 168 | defer cancel() 169 | // Filter by the current PidNS. 170 | pidNS, err := exectrace.GetPidNS() 171 | require.NoError(t, err) 172 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 173 | PidNS: pidNS, 174 | LogFn: func(uid, gid, pid uint32, logLine string) { 175 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 176 | }, 177 | }) 178 | require.NoError(t, err) 179 | defer tracer.Close() 180 | 181 | // Launch processes. 182 | const expected = "hello exectrace pidns test same" 183 | args := []string{"sh", "-c", "# " + expected} 184 | processDone := spamProcess(ctx, t, args, nil) 185 | 186 | _ = getLogEntry(ctx, t, tracer, expected) 187 | 188 | cancel() 189 | <-processDone 190 | }) 191 | 192 | //nolint:paralleltest 193 | t.Run("Child", func(t *testing.T) { 194 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 195 | defer cancel() 196 | // Filter by the current PidNS. 197 | pidNS, err := exectrace.GetPidNS() 198 | require.NoError(t, err) 199 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 200 | PidNS: pidNS, 201 | LogFn: func(uid, gid, pid uint32, logLine string) { 202 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 203 | }, 204 | }) 205 | require.NoError(t, err) 206 | defer tracer.Close() 207 | 208 | // Launch processes. 209 | const expected = "hello exectrace pidns test child" 210 | args := []string{"sh", "-c", "# " + expected} 211 | processDone := spamProcess(ctx, t, args, func(cmd *exec.Cmd) { 212 | cmd.SysProcAttr = &syscall.SysProcAttr{ 213 | // Subprocess will be in a child PID namespace. 214 | Cloneflags: syscall.CLONE_NEWPID, 215 | } 216 | }) 217 | 218 | _ = getLogEntry(ctx, t, tracer, expected) 219 | 220 | cancel() 221 | <-processDone 222 | }) 223 | 224 | //nolint:paralleltest 225 | t.Run("Different", func(t *testing.T) { 226 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 227 | defer cancel() 228 | // Filter by a slightly different PidNS. 229 | pidNS, err := exectrace.GetPidNS() 230 | require.NoError(t, err) 231 | tracer, err := exectrace.New(&exectrace.TracerOpts{ 232 | PidNS: pidNS + 1, 233 | LogFn: func(uid, gid, pid uint32, logLine string) { 234 | t.Errorf("tracer error log (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 235 | }, 236 | }) 237 | require.NoError(t, err) 238 | defer tracer.Close() 239 | 240 | // Launch processes. 241 | const expected = "hello exectrace pidns test different" 242 | args := []string{"sh", "-c", "# " + expected} 243 | processDone := spamProcess(ctx, t, args, nil) 244 | 245 | // We should not see any events. Read events for up to 5 seconds. 246 | go func() { 247 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 248 | defer cancel() 249 | <-ctx.Done() 250 | _ = tracer.Close() 251 | }() 252 | event, err := tracer.Read() 253 | if err == nil { 254 | t.Fatalf("unexpected event: %+v", event) 255 | } 256 | if !xerrors.Is(err, io.EOF) { 257 | t.Fatalf("tracer.Read: %v", err) 258 | } 259 | 260 | cancel() 261 | <-processDone 262 | }) 263 | } 264 | 265 | // spamProcess runs the given command every 100ms. The returned channel is 266 | // closed when the goroutine exits (either if there's a problem or if the 267 | // context is canceled). 268 | func spamProcess(ctx context.Context, t *testing.T, args []string, mutateCmdFn func(*exec.Cmd)) <-chan struct{} { 269 | done := make(chan struct{}) 270 | go func() { 271 | defer close(done) 272 | for { 273 | select { 274 | case <-ctx.Done(): 275 | return 276 | default: 277 | } 278 | 279 | //nolint:gosec 280 | cmd := exec.CommandContext(ctx, args[0], args[1:]...) 281 | if mutateCmdFn != nil { 282 | mutateCmdFn(cmd) 283 | } 284 | _, err := cmd.CombinedOutput() 285 | if err != nil { 286 | if ctx.Err() != nil { 287 | return 288 | } 289 | t.Errorf("command launch failure in spamProcess: %+v", err) 290 | return 291 | } 292 | time.Sleep(100 * time.Millisecond) 293 | } 294 | }() 295 | 296 | return done 297 | } 298 | 299 | // getLogEntry returns the next log entry from the tracer which contains the 300 | // given string in it's arguments. 301 | func getLogEntry(ctx context.Context, t *testing.T, tracer exectrace.Tracer, expected string) *exectrace.Event { 302 | t.Helper() 303 | 304 | // Kill the tracer when the context expires. 305 | go func() { 306 | <-ctx.Done() 307 | _ = tracer.Close() 308 | }() 309 | 310 | // Consume log lines until we find our process. 311 | for { 312 | select { 313 | case <-ctx.Done(): 314 | t.Fatal("timed out waiting for process") 315 | default: 316 | } 317 | 318 | event, err := tracer.Read() 319 | if err != nil { 320 | t.Fatalf("tracer.Read: %v", err) 321 | } 322 | 323 | t.Logf("event: %+v\n", event) 324 | joined := strings.Join(event.Argv, " ") 325 | if !strings.Contains(joined, expected) { 326 | t.Logf("above event does not match: %q does not contain %q", joined, expected) 327 | continue 328 | } 329 | t.Logf("above event matches: %q contains %q", joined, expected) 330 | return event 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /tracer_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package exectrace 5 | 6 | import ( 7 | "bytes" 8 | "encoding/binary" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "runtime" 14 | "runtime/debug" 15 | "strings" 16 | "sync" 17 | 18 | "github.com/cilium/ebpf" 19 | "github.com/cilium/ebpf/link" 20 | "github.com/cilium/ebpf/ringbuf" 21 | "github.com/cilium/ebpf/rlimit" 22 | "github.com/hashicorp/go-multierror" 23 | "golang.org/x/sys/unix" 24 | "golang.org/x/xerrors" 25 | ) 26 | 27 | // These constants are defined in `bpf/handler.c` and must be kept in sync. 28 | const ( 29 | arglen = 32 30 | argsize = 1024 31 | logfmtsize = 1024 32 | logarglen = 3 33 | ) 34 | 35 | var errTracerClosed = xerrors.New("tracer is closed") 36 | 37 | // event contains details about each exec call, sent from the eBPF program to 38 | // userspace through a perf ring buffer. This type must be kept in sync with 39 | // `event_t` in `bpf/handler.c`. 40 | type event struct { 41 | // Details about the process being launched. 42 | Filename [argsize]byte 43 | Argv [arglen][argsize]byte 44 | Argc uint32 45 | UID uint32 46 | GID uint32 47 | PID uint32 48 | 49 | // Name of the calling process. 50 | Comm [argsize]byte 51 | } 52 | 53 | // logEntry contains each kernel log entry from the logs ringbuf. This type must 54 | // be kept in sync with `log_entry_t` in `bpf/handler.c`. 55 | type logEntry struct { 56 | UID uint32 57 | GID uint32 58 | PID uint32 59 | Fmt [logfmtsize]byte 60 | // Args are uint32s but depending on the format string, they may be 61 | // interpreted as int32s instead. 62 | Arg [logarglen]uint32 63 | } 64 | 65 | type tracer struct { 66 | opts *TracerOpts 67 | 68 | objs *bpfObjects 69 | tp link.Link 70 | 71 | rbEvents *ringbuf.Reader 72 | rbLogs *ringbuf.Reader 73 | 74 | closeLock sync.Mutex 75 | closed chan struct{} 76 | } 77 | 78 | var _ Tracer = &tracer{} 79 | 80 | // New instantiates all of the BPF objects into the running kernel, starts 81 | // tracing, and returns the created Tracer. After calling this successfully, the 82 | // caller should immediately attach a for loop running `h.Read()`. 83 | // 84 | // The returned Tracer MUST be closed to avoid leaking kernel resources. 85 | func New(opts *TracerOpts) (Tracer, error) { 86 | if opts == nil { 87 | opts = &TracerOpts{} 88 | } 89 | if opts.LogFn == nil { 90 | opts.LogFn = func(uid, gid, pid uint32, logLine string) { 91 | log.Printf("error log from exectrace tracer (uid=%v, gid=%v, pid=%v): %s", uid, gid, pid, logLine) 92 | } 93 | } 94 | 95 | objs, err := loadBPFObjects() 96 | if err != nil { 97 | return nil, xerrors.Errorf("load BPF objects: %w", err) 98 | } 99 | 100 | t := &tracer{ 101 | opts: opts, 102 | objs: objs, 103 | tp: nil, 104 | rbEvents: nil, 105 | rbLogs: nil, 106 | 107 | closeLock: sync.Mutex{}, 108 | closed: make(chan struct{}), 109 | } 110 | err = t.start() 111 | if err != nil { 112 | // Best effort. 113 | _ = t.Close() 114 | return nil, xerrors.Errorf("start tracer: %w", err) 115 | } 116 | 117 | // It could be very bad if someone forgot to close this, so we'll try to 118 | // detect when it doesn't get closed and log a warning. 119 | stack := debug.Stack() 120 | runtime.SetFinalizer(t, func(t *tracer) { 121 | err := t.Close() 122 | if xerrors.Is(err, errTracerClosed) { 123 | return 124 | } 125 | 126 | log.Printf("tracer was finalized but was not closed, created at: %s", stack) 127 | log.Print("tracers must be closed when finished with to avoid leaked kernel resources") 128 | if err != nil { 129 | log.Printf("closing tracer failed: %+v", err) 130 | } 131 | }) 132 | 133 | return t, nil 134 | } 135 | 136 | func (t *tracer) FD() int { 137 | return t.objs.EnterExecveProg.FD() 138 | } 139 | 140 | func (t *tracer) start() error { 141 | // If we don't startup successfully, we need to make sure all of the stuff 142 | // is cleaned up properly or we'll be leaking kernel resources. 143 | ok := false 144 | defer func() { 145 | if !ok { 146 | // Best effort. 147 | _ = t.Close() 148 | } 149 | }() 150 | 151 | // Allow the current process to lock memory for eBPF resources. This does 152 | // nothing on 5.11+ kernels which don't need this. 153 | err := rlimit.RemoveMemlock() 154 | if err != nil { 155 | return xerrors.Errorf("remove memlock: %w", err) 156 | } 157 | 158 | // Set filter options on the filters map. 159 | if t.opts.PidNS != 0 { 160 | err = t.objs.FiltersMap.Update(uint32(0), t.opts.PidNS, ebpf.UpdateAny) 161 | if err != nil { 162 | return xerrors.Errorf("apply PID NS filter to eBPF map: %w", err) 163 | } 164 | } 165 | 166 | // Attach the eBPF program to the `sys_enter_execve` tracepoint, which 167 | // is triggered at the beginning of each `execve()` syscall. 168 | t.tp, err = link.Tracepoint("syscalls", "sys_enter_execve", t.objs.EnterExecveProg, nil) 169 | if err != nil { 170 | return xerrors.Errorf("open tracepoint: %w", err) 171 | } 172 | 173 | // Create the reader for the event ringbuf. 174 | t.rbEvents, err = ringbuf.NewReader(t.objs.EventsMap) 175 | if err != nil { 176 | return xerrors.Errorf("open events ringbuf reader: %w", err) 177 | } 178 | 179 | // Create the reader for the log ringbuf. 180 | t.rbLogs, err = ringbuf.NewReader(t.objs.LogsMap) 181 | if err != nil { 182 | return xerrors.Errorf("open logs ringbuf reader: %w", err) 183 | } 184 | 185 | // Start slurping up logs. 186 | go t.readLogs(t.rbLogs, t.opts.LogFn) 187 | 188 | ok = true 189 | return nil 190 | } 191 | 192 | // Read reads an event from the eBPF program via the ringbuf, parses it and 193 | // returns it. If the *tracer is closed during the blocked call, and error that 194 | // wraps io.EOF will be returned. 195 | func (t *tracer) Read() (*Event, error) { 196 | rb := t.rbEvents 197 | if rb == nil { 198 | return nil, xerrors.Errorf("events ringbuf reader is not initialized: %w", io.EOF) 199 | } 200 | 201 | record, err := rb.Read() 202 | if err != nil { 203 | if errors.Is(err, ringbuf.ErrClosed) { 204 | return nil, xerrors.Errorf("tracer closed: %w", io.EOF) 205 | } 206 | 207 | return nil, xerrors.Errorf("read from ringbuf: %w", err) 208 | } 209 | 210 | // Parse the ringbuf event entry into an event structure. 211 | var rawEvent event 212 | err = binary.Read(bytes.NewBuffer(record.RawSample), NativeEndian, &rawEvent) 213 | if err != nil { 214 | return nil, xerrors.Errorf("parse raw ringbuf entry into event struct: %w", err) 215 | } 216 | 217 | ev := &Event{ 218 | Filename: unix.ByteSliceToString(rawEvent.Filename[:]), 219 | Argv: []string{}, // populated below 220 | Truncated: rawEvent.Argc == arglen+1, 221 | PID: rawEvent.PID, 222 | UID: rawEvent.UID, 223 | GID: rawEvent.GID, 224 | Comm: unix.ByteSliceToString(rawEvent.Comm[:]), 225 | } 226 | 227 | // Copy only the args we're allowed to read from the array. If we read more 228 | // than rawEvent.Argc, we could be copying non-zeroed memory. 229 | argc := int(rawEvent.Argc) 230 | if argc > arglen { 231 | argc = arglen 232 | } 233 | for i := 0; i < argc; i++ { 234 | str := unix.ByteSliceToString(rawEvent.Argv[i][:]) 235 | // The copy in the eBPF code only copies 1023 bytes. 236 | if len(str) >= argsize-1 { 237 | ev.Truncated = true 238 | // Set final 3 bytes to "..." to indicate truncation. 239 | str = str[:argsize-3] + "..." 240 | } 241 | if strings.TrimSpace(str) != "" { 242 | ev.Argv = append(ev.Argv, str) 243 | } 244 | } 245 | 246 | return ev, nil 247 | } 248 | 249 | func (t *tracer) readLogs(rbLogs *ringbuf.Reader, logFn func(uid, gid, pid uint32, logLine string)) { 250 | defer func() { 251 | if r := recover(); r != nil { 252 | logFn(0, 0, 0, fmt.Sprintf("panic in (*tracer).readLogs() goroutine: %v", r)) 253 | _ = t.Close() 254 | } 255 | }() 256 | 257 | for { 258 | record, err := rbLogs.Read() 259 | if err != nil { 260 | if errors.Is(err, ringbuf.ErrClosed) { 261 | return 262 | } 263 | 264 | logFn(0, 0, 0, fmt.Sprintf("read from logs ringbuf: %+v", err)) 265 | continue 266 | } 267 | 268 | var logEntry logEntry 269 | err = binary.Read(bytes.NewBuffer(record.RawSample), NativeEndian, &logEntry) 270 | if err != nil { 271 | logFn(0, 0, 0, fmt.Sprintf("parse raw ringbuf entry into logEntry struct: %+v", err)) 272 | continue 273 | } 274 | 275 | // Format the log line. 276 | // 1. Find all %u and %d directives in the string (this is all we 277 | // support). 278 | // 2. For each: 279 | // 1. If it's a %u, replace it with the next uint32 in the args. 280 | // 2. If it's a %d, cast the next uint32 to an int32 and replace. 281 | logLine := unix.ByteSliceToString(logEntry.Fmt[:]) 282 | for i := 0; i < logarglen; i++ { 283 | arg := logEntry.Arg[i] 284 | 285 | // Find the next %u or %d in the log line. 286 | uIndex := strings.Index(logLine, `%u`) 287 | dIndex := strings.Index(logLine, `%d`) 288 | if uIndex == -1 && dIndex == -1 { 289 | break 290 | } 291 | if uIndex < dIndex || dIndex == -1 { 292 | logLine = strings.Replace(logLine, `%u`, fmt.Sprint(arg), 1) 293 | } 294 | if dIndex < uIndex || uIndex == -1 { 295 | logLine = strings.Replace(logLine, `%d`, fmt.Sprint(int32(arg)), 1) //nolint:gosec // we intentionally want to cast directly to int32 296 | } 297 | } 298 | 299 | logFn(logEntry.UID, logEntry.GID, logEntry.PID, logLine) 300 | } 301 | } 302 | 303 | // Close gracefully closes and frees all resources associated with the eBPF 304 | // tracepoints, maps and other resources. Any blocked `Read()` operations will 305 | // return an error that wraps `io.EOF`. 306 | func (t *tracer) Close() error { 307 | t.closeLock.Lock() 308 | defer t.closeLock.Unlock() 309 | select { 310 | case <-t.closed: 311 | return errTracerClosed 312 | default: 313 | } 314 | close(t.closed) 315 | runtime.SetFinalizer(t, nil) 316 | 317 | // Close everything started in h.Start() in reverse order. 318 | var merr error 319 | if t.rbLogs != nil { 320 | err := t.rbLogs.Close() 321 | if err != nil { 322 | merr = multierror.Append(merr, xerrors.Errorf("close logs ringbuf reader: %w", err)) 323 | } 324 | } 325 | if t.rbEvents != nil { 326 | err := t.rbEvents.Close() 327 | if err != nil { 328 | merr = multierror.Append(merr, xerrors.Errorf("close events ringbuf reader: %w", err)) 329 | } 330 | } 331 | if t.tp != nil { 332 | err := t.tp.Close() 333 | if err != nil { 334 | merr = multierror.Append(merr, xerrors.Errorf("close tracepoint: %w", err)) 335 | } 336 | } 337 | if t.objs != nil { 338 | err := t.objs.Close() 339 | if err != nil { 340 | merr = multierror.Append(merr, xerrors.Errorf("close eBPF objects: %w", err)) 341 | } 342 | } 343 | 344 | return merr 345 | } 346 | -------------------------------------------------------------------------------- /enterprise/templates/kubernetes-envbox/main.tf: -------------------------------------------------------------------------------- 1 | // This template file is similar to the upstream Coder Kubernetes template. 2 | // 3 | // Changes: 4 | // - Adds the exectrace sidecar container. 5 | // - Updates the workspace container command to send the process ID namespace 6 | // inum to the sidecar container. 7 | // - Updates the workspace agent subsystem to "exectrace" for telemetry. 8 | // 9 | // If you make changes to the exectrace components in this file, please update 10 | // the corresponding docs on coder.com. 11 | 12 | terraform { 13 | required_providers { 14 | coder = { 15 | source = "coder/coder" 16 | version = "0.6.12" 17 | } 18 | kubernetes = { 19 | source = "hashicorp/kubernetes" 20 | version = "~> 2.12.1" 21 | } 22 | } 23 | } 24 | 25 | data "coder_parameter" "home_disk" { 26 | name = "Disk Size" 27 | description = "How large should the disk storing the home directory be?" 28 | icon = "https://cdn-icons-png.flaticon.com/512/2344/2344147.png" 29 | type = "number" 30 | default = 10 31 | mutable = true 32 | validation { 33 | min = 10 34 | max = 100 35 | } 36 | } 37 | 38 | variable "use_kubeconfig" { 39 | type = bool 40 | sensitive = true 41 | default = true 42 | description = <<-EOF 43 | Use host kubeconfig? (true/false) 44 | Set this to false if the Coder host is itself running as a Pod on the same 45 | Kubernetes cluster as you are deploying workspaces to. 46 | Set this to true if the Coder host is running outside the Kubernetes cluster 47 | for workspaces. A valid "~/.kube/config" must be present on the Coder host. 48 | EOF 49 | } 50 | 51 | provider "coder" { 52 | feature_use_managed_variables = true 53 | } 54 | 55 | variable "namespace" { 56 | type = string 57 | sensitive = true 58 | description = "The namespace to create workspaces in (must exist prior to creating workspaces)" 59 | } 60 | 61 | variable "create_tun" { 62 | type = bool 63 | sensitive = true 64 | description = "Add a TUN device to the workspace." 65 | default = false 66 | } 67 | 68 | variable "create_fuse" { 69 | type = bool 70 | description = "Add a FUSE device to the workspace." 71 | sensitive = true 72 | default = false 73 | } 74 | 75 | variable "max_cpus" { 76 | type = string 77 | sensitive = true 78 | description = "Max number of CPUs the workspace may use (e.g. 2)." 79 | } 80 | 81 | variable "min_cpus" { 82 | type = string 83 | sensitive = true 84 | description = "Minimum number of CPUs the workspace may use (e.g. .1)." 85 | } 86 | 87 | variable "max_memory" { 88 | type = string 89 | description = "Maximum amount of memory to allocate the workspace (in GB)." 90 | sensitive = true 91 | } 92 | 93 | variable "min_memory" { 94 | type = string 95 | description = "Minimum amount of memory to allocate the workspace (in GB)." 96 | sensitive = true 97 | } 98 | 99 | provider "kubernetes" { 100 | # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences 101 | config_path = var.use_kubeconfig == true ? "~/.kube/config" : null 102 | } 103 | 104 | data "coder_workspace" "me" {} 105 | 106 | resource "coder_agent" "main" { 107 | os = "linux" 108 | arch = "amd64" 109 | startup_script = </dev/null 2>&1; then 169 | echo "curl is required to download the Coder binary" 170 | echo "Please install curl to your image and try again" 171 | # 127 is command not found. 172 | exit 127 173 | fi 174 | 175 | echo "Sending process ID namespace inum to exectrace sidecar" 176 | rc=0 177 | max_retry=5 178 | counter=0 179 | until [ $counter -ge $max_retry ]; do 180 | set +e 181 | curl \ 182 | --fail \ 183 | --silent \ 184 | --connect-timeout 5 \ 185 | -X POST \ 186 | -H "Content-Type: text/plain" \ 187 | --data "$pidns_inum" \ 188 | http://127.0.0.1:56123 189 | rc=$? 190 | set -e 191 | if [ $rc -eq 0 ]; then 192 | break 193 | fi 194 | 195 | counter=$((counter+1)) 196 | echo "Curl failed with exit code $${rc}, attempt $${counter}/$${max_retry}; Retrying in 3 seconds..." 197 | sleep 3 198 | done 199 | if [ $rc -ne 0 ]; then 200 | echo "Failed to send process ID namespace inum to exectrace sidecar" 201 | exit $rc 202 | fi 203 | 204 | EOT 205 | } 206 | 207 | resource "kubernetes_pod" "main" { 208 | count = data.coder_workspace.me.start_count 209 | 210 | metadata { 211 | name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}" 212 | namespace = var.namespace 213 | } 214 | 215 | spec { 216 | restart_policy = "Never" 217 | 218 | // NOTE: this container is added compared to the upstream kubernetes 219 | // template 220 | container { 221 | name = "exectrace" 222 | image = "ghcr.io/coder/exectrace:latest" 223 | image_pull_policy = "Always" 224 | command = [ 225 | "/opt/exectrace", 226 | "--init-address", "127.0.0.1:56123", 227 | "--label", "workspace_id=${data.coder_workspace.me.id}", 228 | "--label", "workspace_name=${data.coder_workspace.me.name}", 229 | "--label", "user_id=${data.coder_workspace.me.owner_id}", 230 | "--label", "username=${data.coder_workspace.me.owner}", 231 | "--label", "user_email=${data.coder_workspace.me.owner_email}", 232 | ] 233 | security_context { 234 | // exectrace must be started as root so it can attach probes into the 235 | // kernel to record process events with high throughput. 236 | run_as_user = "0" 237 | run_as_group = "0" 238 | // exectrace requires a privileged container so it can control mounts 239 | // and perform privileged syscalls against the host kernel to attach 240 | // probes. 241 | privileged = true 242 | } 243 | } 244 | 245 | container { 246 | name = "dev" 247 | image = "ghcr.io/coder/envbox:latest" 248 | image_pull_policy = "Always" 249 | // NOTE: this command is changed compared to the upstream kubernetes 250 | // template 251 | command = [ 252 | "sh", 253 | "-c", 254 | "${local.exectrace_init_script}\n\nexec /envbox docker", 255 | ] 256 | 257 | security_context { 258 | privileged = true 259 | } 260 | 261 | resources { 262 | requests = { 263 | "cpu" : "${var.min_cpus}" 264 | "memory" : "${var.min_memory}G" 265 | } 266 | 267 | limits = { 268 | "cpu" : "${var.max_cpus}" 269 | "memory" : "${var.max_memory}G" 270 | } 271 | } 272 | 273 | env { 274 | name = "CODER_AGENT_TOKEN" 275 | value = coder_agent.main.token 276 | } 277 | 278 | env { 279 | name = "CODER_AGENT_URL" 280 | value = data.coder_workspace.me.access_url 281 | } 282 | env { 283 | name = "CODER_AGENT_SUBSYSTEM" 284 | # The value "envbox" gets added by envbox. 285 | value = "exectrace" 286 | } 287 | 288 | env { 289 | name = "CODER_INNER_IMAGE" 290 | value = "index.docker.io/codercom/enterprise-base@sha256:069e84783d134841cbb5007a16d9025b6aed67bc5b95eecc118eb96dccd6de68" 291 | } 292 | 293 | env { 294 | name = "CODER_INNER_USERNAME" 295 | value = "coder" 296 | } 297 | 298 | env { 299 | name = "CODER_BOOTSTRAP_SCRIPT" 300 | value = coder_agent.main.init_script 301 | } 302 | 303 | env { 304 | name = "CODER_MOUNTS" 305 | value = "/home/coder:/home/coder" 306 | } 307 | 308 | env { 309 | name = "CODER_ADD_FUSE" 310 | value = var.create_fuse 311 | } 312 | 313 | env { 314 | name = "CODER_INNER_HOSTNAME" 315 | value = data.coder_workspace.me.name 316 | } 317 | 318 | env { 319 | name = "CODER_ADD_TUN" 320 | value = var.create_tun 321 | } 322 | 323 | env { 324 | name = "CODER_CPUS" 325 | value_from { 326 | resource_field_ref { 327 | resource = "limits.cpu" 328 | } 329 | } 330 | } 331 | 332 | env { 333 | name = "CODER_MEMORY" 334 | value_from { 335 | resource_field_ref { 336 | resource = "limits.memory" 337 | } 338 | } 339 | } 340 | 341 | volume_mount { 342 | mount_path = "/home/coder" 343 | name = "home" 344 | read_only = false 345 | sub_path = "home" 346 | } 347 | 348 | volume_mount { 349 | mount_path = "/var/lib/coder/docker" 350 | name = "home" 351 | sub_path = "cache/docker" 352 | } 353 | 354 | volume_mount { 355 | mount_path = "/var/lib/coder/containers" 356 | name = "home" 357 | sub_path = "cache/containers" 358 | } 359 | 360 | volume_mount { 361 | mount_path = "/var/lib/sysbox" 362 | name = "sysbox" 363 | } 364 | 365 | volume_mount { 366 | mount_path = "/var/lib/containers" 367 | name = "home" 368 | sub_path = "envbox/containers" 369 | } 370 | 371 | volume_mount { 372 | mount_path = "/var/lib/docker" 373 | name = "home" 374 | sub_path = "envbox/docker" 375 | } 376 | 377 | volume_mount { 378 | mount_path = "/usr/src" 379 | name = "usr-src" 380 | } 381 | 382 | volume_mount { 383 | mount_path = "/lib/modules" 384 | name = "lib-modules" 385 | } 386 | } 387 | 388 | volume { 389 | name = "home" 390 | persistent_volume_claim { 391 | claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name 392 | read_only = false 393 | } 394 | } 395 | 396 | volume { 397 | name = "sysbox" 398 | empty_dir {} 399 | } 400 | 401 | volume { 402 | name = "usr-src" 403 | host_path { 404 | path = "/usr/src" 405 | type = "" 406 | } 407 | } 408 | 409 | volume { 410 | name = "lib-modules" 411 | host_path { 412 | path = "/lib/modules" 413 | type = "" 414 | } 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /enterprise/templates/kubernetes/main.tf: -------------------------------------------------------------------------------- 1 | // This template file is similar to the upstream Coder Kubernetes template. 2 | // 3 | // Changes: 4 | // - Adds the exectrace sidecar container. 5 | // - Updates the workspace container command to send the process ID namespace 6 | // inum to the sidecar container. 7 | // - Updates the workspace agent subsystem to "exectrace" for telemetry. 8 | // 9 | // If you make changes to the exectrace components in this file, please update 10 | // the corresponding docs on coder.com. 11 | 12 | terraform { 13 | required_providers { 14 | coder = { 15 | source = "coder/coder" 16 | version = "~> 0.7.0" 17 | } 18 | kubernetes = { 19 | source = "hashicorp/kubernetes" 20 | version = "~> 2.18" 21 | } 22 | } 23 | } 24 | 25 | provider "coder" { 26 | feature_use_managed_variables = true 27 | } 28 | 29 | variable "use_kubeconfig" { 30 | type = bool 31 | description = <<-EOF 32 | Use host kubeconfig? (true/false) 33 | 34 | Set this to false if the Coder host is itself running as a Pod on the same 35 | Kubernetes cluster as you are deploying workspaces to. 36 | 37 | Set this to true if the Coder host is running outside the Kubernetes cluster 38 | for workspaces. A valid "~/.kube/config" must be present on the Coder host. 39 | EOF 40 | default = false 41 | } 42 | 43 | variable "namespace" { 44 | type = string 45 | description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces)" 46 | } 47 | 48 | data "coder_parameter" "cpu" { 49 | name = "cpu" 50 | display_name = "CPU" 51 | description = "The number of CPU cores" 52 | default = "2" 53 | icon = "/icon/memory.svg" 54 | mutable = true 55 | option { 56 | name = "2 Cores" 57 | value = "2" 58 | } 59 | option { 60 | name = "4 Cores" 61 | value = "4" 62 | } 63 | option { 64 | name = "6 Cores" 65 | value = "6" 66 | } 67 | option { 68 | name = "8 Cores" 69 | value = "8" 70 | } 71 | } 72 | 73 | data "coder_parameter" "memory" { 74 | name = "memory" 75 | display_name = "Memory" 76 | description = "The amount of memory in GB" 77 | default = "2" 78 | icon = "/icon/memory.svg" 79 | mutable = true 80 | option { 81 | name = "2 GB" 82 | value = "2" 83 | } 84 | option { 85 | name = "4 GB" 86 | value = "4" 87 | } 88 | option { 89 | name = "6 GB" 90 | value = "6" 91 | } 92 | option { 93 | name = "8 GB" 94 | value = "8" 95 | } 96 | } 97 | 98 | data "coder_parameter" "home_disk_size" { 99 | name = "home_disk_size" 100 | display_name = "Home disk size" 101 | description = "The size of the home disk in GB" 102 | default = "10" 103 | type = "number" 104 | icon = "/emojis/1f4be.png" 105 | mutable = false 106 | validation { 107 | min = 1 108 | max = 99999 109 | } 110 | } 111 | 112 | provider "kubernetes" { 113 | # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences 114 | config_path = var.use_kubeconfig == true ? "~/.kube/config" : null 115 | } 116 | 117 | data "coder_workspace" "me" {} 118 | 119 | resource "coder_agent" "main" { 120 | os = "linux" 121 | arch = "amd64" 122 | startup_script_timeout = 180 123 | startup_script = <<-EOT 124 | set -e 125 | 126 | # install and start code-server 127 | curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0 128 | /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & 129 | EOT 130 | 131 | # The following metadata blocks are optional. They are used to display 132 | # information about your workspace in the dashboard. You can remove them 133 | # if you don't want to display any information. 134 | # For basic resources, you can use the `coder stat` command. 135 | # If you need more control, you can write your own script. 136 | metadata { 137 | display_name = "CPU Usage" 138 | key = "0_cpu_usage" 139 | script = "coder stat cpu" 140 | interval = 10 141 | timeout = 1 142 | } 143 | 144 | metadata { 145 | display_name = "RAM Usage" 146 | key = "1_ram_usage" 147 | script = "coder stat mem" 148 | interval = 10 149 | timeout = 1 150 | } 151 | 152 | metadata { 153 | display_name = "Home Disk" 154 | key = "3_home_disk" 155 | script = "coder stat disk --path $${HOME}" 156 | interval = 60 157 | timeout = 1 158 | } 159 | 160 | metadata { 161 | display_name = "CPU Usage (Host)" 162 | key = "4_cpu_usage_host" 163 | script = "coder stat cpu --host" 164 | interval = 10 165 | timeout = 1 166 | } 167 | 168 | metadata { 169 | display_name = "Memory Usage (Host)" 170 | key = "5_mem_usage_host" 171 | script = "coder stat mem --host" 172 | interval = 10 173 | timeout = 1 174 | } 175 | 176 | metadata { 177 | display_name = "Load Average (Host)" 178 | key = "6_load_host" 179 | # get load avg scaled by number of cores 180 | script = </dev/null 2>&1; then 247 | echo "curl is required to download the Coder binary" 248 | echo "Please install curl to your image and try again" 249 | # 127 is command not found. 250 | exit 127 251 | fi 252 | 253 | echo "Sending process ID namespace inum to exectrace sidecar" 254 | rc=0 255 | max_retry=5 256 | counter=0 257 | until [ $counter -ge $max_retry ]; do 258 | set +e 259 | curl \ 260 | --fail \ 261 | --silent \ 262 | --connect-timeout 5 \ 263 | -X POST \ 264 | -H "Content-Type: text/plain" \ 265 | --data "$pidns_inum" \ 266 | http://127.0.0.1:56123 267 | rc=$? 268 | set -e 269 | if [ $rc -eq 0 ]; then 270 | break 271 | fi 272 | 273 | counter=$((counter+1)) 274 | echo "Curl failed with exit code $${rc}, attempt $${counter}/$${max_retry}; Retrying in 3 seconds..." 275 | sleep 3 276 | done 277 | if [ $rc -ne 0 ]; then 278 | echo "Failed to send process ID namespace inum to exectrace sidecar" 279 | exit $rc 280 | fi 281 | 282 | EOT 283 | } 284 | 285 | resource "kubernetes_pod" "main" { 286 | count = data.coder_workspace.me.start_count 287 | metadata { 288 | name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}" 289 | namespace = var.namespace 290 | labels = { 291 | "app.kubernetes.io/name" = "coder-workspace" 292 | "app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}" 293 | "app.kubernetes.io/part-of" = "coder" 294 | // Coder specific labels. 295 | "com.coder.resource" = "true" 296 | "com.coder.workspace.id" = data.coder_workspace.me.id 297 | "com.coder.workspace.name" = data.coder_workspace.me.name 298 | "com.coder.user.id" = data.coder_workspace.me.owner_id 299 | "com.coder.user.username" = data.coder_workspace.me.owner 300 | } 301 | annotations = { 302 | "com.coder.user.email" = data.coder_workspace.me.owner_email 303 | } 304 | } 305 | spec { 306 | security_context { 307 | run_as_user = "1000" 308 | fs_group = "1000" 309 | } 310 | 311 | // NOTE: this container is added compared to the upstream kubernetes 312 | // template 313 | container { 314 | name = "exectrace" 315 | image = "ghcr.io/coder/exectrace:latest" 316 | image_pull_policy = "Always" 317 | command = [ 318 | "/opt/exectrace", 319 | "--init-address", "127.0.0.1:56123", 320 | "--label", "workspace_id=${data.coder_workspace.me.id}", 321 | "--label", "workspace_name=${data.coder_workspace.me.name}", 322 | "--label", "user_id=${data.coder_workspace.me.owner_id}", 323 | "--label", "username=${data.coder_workspace.me.owner}", 324 | "--label", "user_email=${data.coder_workspace.me.owner_email}", 325 | ] 326 | security_context { 327 | // exectrace must be started as root so it can attach probes into the 328 | // kernel to record process events with high throughput. 329 | run_as_user = "0" 330 | run_as_group = "0" 331 | // exectrace requires a privileged container so it can control mounts 332 | // and perform privileged syscalls against the host kernel to attach 333 | // probes. 334 | privileged = true 335 | } 336 | } 337 | 338 | container { 339 | name = "dev" 340 | image = "codercom/enterprise-base:ubuntu" 341 | image_pull_policy = "Always" 342 | // NOTE: this command is changed compared to the upstream kubernetes 343 | // template 344 | command = [ 345 | "sh", 346 | "-c", 347 | "${local.exectrace_init_script}\n\n${coder_agent.main.init_script}", 348 | ] 349 | security_context { 350 | run_as_user = "1000" 351 | } 352 | env { 353 | name = "CODER_AGENT_TOKEN" 354 | value = coder_agent.main.token 355 | } 356 | env { 357 | name = "CODER_AGENT_SUBSYSTEM" 358 | value = "exectrace" 359 | } 360 | resources { 361 | requests = { 362 | "cpu" = "250m" 363 | "memory" = "512Mi" 364 | } 365 | limits = { 366 | "cpu" = "${data.coder_parameter.cpu.value}" 367 | "memory" = "${data.coder_parameter.memory.value}Gi" 368 | } 369 | } 370 | volume_mount { 371 | mount_path = "/home/coder" 372 | name = "home" 373 | read_only = false 374 | } 375 | } 376 | 377 | volume { 378 | name = "home" 379 | persistent_volume_claim { 380 | claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name 381 | read_only = false 382 | } 383 | } 384 | 385 | 386 | affinity { 387 | pod_anti_affinity { 388 | // This affinity attempts to spread out all workspace pods evenly across 389 | // nodes. 390 | preferred_during_scheduling_ignored_during_execution { 391 | weight = 1 392 | pod_affinity_term { 393 | topology_key = "kubernetes.io/hostname" 394 | label_selector { 395 | match_expressions { 396 | key = "app.kubernetes.io/name" 397 | operator = "In" 398 | values = ["coder-workspace"] 399 | } 400 | } 401 | } 402 | } 403 | } 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /LICENSE.GPL: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022 Coder Technologies, Inc. 2 | 3 | This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License along 14 | with this program; if not, write to the Free Software Foundation, Inc., 15 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | You can contact the copyright holder by using the contact form found at 18 | https://coder.com. 19 | 20 | 21 | 22 | GNU GENERAL PUBLIC LICENSE 23 | Version 2, June 1991 24 | 25 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 26 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 27 | Everyone is permitted to copy and distribute verbatim copies 28 | of this license document, but changing it is not allowed. 29 | 30 | Preamble 31 | 32 | The licenses for most software are designed to take away your 33 | freedom to share and change it. By contrast, the GNU General Public 34 | License is intended to guarantee your freedom to share and change free 35 | software--to make sure the software is free for all its users. This 36 | General Public License applies to most of the Free Software 37 | Foundation's software and to any other program whose authors commit to 38 | using it. (Some other Free Software Foundation software is covered by 39 | the GNU Lesser General Public License instead.) You can apply it to 40 | your programs, too. 41 | 42 | When we speak of free software, we are referring to freedom, not 43 | price. Our General Public Licenses are designed to make sure that you 44 | have the freedom to distribute copies of free software (and charge for 45 | this service if you wish), that you receive source code or can get it 46 | if you want it, that you can change the software or use pieces of it 47 | in new free programs; and that you know you can do these things. 48 | 49 | To protect your rights, we need to make restrictions that forbid 50 | anyone to deny you these rights or to ask you to surrender the rights. 51 | These restrictions translate to certain responsibilities for you if you 52 | distribute copies of the software, or if you modify it. 53 | 54 | For example, if you distribute copies of such a program, whether 55 | gratis or for a fee, you must give the recipients all the rights that 56 | you have. You must make sure that they, too, receive or can get the 57 | source code. And you must show them these terms so they know their 58 | rights. 59 | 60 | We protect your rights with two steps: (1) copyright the software, and 61 | (2) offer you this license which gives you legal permission to copy, 62 | distribute and/or modify the software. 63 | 64 | Also, for each author's protection and ours, we want to make certain 65 | that everyone understands that there is no warranty for this free 66 | software. If the software is modified by someone else and passed on, we 67 | want its recipients to know that what they have is not the original, so 68 | that any problems introduced by others will not reflect on the original 69 | authors' reputations. 70 | 71 | Finally, any free program is threatened constantly by software 72 | patents. We wish to avoid the danger that redistributors of a free 73 | program will individually obtain patent licenses, in effect making the 74 | program proprietary. To prevent this, we have made it clear that any 75 | patent must be licensed for everyone's free use or not licensed at all. 76 | 77 | The precise terms and conditions for copying, distribution and 78 | modification follow. 79 | 80 | GNU GENERAL PUBLIC LICENSE 81 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 82 | 83 | 0. This License applies to any program or other work which contains 84 | a notice placed by the copyright holder saying it may be distributed 85 | under the terms of this General Public License. The "Program", below, 86 | refers to any such program or work, and a "work based on the Program" 87 | means either the Program or any derivative work under copyright law: 88 | that is to say, a work containing the Program or a portion of it, 89 | either verbatim or with modifications and/or translated into another 90 | language. (Hereinafter, translation is included without limitation in 91 | the term "modification".) Each licensee is addressed as "you". 92 | 93 | Activities other than copying, distribution and modification are not 94 | covered by this License; they are outside its scope. The act of 95 | running the Program is not restricted, and the output from the Program 96 | is covered only if its contents constitute a work based on the 97 | Program (independent of having been made by running the Program). 98 | Whether that is true depends on what the Program does. 99 | 100 | 1. You may copy and distribute verbatim copies of the Program's 101 | source code as you receive it, in any medium, provided that you 102 | conspicuously and appropriately publish on each copy an appropriate 103 | copyright notice and disclaimer of warranty; keep intact all the 104 | notices that refer to this License and to the absence of any warranty; 105 | and give any other recipients of the Program a copy of this License 106 | along with the Program. 107 | 108 | You may charge a fee for the physical act of transferring a copy, and 109 | you may at your option offer warranty protection in exchange for a fee. 110 | 111 | 2. You may modify your copy or copies of the Program or any portion 112 | of it, thus forming a work based on the Program, and copy and 113 | distribute such modifications or work under the terms of Section 1 114 | above, provided that you also meet all of these conditions: 115 | 116 | a) You must cause the modified files to carry prominent notices 117 | stating that you changed the files and the date of any change. 118 | 119 | b) You must cause any work that you distribute or publish, that in 120 | whole or in part contains or is derived from the Program or any 121 | part thereof, to be licensed as a whole at no charge to all third 122 | parties under the terms of this License. 123 | 124 | c) If the modified program normally reads commands interactively 125 | when run, you must cause it, when started running for such 126 | interactive use in the most ordinary way, to print or display an 127 | announcement including an appropriate copyright notice and a 128 | notice that there is no warranty (or else, saying that you provide 129 | a warranty) and that users may redistribute the program under 130 | these conditions, and telling the user how to view a copy of this 131 | License. (Exception: if the Program itself is interactive but 132 | does not normally print such an announcement, your work based on 133 | the Program is not required to print an announcement.) 134 | 135 | These requirements apply to the modified work as a whole. If 136 | identifiable sections of that work are not derived from the Program, 137 | and can be reasonably considered independent and separate works in 138 | themselves, then this License, and its terms, do not apply to those 139 | sections when you distribute them as separate works. But when you 140 | distribute the same sections as part of a whole which is a work based 141 | on the Program, the distribution of the whole must be on the terms of 142 | this License, whose permissions for other licensees extend to the 143 | entire whole, and thus to each and every part regardless of who wrote it. 144 | 145 | Thus, it is not the intent of this section to claim rights or contest 146 | your rights to work written entirely by you; rather, the intent is to 147 | exercise the right to control the distribution of derivative or 148 | collective works based on the Program. 149 | 150 | In addition, mere aggregation of another work not based on the Program 151 | with the Program (or with a work based on the Program) on a volume of 152 | a storage or distribution medium does not bring the other work under 153 | the scope of this License. 154 | 155 | 3. You may copy and distribute the Program (or a work based on it, 156 | under Section 2) in object code or executable form under the terms of 157 | Sections 1 and 2 above provided that you also do one of the following: 158 | 159 | a) Accompany it with the complete corresponding machine-readable 160 | source code, which must be distributed under the terms of Sections 161 | 1 and 2 above on a medium customarily used for software interchange; or, 162 | 163 | b) Accompany it with a written offer, valid for at least three 164 | years, to give any third party, for a charge no more than your 165 | cost of physically performing source distribution, a complete 166 | machine-readable copy of the corresponding source code, to be 167 | distributed under the terms of Sections 1 and 2 above on a medium 168 | customarily used for software interchange; or, 169 | 170 | c) Accompany it with the information you received as to the offer 171 | to distribute corresponding source code. (This alternative is 172 | allowed only for noncommercial distribution and only if you 173 | received the program in object code or executable form with such 174 | an offer, in accord with Subsection b above.) 175 | 176 | The source code for a work means the preferred form of the work for 177 | making modifications to it. For an executable work, complete source 178 | code means all the source code for all modules it contains, plus any 179 | associated interface definition files, plus the scripts used to 180 | control compilation and installation of the executable. However, as a 181 | special exception, the source code distributed need not include 182 | anything that is normally distributed (in either source or binary 183 | form) with the major components (compiler, kernel, and so on) of the 184 | operating system on which the executable runs, unless that component 185 | itself accompanies the executable. 186 | 187 | If distribution of executable or object code is made by offering 188 | access to copy from a designated place, then offering equivalent 189 | access to copy the source code from the same place counts as 190 | distribution of the source code, even though third parties are not 191 | compelled to copy the source along with the object code. 192 | 193 | 4. You may not copy, modify, sublicense, or distribute the Program 194 | except as expressly provided under this License. Any attempt 195 | otherwise to copy, modify, sublicense or distribute the Program is 196 | void, and will automatically terminate your rights under this License. 197 | However, parties who have received copies, or rights, from you under 198 | this License will not have their licenses terminated so long as such 199 | parties remain in full compliance. 200 | 201 | 5. You are not required to accept this License, since you have not 202 | signed it. However, nothing else grants you permission to modify or 203 | distribute the Program or its derivative works. These actions are 204 | prohibited by law if you do not accept this License. Therefore, by 205 | modifying or distributing the Program (or any work based on the 206 | Program), you indicate your acceptance of this License to do so, and 207 | all its terms and conditions for copying, distributing or modifying 208 | the Program or works based on it. 209 | 210 | 6. Each time you redistribute the Program (or any work based on the 211 | Program), the recipient automatically receives a license from the 212 | original licensor to copy, distribute or modify the Program subject to 213 | these terms and conditions. You may not impose any further 214 | restrictions on the recipients' exercise of the rights granted herein. 215 | You are not responsible for enforcing compliance by third parties to 216 | this License. 217 | 218 | 7. If, as a consequence of a court judgment or allegation of patent 219 | infringement or for any other reason (not limited to patent issues), 220 | conditions are imposed on you (whether by court order, agreement or 221 | otherwise) that contradict the conditions of this License, they do not 222 | excuse you from the conditions of this License. If you cannot 223 | distribute so as to satisfy simultaneously your obligations under this 224 | License and any other pertinent obligations, then as a consequence you 225 | may not distribute the Program at all. For example, if a patent 226 | license would not permit royalty-free redistribution of the Program by 227 | all those who receive copies directly or indirectly through you, then 228 | the only way you could satisfy both it and this License would be to 229 | refrain entirely from distribution of the Program. 230 | 231 | If any portion of this section is held invalid or unenforceable under 232 | any particular circumstance, the balance of the section is intended to 233 | apply and the section as a whole is intended to apply in other 234 | circumstances. 235 | 236 | It is not the purpose of this section to induce you to infringe any 237 | patents or other property right claims or to contest validity of any 238 | such claims; this section has the sole purpose of protecting the 239 | integrity of the free software distribution system, which is 240 | implemented by public license practices. Many people have made 241 | generous contributions to the wide range of software distributed 242 | through that system in reliance on consistent application of that 243 | system; it is up to the author/donor to decide if he or she is willing 244 | to distribute software through any other system and a licensee cannot 245 | impose that choice. 246 | 247 | This section is intended to make thoroughly clear what is believed to 248 | be a consequence of the rest of this License. 249 | 250 | 8. If the distribution and/or use of the Program is restricted in 251 | certain countries either by patents or by copyrighted interfaces, the 252 | original copyright holder who places the Program under this License 253 | may add an explicit geographical distribution limitation excluding 254 | those countries, so that distribution is permitted only in or among 255 | countries not thus excluded. In such case, this License incorporates 256 | the limitation as if written in the body of this License. 257 | 258 | 9. The Free Software Foundation may publish revised and/or new versions 259 | of the General Public License from time to time. Such new versions will 260 | be similar in spirit to the present version, but may differ in detail to 261 | address new problems or concerns. 262 | 263 | Each version is given a distinguishing version number. If the Program 264 | specifies a version number of this License which applies to it and "any 265 | later version", you have the option of following the terms and conditions 266 | either of that version or of any later version published by the Free 267 | Software Foundation. If the Program does not specify a version number of 268 | this License, you may choose any version ever published by the Free Software 269 | Foundation. 270 | 271 | 10. If you wish to incorporate parts of the Program into other free 272 | programs whose distribution conditions are different, write to the author 273 | to ask for permission. For software which is copyrighted by the Free 274 | Software Foundation, write to the Free Software Foundation; we sometimes 275 | make exceptions for this. Our decision will be guided by the two goals 276 | of preserving the free status of all derivatives of our free software and 277 | of promoting the sharing and reuse of software generally. 278 | 279 | NO WARRANTY 280 | 281 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 282 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 283 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 284 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 285 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 286 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 287 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 288 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 289 | REPAIR OR CORRECTION. 290 | 291 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 292 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 293 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 294 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 295 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 296 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 297 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 298 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 299 | POSSIBILITY OF SUCH DAMAGES. 300 | 301 | END OF TERMS AND CONDITIONS 302 | -------------------------------------------------------------------------------- /bpf/bpf_helpers.h: -------------------------------------------------------------------------------- 1 | // This file is taken from libbpf v0.8.2. 2 | // https://github.com/libbpf/libbpf/blob/v0.8.2/src/bpf_helpers.h 3 | // 4 | // Licensed under LGPL 2.1 or the BSD 2 Clause. 5 | 6 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 7 | #ifndef __BPF_HELPERS__ 8 | #define __BPF_HELPERS__ 9 | 10 | /* 11 | * Note that bpf programs need to include either 12 | * vmlinux.h (auto-generated from BTF) or linux/types.h 13 | * in advance since bpf_helper_defs.h uses such types 14 | * as __u64. 15 | */ 16 | #include "bpf_helper_defs.h" 17 | 18 | #define __uint(name, val) int (*name)[val] 19 | #define __type(name, val) typeof(val) *name 20 | #define __array(name, val) typeof(val) *name[] 21 | #define __ulong(name, val) enum { ___bpf_concat(__unique_value, __COUNTER__) = val } name 22 | 23 | /* 24 | * Helper macro to place programs, maps, license in 25 | * different sections in elf_bpf file. Section names 26 | * are interpreted by libbpf depending on the context (BPF programs, BPF maps, 27 | * extern variables, etc). 28 | * To allow use of SEC() with externs (e.g., for extern .maps declarations), 29 | * make sure __attribute__((unused)) doesn't trigger compilation warning. 30 | */ 31 | #if __GNUC__ && !__clang__ 32 | 33 | /* 34 | * Pragma macros are broken on GCC 35 | * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55578 36 | * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90400 37 | */ 38 | #define SEC(name) __attribute__((section(name), used)) 39 | 40 | #else 41 | 42 | #define SEC(name) \ 43 | _Pragma("GCC diagnostic push") \ 44 | _Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \ 45 | __attribute__((section(name), used)) \ 46 | _Pragma("GCC diagnostic pop") \ 47 | 48 | #endif 49 | 50 | /* Avoid 'linux/stddef.h' definition of '__always_inline'. */ 51 | #undef __always_inline 52 | #define __always_inline inline __attribute__((always_inline)) 53 | 54 | #ifndef __noinline 55 | #define __noinline __attribute__((noinline)) 56 | #endif 57 | #ifndef __weak 58 | #define __weak __attribute__((weak)) 59 | #endif 60 | 61 | /* 62 | * Use __hidden attribute to mark a non-static BPF subprogram effectively 63 | * static for BPF verifier's verification algorithm purposes, allowing more 64 | * extensive and permissive BPF verification process, taking into account 65 | * subprogram's caller context. 66 | */ 67 | #define __hidden __attribute__((visibility("hidden"))) 68 | 69 | /* When utilizing vmlinux.h with BPF CO-RE, user BPF programs can't include 70 | * any system-level headers (such as stddef.h, linux/version.h, etc), and 71 | * commonly-used macros like NULL and KERNEL_VERSION aren't available through 72 | * vmlinux.h. This just adds unnecessary hurdles and forces users to re-define 73 | * them on their own. So as a convenience, provide such definitions here. 74 | */ 75 | #ifndef NULL 76 | #define NULL ((void *)0) 77 | #endif 78 | 79 | #ifndef KERNEL_VERSION 80 | #define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c))) 81 | #endif 82 | 83 | /* 84 | * Helper macros to manipulate data structures 85 | */ 86 | 87 | /* offsetof() definition that uses __builtin_offset() might not preserve field 88 | * offset CO-RE relocation properly, so force-redefine offsetof() using 89 | * old-school approach which works with CO-RE correctly 90 | */ 91 | #undef offsetof 92 | #define offsetof(type, member) ((unsigned long)&((type *)0)->member) 93 | 94 | /* redefined container_of() to ensure we use the above offsetof() macro */ 95 | #undef container_of 96 | #define container_of(ptr, type, member) \ 97 | ({ \ 98 | void *__mptr = (void *)(ptr); \ 99 | ((type *)(__mptr - offsetof(type, member))); \ 100 | }) 101 | 102 | /* 103 | * Compiler (optimization) barrier. 104 | */ 105 | #ifndef barrier 106 | #define barrier() asm volatile("" ::: "memory") 107 | #endif 108 | 109 | /* Variable-specific compiler (optimization) barrier. It's a no-op which makes 110 | * compiler believe that there is some black box modification of a given 111 | * variable and thus prevents compiler from making extra assumption about its 112 | * value and potential simplifications and optimizations on this variable. 113 | * 114 | * E.g., compiler might often delay or even omit 32-bit to 64-bit casting of 115 | * a variable, making some code patterns unverifiable. Putting barrier_var() 116 | * in place will ensure that cast is performed before the barrier_var() 117 | * invocation, because compiler has to pessimistically assume that embedded 118 | * asm section might perform some extra operations on that variable. 119 | * 120 | * This is a variable-specific variant of more global barrier(). 121 | */ 122 | #ifndef barrier_var 123 | #define barrier_var(var) asm volatile("" : "+r"(var)) 124 | #endif 125 | 126 | /* 127 | * Helper macro to throw a compilation error if __bpf_unreachable() gets 128 | * built into the resulting code. This works given BPF back end does not 129 | * implement __builtin_trap(). This is useful to assert that certain paths 130 | * of the program code are never used and hence eliminated by the compiler. 131 | * 132 | * For example, consider a switch statement that covers known cases used by 133 | * the program. __bpf_unreachable() can then reside in the default case. If 134 | * the program gets extended such that a case is not covered in the switch 135 | * statement, then it will throw a build error due to the default case not 136 | * being compiled out. 137 | */ 138 | #ifndef __bpf_unreachable 139 | # define __bpf_unreachable() __builtin_trap() 140 | #endif 141 | 142 | /* 143 | * Helper function to perform a tail call with a constant/immediate map slot. 144 | */ 145 | #if __clang_major__ >= 8 && defined(__bpf__) 146 | static __always_inline void 147 | bpf_tail_call_static(void *ctx, const void *map, const __u32 slot) 148 | { 149 | if (!__builtin_constant_p(slot)) 150 | __bpf_unreachable(); 151 | 152 | /* 153 | * Provide a hard guarantee that LLVM won't optimize setting r2 (map 154 | * pointer) and r3 (constant map index) from _different paths_ ending 155 | * up at the _same_ call insn as otherwise we won't be able to use the 156 | * jmpq/nopl retpoline-free patching by the x86-64 JIT in the kernel 157 | * given they mismatch. See also d2e4c1e6c294 ("bpf: Constant map key 158 | * tracking for prog array pokes") for details on verifier tracking. 159 | * 160 | * Note on clobber list: we need to stay in-line with BPF calling 161 | * convention, so even if we don't end up using r0, r4, r5, we need 162 | * to mark them as clobber so that LLVM doesn't end up using them 163 | * before / after the call. 164 | */ 165 | asm volatile("r1 = %[ctx]\n\t" 166 | "r2 = %[map]\n\t" 167 | "r3 = %[slot]\n\t" 168 | "call 12" 169 | :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot) 170 | : "r0", "r1", "r2", "r3", "r4", "r5"); 171 | } 172 | #endif 173 | 174 | enum libbpf_pin_type { 175 | LIBBPF_PIN_NONE, 176 | /* PIN_BY_NAME: pin maps by name (in /sys/fs/bpf by default) */ 177 | LIBBPF_PIN_BY_NAME, 178 | }; 179 | 180 | enum libbpf_tristate { 181 | TRI_NO = 0, 182 | TRI_YES = 1, 183 | TRI_MODULE = 2, 184 | }; 185 | 186 | #define __kconfig __attribute__((section(".kconfig"))) 187 | #define __ksym __attribute__((section(".ksyms"))) 188 | #define __kptr_untrusted __attribute__((btf_type_tag("kptr_untrusted"))) 189 | #define __kptr __attribute__((btf_type_tag("kptr"))) 190 | #define __percpu_kptr __attribute__((btf_type_tag("percpu_kptr"))) 191 | 192 | #define bpf_ksym_exists(sym) ({ \ 193 | _Static_assert(!__builtin_constant_p(!!sym), #sym " should be marked as __weak"); \ 194 | !!sym; \ 195 | }) 196 | 197 | #define __arg_ctx __attribute__((btf_decl_tag("arg:ctx"))) 198 | #define __arg_nonnull __attribute((btf_decl_tag("arg:nonnull"))) 199 | #define __arg_nullable __attribute((btf_decl_tag("arg:nullable"))) 200 | #define __arg_trusted __attribute((btf_decl_tag("arg:trusted"))) 201 | #define __arg_arena __attribute((btf_decl_tag("arg:arena"))) 202 | 203 | #ifndef ___bpf_concat 204 | #define ___bpf_concat(a, b) a ## b 205 | #endif 206 | #ifndef ___bpf_apply 207 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n) 208 | #endif 209 | #ifndef ___bpf_nth 210 | #define ___bpf_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, N, ...) N 211 | #endif 212 | #ifndef ___bpf_narg 213 | #define ___bpf_narg(...) \ 214 | ___bpf_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 215 | #endif 216 | 217 | #define ___bpf_fill0(arr, p, x) do {} while (0) 218 | #define ___bpf_fill1(arr, p, x) arr[p] = x 219 | #define ___bpf_fill2(arr, p, x, args...) arr[p] = x; ___bpf_fill1(arr, p + 1, args) 220 | #define ___bpf_fill3(arr, p, x, args...) arr[p] = x; ___bpf_fill2(arr, p + 1, args) 221 | #define ___bpf_fill4(arr, p, x, args...) arr[p] = x; ___bpf_fill3(arr, p + 1, args) 222 | #define ___bpf_fill5(arr, p, x, args...) arr[p] = x; ___bpf_fill4(arr, p + 1, args) 223 | #define ___bpf_fill6(arr, p, x, args...) arr[p] = x; ___bpf_fill5(arr, p + 1, args) 224 | #define ___bpf_fill7(arr, p, x, args...) arr[p] = x; ___bpf_fill6(arr, p + 1, args) 225 | #define ___bpf_fill8(arr, p, x, args...) arr[p] = x; ___bpf_fill7(arr, p + 1, args) 226 | #define ___bpf_fill9(arr, p, x, args...) arr[p] = x; ___bpf_fill8(arr, p + 1, args) 227 | #define ___bpf_fill10(arr, p, x, args...) arr[p] = x; ___bpf_fill9(arr, p + 1, args) 228 | #define ___bpf_fill11(arr, p, x, args...) arr[p] = x; ___bpf_fill10(arr, p + 1, args) 229 | #define ___bpf_fill12(arr, p, x, args...) arr[p] = x; ___bpf_fill11(arr, p + 1, args) 230 | #define ___bpf_fill(arr, args...) \ 231 | ___bpf_apply(___bpf_fill, ___bpf_narg(args))(arr, 0, args) 232 | 233 | /* 234 | * BPF_SEQ_PRINTF to wrap bpf_seq_printf to-be-printed values 235 | * in a structure. 236 | */ 237 | #define BPF_SEQ_PRINTF(seq, fmt, args...) \ 238 | ({ \ 239 | static const char ___fmt[] = fmt; \ 240 | unsigned long long ___param[___bpf_narg(args)]; \ 241 | \ 242 | _Pragma("GCC diagnostic push") \ 243 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 244 | ___bpf_fill(___param, args); \ 245 | _Pragma("GCC diagnostic pop") \ 246 | \ 247 | bpf_seq_printf(seq, ___fmt, sizeof(___fmt), \ 248 | ___param, sizeof(___param)); \ 249 | }) 250 | 251 | /* 252 | * BPF_SNPRINTF wraps the bpf_snprintf helper with variadic arguments instead of 253 | * an array of u64. 254 | */ 255 | #define BPF_SNPRINTF(out, out_size, fmt, args...) \ 256 | ({ \ 257 | static const char ___fmt[] = fmt; \ 258 | unsigned long long ___param[___bpf_narg(args)]; \ 259 | \ 260 | _Pragma("GCC diagnostic push") \ 261 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 262 | ___bpf_fill(___param, args); \ 263 | _Pragma("GCC diagnostic pop") \ 264 | \ 265 | bpf_snprintf(out, out_size, ___fmt, \ 266 | ___param, sizeof(___param)); \ 267 | }) 268 | 269 | #ifdef BPF_NO_GLOBAL_DATA 270 | #define BPF_PRINTK_FMT_MOD 271 | #else 272 | #define BPF_PRINTK_FMT_MOD static const 273 | #endif 274 | 275 | #define __bpf_printk(fmt, ...) \ 276 | ({ \ 277 | BPF_PRINTK_FMT_MOD char ____fmt[] = fmt; \ 278 | bpf_trace_printk(____fmt, sizeof(____fmt), \ 279 | ##__VA_ARGS__); \ 280 | }) 281 | 282 | /* 283 | * __bpf_vprintk wraps the bpf_trace_vprintk helper with variadic arguments 284 | * instead of an array of u64. 285 | */ 286 | #define __bpf_vprintk(fmt, args...) \ 287 | ({ \ 288 | static const char ___fmt[] = fmt; \ 289 | unsigned long long ___param[___bpf_narg(args)]; \ 290 | \ 291 | _Pragma("GCC diagnostic push") \ 292 | _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ 293 | ___bpf_fill(___param, args); \ 294 | _Pragma("GCC diagnostic pop") \ 295 | \ 296 | bpf_trace_vprintk(___fmt, sizeof(___fmt), \ 297 | ___param, sizeof(___param)); \ 298 | }) 299 | 300 | /* Use __bpf_printk when bpf_printk call has 3 or fewer fmt args 301 | * Otherwise use __bpf_vprintk 302 | */ 303 | #define ___bpf_pick_printk(...) \ 304 | ___bpf_nth(_, ##__VA_ARGS__, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ 305 | __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ 306 | __bpf_vprintk, __bpf_vprintk, __bpf_printk /*3*/, __bpf_printk /*2*/,\ 307 | __bpf_printk /*1*/, __bpf_printk /*0*/) 308 | 309 | /* Helper macro to print out debug messages */ 310 | #define bpf_printk(fmt, args...) ___bpf_pick_printk(args)(fmt, ##args) 311 | 312 | struct bpf_iter_num; 313 | 314 | extern int bpf_iter_num_new(struct bpf_iter_num *it, int start, int end) __weak __ksym; 315 | extern int *bpf_iter_num_next(struct bpf_iter_num *it) __weak __ksym; 316 | extern void bpf_iter_num_destroy(struct bpf_iter_num *it) __weak __ksym; 317 | 318 | #ifndef bpf_for_each 319 | /* bpf_for_each(iter_type, cur_elem, args...) provides generic construct for 320 | * using BPF open-coded iterators without having to write mundane explicit 321 | * low-level loop logic. Instead, it provides for()-like generic construct 322 | * that can be used pretty naturally. E.g., for some hypothetical cgroup 323 | * iterator, you'd write: 324 | * 325 | * struct cgroup *cg, *parent_cg = <...>; 326 | * 327 | * bpf_for_each(cgroup, cg, parent_cg, CG_ITER_CHILDREN) { 328 | * bpf_printk("Child cgroup id = %d", cg->cgroup_id); 329 | * if (cg->cgroup_id == 123) 330 | * break; 331 | * } 332 | * 333 | * I.e., it looks almost like high-level for each loop in other languages, 334 | * supports continue/break, and is verifiable by BPF verifier. 335 | * 336 | * For iterating integers, the difference betwen bpf_for_each(num, i, N, M) 337 | * and bpf_for(i, N, M) is in that bpf_for() provides additional proof to 338 | * verifier that i is in [N, M) range, and in bpf_for_each() case i is `int 339 | * *`, not just `int`. So for integers bpf_for() is more convenient. 340 | * 341 | * Note: this macro relies on C99 feature of allowing to declare variables 342 | * inside for() loop, bound to for() loop lifetime. It also utilizes GCC 343 | * extension: __attribute__((cleanup())), supported by both GCC and 344 | * Clang. 345 | */ 346 | #define bpf_for_each(type, cur, args...) for ( \ 347 | /* initialize and define destructor */ \ 348 | struct bpf_iter_##type ___it __attribute__((aligned(8), /* enforce, just in case */, \ 349 | cleanup(bpf_iter_##type##_destroy))), \ 350 | /* ___p pointer is just to call bpf_iter_##type##_new() *once* to init ___it */ \ 351 | *___p __attribute__((unused)) = ( \ 352 | bpf_iter_##type##_new(&___it, ##args), \ 353 | /* this is a workaround for Clang bug: it currently doesn't emit BTF */ \ 354 | /* for bpf_iter_##type##_destroy() when used from cleanup() attribute */ \ 355 | (void)bpf_iter_##type##_destroy, (void *)0); \ 356 | /* iteration and termination check */ \ 357 | (((cur) = bpf_iter_##type##_next(&___it))); \ 358 | ) 359 | #endif /* bpf_for_each */ 360 | 361 | #ifndef bpf_for 362 | /* bpf_for(i, start, end) implements a for()-like looping construct that sets 363 | * provided integer variable *i* to values starting from *start* through, 364 | * but not including, *end*. It also proves to BPF verifier that *i* belongs 365 | * to range [start, end), so this can be used for accessing arrays without 366 | * extra checks. 367 | * 368 | * Note: *start* and *end* are assumed to be expressions with no side effects 369 | * and whose values do not change throughout bpf_for() loop execution. They do 370 | * not have to be statically known or constant, though. 371 | * 372 | * Note: similarly to bpf_for_each(), it relies on C99 feature of declaring for() 373 | * loop bound variables and cleanup attribute, supported by GCC and Clang. 374 | */ 375 | #define bpf_for(i, start, end) for ( \ 376 | /* initialize and define destructor */ \ 377 | struct bpf_iter_num ___it __attribute__((aligned(8), /* enforce, just in case */ \ 378 | cleanup(bpf_iter_num_destroy))), \ 379 | /* ___p pointer is necessary to call bpf_iter_num_new() *once* to init ___it */ \ 380 | *___p __attribute__((unused)) = ( \ 381 | bpf_iter_num_new(&___it, (start), (end)), \ 382 | /* this is a workaround for Clang bug: it currently doesn't emit BTF */ \ 383 | /* for bpf_iter_num_destroy() when used from cleanup() attribute */ \ 384 | (void)bpf_iter_num_destroy, (void *)0); \ 385 | ({ \ 386 | /* iteration step */ \ 387 | int *___t = bpf_iter_num_next(&___it); \ 388 | /* termination and bounds check */ \ 389 | (___t && ((i) = *___t, (i) >= (start) && (i) < (end))); \ 390 | }); \ 391 | ) 392 | #endif /* bpf_for */ 393 | 394 | #ifndef bpf_repeat 395 | /* bpf_repeat(N) performs N iterations without exposing iteration number 396 | * 397 | * Note: similarly to bpf_for_each(), it relies on C99 feature of declaring for() 398 | * loop bound variables and cleanup attribute, supported by GCC and Clang. 399 | */ 400 | #define bpf_repeat(N) for ( \ 401 | /* initialize and define destructor */ \ 402 | struct bpf_iter_num ___it __attribute__((aligned(8), /* enforce, just in case */ \ 403 | cleanup(bpf_iter_num_destroy))), \ 404 | /* ___p pointer is necessary to call bpf_iter_num_new() *once* to init ___it */ \ 405 | *___p __attribute__((unused)) = ( \ 406 | bpf_iter_num_new(&___it, 0, (N)), \ 407 | /* this is a workaround for Clang bug: it currently doesn't emit BTF */ \ 408 | /* for bpf_iter_num_destroy() when used from cleanup() attribute */ \ 409 | (void)bpf_iter_num_destroy, (void *)0); \ 410 | bpf_iter_num_next(&___it); \ 411 | /* nothing here */ \ 412 | ) 413 | #endif /* bpf_repeat */ 414 | 415 | #endif 416 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022 Coder Technologies, Inc. 2 | 3 | This work is dual-licensed under the MIT or GPL 2.0 (or later) licenses. You may 4 | select, at your option, one of the above-listed licenses. You can find the 5 | license texts in LICENSE.MIT or LICENSE.GPL, or at the bottom of this file. 6 | 7 | Some files have specific licenses, such files will have a specific license 8 | mentioned at the top of the file. 9 | 10 | SPDX-License-Identifier: MIT or GPL-2.0-or-later 11 | 12 | --- 13 | 14 | MIT License 15 | 16 | Copyright (C) 2022 Coder Technologies, Inc. 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | 36 | --- 37 | 38 | GPL 2.0 License 39 | 40 | Copyright (C) 2022 Coder Technologies, Inc. 41 | 42 | This program is free software; you can redistribute it and/or modify 43 | it under the terms of the GNU General Public License as published by 44 | the Free Software Foundation; either version 2 of the License, or 45 | (at your option) any later version. 46 | 47 | This program is distributed in the hope that it will be useful, 48 | but WITHOUT ANY WARRANTY; without even the implied warranty of 49 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 50 | GNU General Public License for more details. 51 | 52 | You should have received a copy of the GNU General Public License along 53 | with this program; if not, write to the Free Software Foundation, Inc., 54 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 55 | 56 | You can contact the copyright holder by using the contact form found at 57 | https://coder.com. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | Version 2, June 1991 61 | 62 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 63 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 64 | Everyone is permitted to copy and distribute verbatim copies 65 | of this license document, but changing it is not allowed. 66 | 67 | Preamble 68 | 69 | The licenses for most software are designed to take away your 70 | freedom to share and change it. By contrast, the GNU General Public 71 | License is intended to guarantee your freedom to share and change free 72 | software--to make sure the software is free for all its users. This 73 | General Public License applies to most of the Free Software 74 | Foundation's software and to any other program whose authors commit to 75 | using it. (Some other Free Software Foundation software is covered by 76 | the GNU Lesser General Public License instead.) You can apply it to 77 | your programs, too. 78 | 79 | When we speak of free software, we are referring to freedom, not 80 | price. Our General Public Licenses are designed to make sure that you 81 | have the freedom to distribute copies of free software (and charge for 82 | this service if you wish), that you receive source code or can get it 83 | if you want it, that you can change the software or use pieces of it 84 | in new free programs; and that you know you can do these things. 85 | 86 | To protect your rights, we need to make restrictions that forbid 87 | anyone to deny you these rights or to ask you to surrender the rights. 88 | These restrictions translate to certain responsibilities for you if you 89 | distribute copies of the software, or if you modify it. 90 | 91 | For example, if you distribute copies of such a program, whether 92 | gratis or for a fee, you must give the recipients all the rights that 93 | you have. You must make sure that they, too, receive or can get the 94 | source code. And you must show them these terms so they know their 95 | rights. 96 | 97 | We protect your rights with two steps: (1) copyright the software, and 98 | (2) offer you this license which gives you legal permission to copy, 99 | distribute and/or modify the software. 100 | 101 | Also, for each author's protection and ours, we want to make certain 102 | that everyone understands that there is no warranty for this free 103 | software. If the software is modified by someone else and passed on, we 104 | want its recipients to know that what they have is not the original, so 105 | that any problems introduced by others will not reflect on the original 106 | authors' reputations. 107 | 108 | Finally, any free program is threatened constantly by software 109 | patents. We wish to avoid the danger that redistributors of a free 110 | program will individually obtain patent licenses, in effect making the 111 | program proprietary. To prevent this, we have made it clear that any 112 | patent must be licensed for everyone's free use or not licensed at all. 113 | 114 | The precise terms and conditions for copying, distribution and 115 | modification follow. 116 | 117 | GNU GENERAL PUBLIC LICENSE 118 | 119 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 120 | 121 | 0. This License applies to any program or other work which contains 122 | a notice placed by the copyright holder saying it may be distributed 123 | under the terms of this General Public License. The "Program", below, 124 | refers to any such program or work, and a "work based on the Program" 125 | means either the Program or any derivative work under copyright law: 126 | that is to say, a work containing the Program or a portion of it, 127 | either verbatim or with modifications and/or translated into another 128 | language. (Hereinafter, translation is included without limitation in 129 | the term "modification".) Each licensee is addressed as "you". 130 | 131 | Activities other than copying, distribution and modification are not 132 | covered by this License; they are outside its scope. The act of 133 | running the Program is not restricted, and the output from the Program 134 | is covered only if its contents constitute a work based on the 135 | Program (independent of having been made by running the Program). 136 | Whether that is true depends on what the Program does. 137 | 138 | 1. You may copy and distribute verbatim copies of the Program's 139 | source code as you receive it, in any medium, provided that you 140 | conspicuously and appropriately publish on each copy an appropriate 141 | copyright notice and disclaimer of warranty; keep intact all the 142 | notices that refer to this License and to the absence of any warranty; 143 | and give any other recipients of the Program a copy of this License 144 | along with the Program. 145 | 146 | You may charge a fee for the physical act of transferring a copy, and 147 | you may at your option offer warranty protection in exchange for a fee. 148 | 149 | 2. You may modify your copy or copies of the Program or any portion 150 | of it, thus forming a work based on the Program, and copy and 151 | distribute such modifications or work under the terms of Section 1 152 | above, provided that you also meet all of these conditions: 153 | 154 | 155 | a) You must cause the modified files to carry prominent notices 156 | stating that you changed the files and the date of any change. 157 | 158 | b) You must cause any work that you distribute or publish, that in 159 | whole or in part contains or is derived from the Program or any 160 | part thereof, to be licensed as a whole at no charge to all third 161 | parties under the terms of this License. 162 | 163 | c) If the modified program normally reads commands interactively 164 | when run, you must cause it, when started running for such 165 | interactive use in the most ordinary way, to print or display an 166 | announcement including an appropriate copyright notice and a 167 | notice that there is no warranty (or else, saying that you provide 168 | a warranty) and that users may redistribute the program under 169 | these conditions, and telling the user how to view a copy of this 170 | License. (Exception: if the Program itself is interactive but 171 | does not normally print such an announcement, your work based on 172 | the Program is not required to print an announcement.) 173 | 174 | These requirements apply to the modified work as a whole. If 175 | identifiable sections of that work are not derived from the Program, 176 | and can be reasonably considered independent and separate works in 177 | themselves, then this License, and its terms, do not apply to those 178 | sections when you distribute them as separate works. But when you 179 | distribute the same sections as part of a whole which is a work based 180 | on the Program, the distribution of the whole must be on the terms of 181 | this License, whose permissions for other licensees extend to the 182 | entire whole, and thus to each and every part regardless of who wrote it. 183 | 184 | Thus, it is not the intent of this section to claim rights or contest 185 | your rights to work written entirely by you; rather, the intent is to 186 | exercise the right to control the distribution of derivative or 187 | collective works based on the Program. 188 | 189 | In addition, mere aggregation of another work not based on the Program 190 | with the Program (or with a work based on the Program) on a volume of 191 | a storage or distribution medium does not bring the other work under 192 | the scope of this License. 193 | 194 | 3. You may copy and distribute the Program (or a work based on it, 195 | under Section 2) in object code or executable form under the terms of 196 | Sections 1 and 2 above provided that you also do one of the following: 197 | 198 | 199 | a) Accompany it with the complete corresponding machine-readable 200 | source code, which must be distributed under the terms of Sections 201 | 1 and 2 above on a medium customarily used for software interchange; or, 202 | 203 | b) Accompany it with a written offer, valid for at least three 204 | years, to give any third party, for a charge no more than your 205 | cost of physically performing source distribution, a complete 206 | machine-readable copy of the corresponding source code, to be 207 | distributed under the terms of Sections 1 and 2 above on a medium 208 | customarily used for software interchange; or, 209 | 210 | c) Accompany it with the information you received as to the offer 211 | to distribute corresponding source code. (This alternative is 212 | allowed only for noncommercial distribution and only if you 213 | received the program in object code or executable form with such 214 | an offer, in accord with Subsection b above.) 215 | 216 | The source code for a work means the preferred form of the work for 217 | making modifications to it. For an executable work, complete source 218 | code means all the source code for all modules it contains, plus any 219 | associated interface definition files, plus the scripts used to 220 | control compilation and installation of the executable. However, as a 221 | special exception, the source code distributed need not include 222 | anything that is normally distributed (in either source or binary 223 | form) with the major components (compiler, kernel, and so on) of the 224 | operating system on which the executable runs, unless that component 225 | itself accompanies the executable. 226 | 227 | If distribution of executable or object code is made by offering 228 | access to copy from a designated place, then offering equivalent 229 | access to copy the source code from the same place counts as 230 | distribution of the source code, even though third parties are not 231 | compelled to copy the source along with the object code. 232 | 233 | 4. You may not copy, modify, sublicense, or distribute the Program 234 | except as expressly provided under this License. Any attempt 235 | otherwise to copy, modify, sublicense or distribute the Program is 236 | void, and will automatically terminate your rights under this License. 237 | However, parties who have received copies, or rights, from you under 238 | this License will not have their licenses terminated so long as such 239 | parties remain in full compliance. 240 | 241 | 5. You are not required to accept this License, since you have not 242 | signed it. However, nothing else grants you permission to modify or 243 | distribute the Program or its derivative works. These actions are 244 | prohibited by law if you do not accept this License. Therefore, by 245 | modifying or distributing the Program (or any work based on the 246 | Program), you indicate your acceptance of this License to do so, and 247 | all its terms and conditions for copying, distributing or modifying 248 | the Program or works based on it. 249 | 250 | 6. Each time you redistribute the Program (or any work based on the 251 | Program), the recipient automatically receives a license from the 252 | original licensor to copy, distribute or modify the Program subject to 253 | these terms and conditions. You may not impose any further 254 | restrictions on the recipients' exercise of the rights granted herein. 255 | You are not responsible for enforcing compliance by third parties to 256 | this License. 257 | 258 | 7. If, as a consequence of a court judgment or allegation of patent 259 | infringement or for any other reason (not limited to patent issues), 260 | conditions are imposed on you (whether by court order, agreement or 261 | otherwise) that contradict the conditions of this License, they do not 262 | excuse you from the conditions of this License. If you cannot 263 | distribute so as to satisfy simultaneously your obligations under this 264 | License and any other pertinent obligations, then as a consequence you 265 | may not distribute the Program at all. For example, if a patent 266 | license would not permit royalty-free redistribution of the Program by 267 | all those who receive copies directly or indirectly through you, then 268 | the only way you could satisfy both it and this License would be to 269 | refrain entirely from distribution of the Program. 270 | 271 | If any portion of this section is held invalid or unenforceable under 272 | any particular circumstance, the balance of the section is intended to 273 | apply and the section as a whole is intended to apply in other 274 | circumstances. 275 | 276 | It is not the purpose of this section to induce you to infringe any 277 | patents or other property right claims or to contest validity of any 278 | such claims; this section has the sole purpose of protecting the 279 | integrity of the free software distribution system, which is 280 | implemented by public license practices. Many people have made 281 | generous contributions to the wide range of software distributed 282 | through that system in reliance on consistent application of that 283 | system; it is up to the author/donor to decide if he or she is willing 284 | to distribute software through any other system and a licensee cannot 285 | impose that choice. 286 | 287 | This section is intended to make thoroughly clear what is believed to 288 | be a consequence of the rest of this License. 289 | 290 | 8. If the distribution and/or use of the Program is restricted in 291 | certain countries either by patents or by copyrighted interfaces, the 292 | original copyright holder who places the Program under this License 293 | may add an explicit geographical distribution limitation excluding 294 | those countries, so that distribution is permitted only in or among 295 | countries not thus excluded. In such case, this License incorporates 296 | the limitation as if written in the body of this License. 297 | 298 | 9. The Free Software Foundation may publish revised and/or new versions 299 | of the General Public License from time to time. Such new versions will 300 | be similar in spirit to the present version, but may differ in detail to 301 | address new problems or concerns. 302 | 303 | Each version is given a distinguishing version number. If the Program 304 | specifies a version number of this License which applies to it and "any 305 | later version", you have the option of following the terms and conditions 306 | either of that version or of any later version published by the Free 307 | Software Foundation. If the Program does not specify a version number of 308 | this License, you may choose any version ever published by the Free Software 309 | Foundation. 310 | 311 | 10. If you wish to incorporate parts of the Program into other free 312 | programs whose distribution conditions are different, write to the author 313 | to ask for permission. For software which is copyrighted by the Free 314 | Software Foundation, write to the Free Software Foundation; we sometimes 315 | make exceptions for this. Our decision will be guided by the two goals 316 | of preserving the free status of all derivatives of our free software and 317 | of promoting the sharing and reuse of software generally. 318 | 319 | NO WARRANTY 320 | 321 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 322 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 323 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 324 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 325 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 326 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 327 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 328 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 329 | REPAIR OR CORRECTION. 330 | 331 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 332 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 333 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 334 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 335 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 336 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 337 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 338 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 339 | POSSIBILITY OF SUCH DAMAGES. 340 | 341 | END OF TERMS AND CONDITIONS 342 | --------------------------------------------------------------------------------