| 1 | package attachment |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "sync" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestAttachmentParentSymlinkCannotEscape(t *testing.T) { |
| 12 | root, outside := t.TempDir(), t.TempDir() |
| 13 | if err := os.MkdirAll(filepath.Join(root, ".reasonix"), 0700); err != nil { |
| 14 | t.Fatal(err) |
| 15 | } |
| 16 | if err := os.WriteFile(filepath.Join(outside, "secret.png"), opaquePNG(t, 8, 8), 0600); err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | if err := os.Symlink(outside, filepath.Join(root, ".reasonix", "attachments")); err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | _, err := testService(t).PrepareBatch(t.Context(), []Source{{WorkspaceRoot: root, Path: ".reasonix/attachments/secret.png", Confine: ".reasonix/attachments"}}) |
| 23 | if err == nil { |
| 24 | t.Fatal("accepted image outside workspace through parent symlink") |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // Done is first consulted by share after it has registered this waiter. |
| 29 | type joinedVariantContext struct { |
| 30 | context.Context |
| 31 | joined chan struct{} |
| 32 | once sync.Once |
| 33 | } |
| 34 | |
| 35 | func (c *joinedVariantContext) Done() <-chan struct{} { |
| 36 | c.once.Do(func() { close(c.joined) }) |
| 37 | return c.Context.Done() |
| 38 | } |
| 39 | |
| 40 | func TestVariantWaitersCancelIndependently(t *testing.T) { |
| 41 | cache := NewVariantCache(DefaultCacheBytes, 2) |
| 42 | key := variantKey{digest: "independent"} |
| 43 | started, release := make(chan struct{}), make(chan struct{}) |
| 44 | ctx, cancel := context.WithCancel(t.Context()) |
| 45 | first := make(chan error, 1) |
| 46 | go func() { |
| 47 | _, err := cache.share(ctx, key, func(work context.Context) (Variant, error) { |
| 48 | close(started) |
| 49 | <-release |
| 50 | return Variant{Bytes: []byte("shared")}, work.Err() |
| 51 | }) |
| 52 | first <- err |
| 53 | }() |
| 54 | <-started |
| 55 | secondCtx := &joinedVariantContext{Context: t.Context(), joined: make(chan struct{})} |
| 56 | second := make(chan error, 1) |
| 57 | go func() { |
| 58 | value, err := cache.share(secondCtx, key, func(context.Context) (Variant, error) { |
| 59 | t.Error("second waiter started a second transform") |
| 60 | return Variant{}, nil |
| 61 | }) |
| 62 | if err == nil && string(value.Bytes) != "shared" { |
| 63 | t.Error("wrong shared result") |
| 64 | } |
| 65 | second <- err |
| 66 | }() |
| 67 | <-secondCtx.joined |
| 68 | cancel() |
| 69 | if err := <-first; !Is(err, CodeCanceled) { |
| 70 | t.Fatalf("first waiter = %v", err) |
| 71 | } |
| 72 | close(release) |
| 73 | if err := <-second; err != nil { |
| 74 | t.Fatalf("second waiter canceled by first: %v", err) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestCanceledVariantCannotCaptureSuccessor(t *testing.T) { |
| 79 | cache := NewVariantCache(DefaultCacheBytes, 2) |
| 80 | key := variantKey{digest: "test"} |
| 81 | started, finish, returned := make(chan struct{}), make(chan struct{}), make(chan struct{}) |
| 82 | ctx, cancel := context.WithCancel(t.Context()) |
| 83 | go func() { |
| 84 | defer close(returned) |
| 85 | _, _ = cache.share(ctx, key, func(workCtx context.Context) (Variant, error) { |
| 86 | close(started) |
| 87 | <-finish |
| 88 | return Variant{}, workCtx.Err() |
| 89 | }) |
| 90 | }() |
| 91 | <-started |
| 92 | cancel() |
| 93 | <-returned |
| 94 | defer close(finish) |
| 95 | result, err := cache.share(t.Context(), key, func(context.Context) (Variant, error) { |
| 96 | return Variant{Bytes: []byte("successor")}, nil |
| 97 | }) |
| 98 | if err != nil || string(result.Bytes) != "successor" { |
| 99 | t.Fatalf("successor result: %q, %v", result.Bytes, err) |
| 100 | } |
| 101 | } |
| 102 |