Skip to content

Commit ca2183c

Browse files
authored
Merge pull request #1219 from buildkite/pdp-1621-replace-bash-with-go
Replace Bash fix-permissions script with Go
2 parents f9ab0e0 + b1f5145 commit ca2183c

File tree

22 files changed

+533
-280
lines changed

22 files changed

+533
-280
lines changed

.buildkite/docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
version: '3'
2+
3+
services:
4+
fixperms-tests:
5+
image: golang:latest
6+
working_dir: /code
7+
volumes:
8+
- ..:/code:ro
9+
command: go test -v ./...
10+
11+
fixperms-build:
12+
image: golang:latest
13+
working_dir: /code
14+
volumes:
15+
- ..:/code
16+
- /var/lib/buildkite-agent/git-mirrors:/var/lib/buildkite-agent/git-mirrors
17+
command: .buildkite/steps/build-fixperms.sh

.buildkite/pipeline.yaml

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,28 @@ steps:
77
agents:
88
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
99

10-
- id: "bats-tests"
11-
name: ":bash: Unit tests"
10+
- id: "fixperms-tests"
11+
name: ":go: fixperms tests"
1212
agents:
1313
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
1414
plugins:
15-
docker-compose#v2.1.0:
16-
run: unit-tests
17-
config: docker-compose.unit-tests.yml
15+
- docker-compose#v2.1.0:
16+
run: fixperms-tests
17+
config: .buildkite/docker-compose.yml
18+
19+
- id: "fixperms-build"
20+
name: ":go: fixperms build"
21+
agents:
22+
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
23+
depends_on:
24+
- "fixperms-tests"
25+
artifact_paths: "build/fix-perms-*"
26+
plugins:
27+
- docker-compose#v2.1.0:
28+
run: fixperms-build
29+
config: .buildkite/docker-compose.yml
30+
- artifacts#v1.9.0:
31+
upload: "builds/fix-perms-*"
1832

1933
- id: "deploy-service-role-stack"
2034
name: ":aws-iam: :cloudformation:"
@@ -23,7 +37,8 @@ steps:
2337
command: .buildkite/steps/deploy-service-role-stack.sh
2438
depends_on:
2539
- "lint"
26-
- "bats-tests"
40+
- "fixperms-tests"
41+
- "fixperms-build"
2742

2843
- id: "packer-windows-amd64"
2944
name: ":packer: :windows:"
@@ -34,7 +49,8 @@ steps:
3449
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
3550
depends_on:
3651
- "lint"
37-
- "bats-tests"
52+
- "fixperms-tests"
53+
- "fixperms-build"
3854

3955
- id: "launch-windows-amd64"
4056
name: ":cloudformation: :windows: AMD64 Launch"
@@ -77,7 +93,8 @@ steps:
7793
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
7894
depends_on:
7995
- "lint"
80-
- "bats-tests"
96+
- "fixperms-tests"
97+
- "fixperms-build"
8198

8299
- id: "launch-linux-amd64"
83100
name: ":cloudformation: :linux: AMD64 Launch"
@@ -119,7 +136,8 @@ steps:
119136
queue: "${BUILDKITE_AGENT_META_DATA_QUEUE}"
120137
depends_on:
121138
- "lint"
122-
- "bats-tests"
139+
- "fixperms-tests"
140+
- "fixperms-build"
123141

124142
- id: "launch-linux-arm64"
125143
name: ":cloudformation: :linux: ARM64 Launch"

.buildkite/steps/build-fixperms.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
for arch in amd64 arm64; do
4+
GOOS=linux GOARCH="${arch}" go build -v -o "build/fix-perms-linux-${arch}" ./internal/fixperms
5+
done

.buildkite/steps/packer.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ fi
1616

1717
mkdir -p "build/"
1818

