1
0
forked from wrenn/wrenn

COPY multi-source support, configurable rootfs size, build fixes

- COPY now supports multiple sources: COPY a.txt b.txt /dest/
  Last argument is always destination (matches Dockerfile semantics).
- COPY resolves relative destinations against current WORKDIR.
- WRENN_DEFAULT_ROOTFS_SIZE env var (e.g. 5G, 2Gi, 1000M, 512Mi)
  controls template rootfs expansion. Used both at agent startup
  (EnsureImageSizes) and after FlattenRootfs (shrink then re-expand).
- Pre-build now sets WORKDIR /home/wrenn-user after USER switch.
- Extracted archive files get chmod a+rX for readability.
- Path traversal validation on COPY sources.
This commit is contained in:
2026-04-12 03:39:17 +06:00
parent 46c43b95c2
commit 25b5258841
8 changed files with 110 additions and 29 deletions

View File

@ -208,25 +208,33 @@ func execCopy(
bctx *ExecContext,
execFn ExecFunc,
) (BuildLogEntry, bool) {
// Validate source path: must be relative and not escape the archive directory.
cleaned := path.Clean(st.Src)
if strings.HasPrefix(cleaned, "..") || strings.HasPrefix(cleaned, "/") {
return BuildLogEntry{
Step: step,
Phase: phase,
Cmd: st.Raw,
Stderr: "COPY source must be a relative path within the archive",
}, false
// Validate all source paths: must be relative and not escape the archive directory.
var srcPaths []string
for _, s := range st.Srcs {
cleaned := path.Clean(s)
if strings.HasPrefix(cleaned, "..") || strings.HasPrefix(cleaned, "/") {
return BuildLogEntry{
Step: step,
Phase: phase,
Cmd: st.Raw,
Stderr: fmt.Sprintf("COPY source must be a relative path within the archive: %q", s),
}, false
}
srcPaths = append(srcPaths, shellescape(BuildFilesDir+"/"+cleaned))
}
src := BuildFilesDir + "/" + cleaned
dst := st.Dst
// Resolve relative destination against the current WORKDIR.
if dst != "" && dst[0] != '/' && bctx.WorkDir != "" {
dst = bctx.WorkDir + "/" + dst
}
owner := "root"
if bctx.User != "" {
owner = bctx.User
}
script := fmt.Sprintf(
"cp -r %s %s && chown -R %s:%s %s",
shellescape(src), shellescape(dst), shellescape(owner), shellescape(owner), shellescape(dst),
strings.Join(srcPaths, " "), shellescape(dst), shellescape(owner), shellescape(owner), shellescape(dst),
)
entry := execRawShell(ctx, st.Raw, sandboxID, phase, step, 60*time.Second, execFn, script)

View File

@ -27,7 +27,7 @@ type Step struct {
Key string // KindENV: variable name; KindUSER: username
Value string // KindENV: variable value
Path string // KindWORKDIR: directory path
Src string // KindCOPY: source path (relative to build archive)
Srcs []string // KindCOPY: source paths (relative to build archive)
Dst string // KindCOPY: destination path inside sandbox
}
@ -148,12 +148,14 @@ func parseUSER(raw, username string) (Step, error) {
func parseCOPY(raw, rest string) (Step, error) {
if rest == "" {
return Step{}, fmt.Errorf("COPY requires <src> <dst>: %q", raw)
return Step{}, fmt.Errorf("COPY requires <src>... <dst>: %q", raw)
}
src, dst, found := strings.Cut(rest, " ")
dst = strings.TrimSpace(dst)
if !found || dst == "" {
return Step{}, fmt.Errorf("COPY requires <src> <dst>: %q", raw)
parts := strings.Fields(rest)
if len(parts) < 2 {
return Step{}, fmt.Errorf("COPY requires <src>... <dst>: %q", raw)
}
return Step{Kind: KindCOPY, Raw: raw, Src: src, Dst: dst}, nil
// Last argument is the destination, everything before is sources.
dst := parts[len(parts)-1]
srcs := parts[:len(parts)-1]
return Step{Kind: KindCOPY, Raw: raw, Srcs: srcs, Dst: dst}, nil
}

View File

@ -1,6 +1,7 @@
package recipe
import (
"reflect"
"testing"
"time"
)
@ -131,7 +132,12 @@ func TestParseStep(t *testing.T) {
{
name: "COPY basic",
input: "COPY config.yaml /etc/app/config.yaml",
want: Step{Kind: KindCOPY, Raw: "COPY config.yaml /etc/app/config.yaml", Src: "config.yaml", Dst: "/etc/app/config.yaml"},
want: Step{Kind: KindCOPY, Raw: "COPY config.yaml /etc/app/config.yaml", Srcs: []string{"config.yaml"}, Dst: "/etc/app/config.yaml"},
},
{
name: "COPY multiple sources",
input: "COPY a.txt b.txt /dest/",
want: Step{Kind: KindCOPY, Raw: "COPY a.txt b.txt /dest/", Srcs: []string{"a.txt", "b.txt"}, Dst: "/dest/"},
},
{
name: "COPY missing dst",
@ -169,7 +175,7 @@ func TestParseStep(t *testing.T) {
if err != nil {
t.Fatalf("ParseStep(%q) unexpected error: %v", tc.input, err)
}
if got != tc.want {
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("ParseStep(%q)\n got %+v\n want %+v", tc.input, got, tc.want)
}
})