Skip to content

Commit 1dc233f

Browse files
author
shiftstack-merge-bot
committed
2 parents 0a1ab4b + b52a216 commit 1dc233f

83 files changed

Lines changed: 2312 additions & 598 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848

4949
# Initializes the CodeQL tools for scanning.
5050
- name: Initialize CodeQL
51-
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
51+
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
5252
with:
5353
languages: ${{ matrix.language }}
5454
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,4 +63,4 @@ jobs:
6363
make all
6464
6565
- name: Perform CodeQL Analysis
66-
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
66+
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4

.github/workflows/trivy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Install go
1717
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
1818
with:
19-
go-version: 1.25.9
19+
go-version: 1.25.10
2020

2121
- name: Build an image from Dockerfile
2222
run: |

go.mod

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/pborman/uuid v1.2.1
1111
github.com/stretchr/testify v1.11.1
1212
go.uber.org/goleak v1.3.0
13-
google.golang.org/grpc v1.80.0
13+
google.golang.org/grpc v1.81.1
1414
google.golang.org/protobuf v1.36.11
1515
k8s.io/api v0.32.10
1616
k8s.io/apimachinery v0.32.10
@@ -118,28 +118,28 @@ require (
118118
go.etcd.io/etcd/client/v3 v3.5.16 // indirect
119119
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
120120
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
121-
go.opentelemetry.io/otel v1.40.0 // indirect
121+
go.opentelemetry.io/otel v1.43.0 // indirect
122122
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
123123
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
124-
go.opentelemetry.io/otel/metric v1.40.0 // indirect
125-
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
126-
go.opentelemetry.io/otel/trace v1.40.0 // indirect
124+
go.opentelemetry.io/otel/metric v1.43.0 // indirect
125+
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
126+
go.opentelemetry.io/otel/trace v1.43.0 // indirect
127127
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
128128
go.uber.org/multierr v1.11.0 // indirect
129129
go.uber.org/zap v1.27.0 // indirect
130130
golang.org/x/crypto v0.50.0 // indirect
131131
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
132-
golang.org/x/mod v0.35.0
133-
golang.org/x/oauth2 v0.34.0 // indirect
132+
golang.org/x/mod v0.36.0
133+
golang.org/x/oauth2 v0.36.0 // indirect
134134
golang.org/x/sync v0.20.0 // indirect
135135
golang.org/x/sys v0.43.0 // indirect
136136
golang.org/x/term v0.42.0 // indirect
137137
golang.org/x/text v0.36.0 // indirect
138138
golang.org/x/time v0.7.0 // indirect
139139
golang.org/x/tools v0.44.0 // indirect
140140
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
141-
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
142-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
141+
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
142+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
143143
gopkg.in/inf.v0 v0.9.1 // indirect
144144
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
145145
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -435,20 +435,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
435435
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
436436
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
437437
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
438-
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
439-
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
438+
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
439+
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
440440
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
441441
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
442442
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
443443
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
444-
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
445-
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
446-
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
447-
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
448-
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
449-
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
450-
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
451-
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
444+
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
445+
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
446+
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
447+
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
448+
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
449+
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
450+
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
451+
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
452452
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
453453
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
454454
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -496,8 +496,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
496496
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
497497
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
498498
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
499-
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
500-
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
499+
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
500+
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
501501
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
502502
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
503503
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -522,8 +522,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
522522
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
523523
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
524524
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
525-
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
526-
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
525+
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
526+
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
527527
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
528528
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
529529
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -623,10 +623,10 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx
623623
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
624624
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
625625
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
626-
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
627-
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
628-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
629-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
626+
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
627+
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
628+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
629+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
630630
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
631631
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
632632
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -635,8 +635,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
635635
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
636636
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
637637
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
638-
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
639-
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
638+
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
639+
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
640640
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
641641
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
642642
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

pkg/nfs/tar.go

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"io/fs"
2626
"os"
2727
"path/filepath"
28+
"sort"
2829
"strings"
30+
"time"
2931
)
3032