19+
if [[ "$os" == "linux" ]] ; then
20+
buildkite-agent artifact download "build/fix-perms-linux-${arch}" ./build
21+
mv "build/fix-perms-linux-${arch}" packer/linux/conf/buildkite-agent/scripts/fix-buildkite-agent-builds-permissions
22+
chmod 755 packer/linux/conf/buildkite-agent/scripts/fix-buildkite-agent-builds-permissions
23+
fi
24+
1925
# Build a hash of packer files and the agent versions
2026
packer_files_sha=$(find Makefile "packer/${os}" plugins/ -type f -print0 | xargs -0 sha1sum | awk '{print $1}' | sort | sha1sum | awk '{print $1}')
2127
stable_agent_sha=$(curl -Lfs "https://download.buildkite.com/agent/stable/latest/${agent_binary}.sha256")

docker-compose.unit-tests.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/buildkite/elastic-ci-stack-for-aws/v6
2+
3+
go 1.20
4+
5+
require (
6+
github.com/google/go-cmp v0.5.9
7+
golang.org/x/sys v0.12.0
8+
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
2+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
4+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/fixperms/fdfs/fdfs.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//go:build linux
2+
3+
// Package fdfs is like os.DirFS, but with a file descriptor and openat(2),
4+
// fchownat(2), etc, to ensure symlinks do not escape.
5+
package fdfs
6+
7+
import (
8+
"fmt"
9+
"io/fs"
10+
"os"
11+
12+
"golang.org/x/sys/unix"
13+
)
14+
15+
const resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV
16+
17+
// FS uses a file descriptor for a directory as the base of a fs.FS.
18+
type FS struct {
19+
file *os.File
20+
}
21+
22+
// DirFS opens the directory dir, and returns an FS rooted at that directory.
23+
// It uses open(2) with O_RDONLY+O_DIRECTORY+O_CLOEXEC. Note that this will
24+
// resolve symlinks in the path, so only use this to open a trusted base path.
25+
func DirFS(dir string) (*FS, error) {
26+
f, err := os.OpenFile(dir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
27+
if err != nil {
28+
return nil, err
29+
}
30+
return &FS{file: f}, nil
31+
}
32+
33+
// Close closes the file descriptor.
34+
func (s *FS) Close() error {
35+
return s.file.Close()
36+
}
37+
38+
// Open wraps openat2(2) with O_RDONLY+O_NOFOLLOW+O_CLOEXEC, and prohibits
39+
// symlinks etc within the path.
40+
func (s *FS) Open(path string) (fs.File, error) {
41+
fd, err := unix.Openat2(int(s.file.Fd()), path, &unix.OpenHow{
42+
Flags: unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC,
43+
Mode: 0,
44+
Resolve: resolveFlags,
45+
})
46+
if err != nil {
47+
return nil, fmt.Errorf("openat2(%d, %q): %w", s.file.Fd(), path, err)
48+
}
49+
return os.NewFile(uintptr(fd), path), nil
50+
}
51+
52+
// Lchown wraps fchownat(2) (with AT_SYMLINK_NOFOLLOW).
53+
func (s *FS) Lchown(path string, uid, gid int) error {
54+
if err := unix.Fchownat(int(s.file.Fd()), path, uid, gid, unix.AT_SYMLINK_NOFOLLOW); err != nil {
55+
return fmt.Errorf("fchownat(%d, %q, %d, %d): %w", s.file.Fd(), path, uid, gid, err)
56+
}
57+
return nil
58+
}
59+
60+
// Sub wraps openat2(2) (with O_RDONLY+O_DIRECTORY+O_NOFOLLOW+O_CLOEXEC), and
61+
// returns an FS.
62+
func (s *FS) Sub(dir string) (*FS, error) {
63+
fd, err := unix.Openat2(int(s.file.Fd()), dir, &unix.OpenHow{
64+
Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NOFOLLOW | unix.O_CLOEXEC,
65+
Mode: 0,
66+
Resolve: resolveFlags,
67+
})
68+
if err != nil {
69+
return nil, fmt.Errorf("openat2(%d, %q): %w", s.file.Fd(), dir, err)
70+
}
71+
return &FS{os.NewFile(uintptr(fd), dir)}, nil
72+
}
73+
74+
// RecursiveChown lchowns everything within the receiver.
75+
func (s *FS) RecursiveChown(uid, gid int) error {
76+
// Q: Why not fs.WalkDir(... s.Lchown(path, uid, gid) ... ) ?
77+
// A: fs.WalkDir gives the callback a subpath to each item. So although
78+
// fs.WalkDir doesn't traverse symlinks, there's a race between walking
79+
// each path (no intermediate symlinks), and passing that path to lchown
80+
// (has possibly changed).
81+
// Solution: More openat.
82+
83+
if err := s.Lchown(".", uid, gid); err != nil {
84+
return err
85+
}
86+
87+
// This closure exists so sd.Close happens before the next loop iteration,
88+
// rather than at the end of RecursiveChown.
89+
chownSubdir := func(name string) error {
90+
sd, err := s.Sub(name)
91+
if err != nil {
92+
return err
93+
}
94+
defer sd.Close()
95+
return sd.RecursiveChown(uid, gid)
96+
}
97+
98+
// The "file" within an *FS should always be a directory.
99+
ds, err := s.file.ReadDir(-1)
100+
if err != nil {
101+
return err
102+
}
103+
for _, d := range ds {
104+
if !d.IsDir() {
105+
if err := s.Lchown(d.Name(), uid, gid); err != nil {
106+
return err
107+
}
108+
continue
109+
}
110+
111+
// Defensively check we're not about to recurse on a symlink.
112+
// (The openat2 call in s.Sub will block it anyway.)
113+
if d.Type()&fs.ModeSymlink != 0 {
114+
continue
115+
}
116+
117+
if err := chownSubdir(d.Name()); err != nil {
118+
return err
119+
}
120+
}
121+
return nil
122+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//go:build linux
2+
3+
package fdfs
4+
5+
import (
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
)
11+
12+
func TestTOCTOUShenanigans(t *testing.T) {
13+
path := "/tmp/TestTOCTOUShenanigans/foo"
14+
if err := os.MkdirAll(path, 0o777); err != nil {
15+
t.Fatalf("os.MkdirAll(%s, %o) = %v", path, 0o777, err)
16+
}
17+
fp := filepath.Join(path, "data")
18+
if err := os.WriteFile(fp, []byte("innocent"), 0o666); err != nil {
19+
t.Fatalf("os.WriteFile(%s, nil, 0o666) = %v", fp, err)
20+
}
21+
22+
path2 := "/tmp/TestTOCTOUShenanigans/crimes"
23+
if err := os.MkdirAll(path2, 0o777); err != nil {
24+
t.Fatalf("os.MkdirAll(%s, %o) = %v", path2, 0o777, err)
25+
}
26+
fp2 := filepath.Join(path2, "data")
27+
if err := os.WriteFile(fp2, []byte("guilty"), 0o666); err != nil {
28+
t.Fatalf("os.WriteFile(%s, nil, 0o666) = %v", fp2, err)
29+
}
30+
31+
// Do it in two steps, to simulate a trusted directory and an untrusted
32+
// subpath.
33+
fsys, err := DirFS("/tmp/TestTOCTOUShenanigans")
34+
if err != nil {
35+
t.Fatalf("DirFS(/tmp/TestTOCTOUShenanigans) error = %v", err)
36+
}
37+
defer fsys.Close()
38+
fooFS, err := fsys.Sub("foo")
39+
if err != nil {
40+
t.Fatalf("DirFS(/tmp/TestTOCTOUShenanigans).Sub(foo) error = %v", err)
41+
}
42+
defer fooFS.Close()
43+
44+
// Replace foo with a symlink to crimes...
45+
path3 := "/tmp/TestTOCTOUShenanigans/foo.bak"
46+
if err := os.Rename(path, path3); err != nil {
47+
t.Fatalf("os.Rename(%s, %s) = %v", path, path3, err)
48+
}
49+
if err := os.Symlink(path2, path); err != nil {
50+
t.Fatalf("os.Symlink(%s, %s) = %v", path2, path, err)
51+
}
52+
53+
// What do we get?
54+
df, err := fs.ReadFile(fooFS, "data")
55+
if err != nil {
56+
t.Fatalf("fs.ReadFile(DirFS(%s), data) error = %v", path, err)
57+
}
58+
if got, want := string(df), "innocent"; got != want {
59+
t.Fatalf("fs.ReadFile(DirFS(%s), data) contents = %q, want %q", path, got, want)
60+
}
61+
}

0 commit comments

Comments
 (0)