3133
func TarPack(srcDirPath string, dstPath string, enableCompression bool) error {
@@ -142,6 +144,17 @@ func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
142144
if err != nil {
143145
return fmt.Errorf("normalizing archive destination path: %w", err)
144146
}
147+
// Ensure destination exists before resolving, so EvalSymlinks works on all platforms.
148+
if mkErr := os.MkdirAll(dstDirPath, 0755); mkErr != nil {
149+
return fmt.Errorf("creating destination directory: %w", mkErr)
150+
}
151+
// Resolve symlinks in dstDirPath itself so containment checks work correctly
152+
// on platforms where temp paths are symlinks (e.g., macOS /tmp -> /private/tmp)
153+
// or short path names (e.g., Windows RUNNER~1 -> runneradmin).
154+
dstDirPath, err = filepath.EvalSymlinks(dstDirPath)
155+
if err != nil {
156+
return fmt.Errorf("resolving destination path: %w", err)
157+
}
145158

146159
tarFile, err := os.Open(srcPath)
147160
if err != nil {
@@ -167,6 +180,15 @@ func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
167180

168181
tarReader := tar.NewReader(tarDst)
169182

183+
// Collect directory timestamps to restore after all files are written,
184+
// because creating files inside a directory updates the directory's mtime.
185+
type dirTimestamp struct {
186+
path string
187+
modTime time.Time
188+
accTime time.Time
189+
}
190+
var dirTimestamps []dirTimestamp
191+
170192
for {
171193
var tarHeader *tar.Header
172194
tarHeader, err = tarReader.Next()
@@ -181,13 +203,38 @@ func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
181203

182204
filePath := filepath.Join(dstDirPath, tarHeader.Name)
183205

184-
// protect against "Zip Slip"
185-
if !strings.HasPrefix(filePath, dstDirPath) {
186-
// mimic standard error, which will be returned in future versions of Go by default
187-
// more info can be found by "tarinsecurepath" variable name
206+
// Robust containment check: use filepath.Rel to prevent prefix collisions
207+
// (e.g., dstDirPath="/tmp/out" vs filePath="/tmp/out2/...")
208+
var rel string
209+
rel, err = filepath.Rel(dstDirPath, filePath)
210+
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || filepath.IsAbs(rel) {
188211
return tar.ErrInsecurePath
189212
}
190213

214+
// Symlink-traversal guard: verify that the real (resolved) parent directory
215+
// of filePath is still within dstDirPath. This catches cases where a prior
216+
// symlink entry created a link inside dstDirPath that points outside, and a
217+
// subsequent entry tries to write through that symlink (e.g., symlink "a" -> /etc
218+
// followed by entry "a/passwd").
219+
// For directories, check filepath.Dir(filePath) — the existing parent — so that
220+
// a symlink "a" followed by "a/b" is caught before MkdirAll creates "b" outside.
221+
parentDir := filepath.Dir(filePath)
222+
if parentDir != dstDirPath && filePath != dstDirPath {
223+
// Walk up to the nearest existing ancestor to handle cases where
224+
// parentDir doesn't exist yet but an ancestor symlink escapes.
225+
checkDir := parentDir
226+
for checkDir != dstDirPath {
227+
if realDir, evalErr := filepath.EvalSymlinks(checkDir); evalErr == nil {
228+
realDirRel, relErr := filepath.Rel(dstDirPath, realDir)
229+
if relErr != nil || realDirRel == ".." || strings.HasPrefix(realDirRel, ".."+string(os.PathSeparator)) || filepath.IsAbs(realDirRel) {
230+
return tar.ErrInsecurePath
231+
}
232+
break
233+
}
234+
checkDir = filepath.Dir(checkDir)
235+
}
236+
}
237+
191238
fileDirPath := filePath
192239
if !fileInfo.Mode().IsDir() {
193240
fileDirPath = filepath.Dir(fileDirPath)
@@ -198,6 +245,21 @@ func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
198245
}
199246

200247
if fileInfo.Mode().IsDir() {
248+
// Remove any existing symlink at filePath to prevent MkdirAll
249+
// and later Chtimes from following it outside dstDirPath.
250+
if existing, lErr := os.Lstat(filePath); lErr == nil && existing.Mode()&fs.ModeSymlink != 0 {
251+
if err = os.Remove(filePath); err != nil {
252+
return fmt.Errorf("removing symlink before mkdir %s: %w", filePath, err)
253+
}
254+
if err = os.MkdirAll(filePath, 0755); err != nil {
255+
return fmt.Errorf("making directory %s: %w", filePath, err)
256+
}
257+
}
258+
dirTimestamps = append(dirTimestamps, dirTimestamp{
259+
path: filePath,
260+
modTime: tarHeader.ModTime,
261+
accTime: tarHeader.AccessTime,
262+
})
201263
continue
202264
}
203265

@@ -208,21 +270,73 @@ func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
208270
continue
209271
}
210272

211-
if err = tarUnpackFile(filePath, tarReader, fileInfo); err != nil {
273+
// Remove any existing symlink at filePath to prevent following it
274+
// when writing a regular file (a malicious tar could plant a symlink
275+
// then overwrite it with a regular file entry targeting outside dst).
276+
if existing, lErr := os.Lstat(filePath); lErr == nil && existing.Mode()&fs.ModeSymlink != 0 {
277+
if err = os.Remove(filePath); err != nil {
278+
return fmt.Errorf("removing symlink before file write %s: %w", filePath, err)
279+
}
280+
}
281+
282+
if err = tarUnpackFile(filePath, tarReader, tarHeader); err != nil {
212283
return fmt.Errorf("unpacking file %s: %w", filePath, err)
213284
}
214285
}
286+
287+
// Restore directory timestamps deepest-first so that restoring a parent's
288+
// mtime is not undone by a subsequent Chtimes on a child directory.
289+
sort.Slice(dirTimestamps, func(i, j int) bool {
290+
return strings.Count(dirTimestamps[i].path, string(os.PathSeparator)) >
291+
strings.Count(dirTimestamps[j].path, string(os.PathSeparator))
292+
})
293+
for _, dt := range dirTimestamps {
294+
if dt.modTime.IsZero() {
295+
continue
296+
}
297+
accTime := dt.accTime
298+
if accTime.IsZero() {
299+
accTime = dt.modTime
300+
}
301+
if err := os.Chtimes(dt.path, accTime, dt.modTime); err != nil {
302+
return fmt.Errorf("restoring timestamps for directory %s: %w", dt.path, err)
303+
}
304+
}
305+
306+
return nil
307+
}
308+
309+
func tarUnpackFile(dstFileName string, src io.Reader, header *tar.Header) (err error) {
310+
srcFileInfo := header.FileInfo()
311+
312+
if err = tarWriteFile(dstFileName, src, srcFileInfo); err != nil {
313+
return err
314+
}
315+
316+
// Restore original timestamps from tar header after the file is closed,
317+
// since some platforms (e.g. Windows) cannot change timestamps on open files.
318+
if header.ModTime.IsZero() {
319+
return nil
320+
}
321+
accTime := header.AccessTime
322+
if accTime.IsZero() {
323+
accTime = header.ModTime
324+
}
325+
if err = os.Chtimes(dstFileName, accTime, header.ModTime); err != nil {
326+
return fmt.Errorf("restoring timestamps for %s: %w", dstFileName, err)
327+
}
328+
215329
return nil
216330
}
217331

218-
func tarUnpackFile(dstFileName string, src io.Reader, srcFileInfo fs.FileInfo) (err error) {
332+
func tarWriteFile(dstFileName string, src io.Reader, srcFileInfo fs.FileInfo) (err error) {
219333
var dstFile *os.File
220-
dstFile, err = os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcFileInfo.Mode().Perm())
334+
dstFile, err = os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcFileInfo.Mode().Perm())
221335
if err != nil {
222336
return fmt.Errorf("opening destination file %s: %w", dstFileName, err)
223337
}
224338
defer func() {
225-
err = errors.Join(err, closeAndWrapErr(dstFile, "closing destination file %s: %w", dstFile))
339+
err = errors.Join(err, closeAndWrapErr(dstFile, "closing destination file %s: %w", dstFileName))
226340
}()
227341

228342
n, err := io.Copy(dstFile, src)

pkg/nfs/tar_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,58 @@ func TestSymlinks(t *testing.T) {
348348
t.Errorf("expected file %s to be: %X, got %X", outputRelSymlinkPath, testContent, data)
349349
}
350350
}
351+
352+
func TestTarUnpackPreservesTimestamps(t *testing.T) {
353+
// Create a source directory with known timestamps
354+
srcDir := t.TempDir()
355+
subDir := filepath.Join(srcDir, "subdir")
356+
if err := os.MkdirAll(subDir, 0755); err != nil {
357+
t.Fatal(err)
358+
}
359+
360+
filePath := filepath.Join(subDir, "testfile.txt")
361+
if err := os.WriteFile(filePath, []byte("hello timestamps"), 0644); err != nil {
362+
t.Fatal(err)
363+
}
364+
365+
// Set known timestamps (2020-06-15 12:00:00 UTC)
366+
knownTime := time.Date(2020, 6, 15, 12, 0, 0, 0, time.UTC)
367+
if err := os.Chtimes(filePath, knownTime, knownTime); err != nil {
368+
t.Fatal(err)
369+
}
370+
if err := os.Chtimes(subDir, knownTime, knownTime); err != nil {
371+
t.Fatal(err)
372+
}
373+
374+
// Pack
375+
archivePath := filepath.Join(t.TempDir(), "test.tar.gz")
376+
if err := TarPack(srcDir, archivePath, true); err != nil {
377+
t.Fatalf("TarPack failed: %v", err)
378+
}
379+
380+
// Unpack
381+
dstDir := t.TempDir()
382+
if err := TarUnpack(archivePath, dstDir, true); err != nil {
383+
t.Fatalf("TarUnpack failed: %v", err)
384+
}
385+
386+
// Verify file timestamp
387+
restoredFile := filepath.Join(dstDir, "subdir", "testfile.txt")
388+
fi, err := os.Stat(restoredFile)
389+
if err != nil {
390+
t.Fatal(err)
391+
}
392+
if diff := fi.ModTime().Sub(knownTime); diff < -time.Second || diff > time.Second {
393+
t.Errorf("file mtime: got %v, want %v (diff %v)", fi.ModTime(), knownTime, diff)
394+
}
395+
396+
// Verify directory timestamp
397+
restoredDir := filepath.Join(dstDir, "subdir")
398+
di, err := os.Stat(restoredDir)
399+
if err != nil {
400+
t.Fatal(err)
401+
}
402+
if diff := di.ModTime().Sub(knownTime); diff < -time.Second || diff > time.Second {
403+
t.Errorf("dir mtime: got %v, want %v (diff %v)", di.ModTime(), knownTime, diff)
404+
}
405+
}

0 commit comments

Comments
 (0)