From 0bb8669223b66d603ff78523c518a8903b85e7a7 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 29 Apr 2026 12:33:43 +0000 Subject: [PATCH 1/3] fix: handle multiline JAVA_OPTS in profile.d assembly script JAVA_OPTS set via YAML block scalar (>) in manifest.yml may contain literal newlines when delivered to the shell. The profile.d script used sed to substitute $JAVA_OPTS into .opts file content, which fails with "unterminated 's' command" when the value spans multiple lines. Fix: normalize USER_JAVA_OPTS to a single line at capture time using tr to convert newlines to spaces and collapse multiple spaces. Added test reproducing the exact failure with multiline JAVA_OPTS containing -javaagent and -Xms/-Xmx/-XX flags. --- src/java/frameworks/java_opts_writer.go | 3 +- src/java/frameworks/java_opts_writer_test.go | 74 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index abfd1c3c7..d1c1c9d5d 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -73,7 +73,8 @@ func CreateJavaOptsAssemblyScript(ctx *common.Context) error { # Expands runtime variables like $DEPS_DIR, $HOME, $JAVA_OPTS, and all other environment variables # Save original JAVA_OPTS from environment (user-provided) -USER_JAVA_OPTS="$JAVA_OPTS" +# Normalize to single line: YAML block scalars (>) may introduce newlines +USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ' | tr -s ' ') # Start building new JAVA_OPTS JAVA_OPTS="" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index e4c154ac0..0b34e67c8 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -2,14 +2,55 @@ package frameworks_test import ( "os" + "os/exec" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/cloudfoundry/java-buildpack/src/java/common" + "github.com/cloudfoundry/java-buildpack/src/java/frameworks" + "github.com/cloudfoundry/libbuildpack" ) +func newJavaOptsContext(buildDir, cacheDir, depsDir string) *common.Context { + logger := libbuildpack.NewLogger(GinkgoWriter) + manifest := &libbuildpack.Manifest{} + installer := &libbuildpack.Installer{} + stager := libbuildpack.NewStager([]string{buildDir, cacheDir, depsDir, "0"}, logger, manifest) + return &common.Context{ + Stager: stager, + Manifest: manifest, + Installer: installer, + Log: logger, + Command: &libbuildpack.Command{}, + } +} + var _ = Describe("Java Opts Writer", func() { + var ( + buildDir string + cacheDir string + depsDir string + ctx *common.Context + ) + + BeforeEach(func() { + var err error + buildDir, err = os.MkdirTemp("", "build") + Expect(err).NotTo(HaveOccurred()) + cacheDir, err = os.MkdirTemp("", "cache") + Expect(err).NotTo(HaveOccurred()) + depsDir, err = os.MkdirTemp("", "deps") + Expect(err).NotTo(HaveOccurred()) + ctx = newJavaOptsContext(buildDir, cacheDir, depsDir) + }) + AfterEach(func() { os.Unsetenv("JAVA_OPTS") + os.RemoveAll(buildDir) + os.RemoveAll(cacheDir) + os.RemoveAll(depsDir) }) Describe("Basic options", func() { @@ -20,4 +61,37 @@ var _ = Describe("Java Opts Writer", func() { Expect(os.Getenv("JAVA_OPTS")).To(Equal(javaOpts)) }) }) + + Describe("CreateJavaOptsAssemblyScript", func() { + It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { + // Reproduce the manifest pattern: + // JAVA_OPTS: > + // -javaagent:$HOME/BOOT-INF/classes/some-java-agent.jar + // -Xms512m + // -Xmx1024m + // YAML '>' folds newlines to spaces, but CF may deliver them as literal newlines + multilineJavaOpts := "-javaagent:$HOME/BOOT-INF/classes/some-java-agent.jar\n-Xms512m\n-Xmx1024m\n-XX:MaxDirectMemorySize=256m" + + err := frameworks.CreateJavaOptsAssemblyScript(ctx) + Expect(err).NotTo(HaveOccurred()) + + // Create an opts file that references $JAVA_OPTS (as frameworks do) + optsDir := filepath.Join(depsDir, "0", "java_opts") + Expect(os.MkdirAll(optsDir, 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte("-javaagent:somepath.jar $JAVA_OPTS"), 0644)).To(Succeed()) + + // Run the generated profile.d script with multiline JAVA_OPTS + scriptPath := filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") + cmd := exec.Command("bash", "-c", + "source "+scriptPath+" && echo \"$JAVA_OPTS\"") + cmd.Env = append(os.Environ(), + "JAVA_OPTS="+multilineJavaOpts, + "DEPS_DIR="+depsDir, + "HOME=/home/vcap/app", + ) + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", string(output)) + Expect(string(output)).To(ContainSubstring("-Xms512m")) + }) + }) }) From 69aa825eef30bd948b66b5f9f5e96f183e1ffde5 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 29 Apr 2026 13:36:28 +0000 Subject: [PATCH 2/3] fix: replace sed with bash parameter expansion in java_opts assembly script Using sed to substitute $DEPS_DIR, $HOME and $JAVA_OPTS in opts file content breaks when those values contain the sed delimiter (|), backslashes, ampersands, or newlines. All are valid in JAVA_OPTS, e.g. javaagent options using pipe syntax: -javaagent:agent.jar=enableExecutorMBeans|disableMyFeature Bash parameter expansion (${var//find/replace}) has no special-character restrictions and replaces sed for these three substitutions. Adds regression test covering pipe character in JAVA_OPTS. --- src/java/frameworks/java_opts_writer.go | 15 +++---- src/java/frameworks/java_opts_writer_test.go | 46 ++++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index d1c1c9d5d..ea58f2f5d 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -85,15 +85,12 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # Read content and expand runtime variables opts_content=$(cat "$opts_file") - # First, expand special variables that need specific handling - # Expand $DEPS_DIR variable - opts_content=$(echo "$opts_content" | sed "s|\$DEPS_DIR|$DEPS_DIR|g") - - # Expand $HOME variable (for app-provided JARs like AspectJ) - opts_content=$(echo "$opts_content" | sed "s|\$HOME|$HOME|g") - - # Expand $JAVA_OPTS to the saved USER_JAVA_OPTS value (not the loop's current JAVA_OPTS) - opts_content=$(echo "$opts_content" | sed "s|\$JAVA_OPTS|$USER_JAVA_OPTS|g") + # Expand $DEPS_DIR, $HOME, $JAVA_OPTS using bash parameter expansion. + # sed-based substitution breaks when these values contain the sed delimiter (|), + # backslashes, ampersands, or newlines — all valid in JAVA_OPTS and paths. + opts_content="${opts_content//\$DEPS_DIR/$DEPS_DIR}" + opts_content="${opts_content//\$HOME/$HOME}" + opts_content="${opts_content//\$JAVA_OPTS/$USER_JAVA_OPTS}" # Now expand all remaining environment variables using eval with proper escaping # This mimics Ruby buildpack behavior where shell naturally expands variables diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 0b34e67c8..76babe0e2 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -63,35 +63,47 @@ var _ = Describe("Java Opts Writer", func() { }) Describe("CreateJavaOptsAssemblyScript", func() { - It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { - // Reproduce the manifest pattern: - // JAVA_OPTS: > - // -javaagent:$HOME/BOOT-INF/classes/some-java-agent.jar - // -Xms512m - // -Xmx1024m - // YAML '>' folds newlines to spaces, but CF may deliver them as literal newlines - multilineJavaOpts := "-javaagent:$HOME/BOOT-INF/classes/some-java-agent.jar\n-Xms512m\n-Xmx1024m\n-XX:MaxDirectMemorySize=256m" - + runScript := func(javaOpts string, optsFileContent string) (string, error) { err := frameworks.CreateJavaOptsAssemblyScript(ctx) Expect(err).NotTo(HaveOccurred()) - // Create an opts file that references $JAVA_OPTS (as frameworks do) optsDir := filepath.Join(depsDir, "0", "java_opts") Expect(os.MkdirAll(optsDir, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte("-javaagent:somepath.jar $JAVA_OPTS"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte(optsFileContent), 0644)).To(Succeed()) - // Run the generated profile.d script with multiline JAVA_OPTS scriptPath := filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") - cmd := exec.Command("bash", "-c", - "source "+scriptPath+" && echo \"$JAVA_OPTS\"") + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && echo \"$JAVA_OPTS\"") cmd.Env = append(os.Environ(), - "JAVA_OPTS="+multilineJavaOpts, + "JAVA_OPTS="+javaOpts, "DEPS_DIR="+depsDir, "HOME=/home/vcap/app", ) output, err := cmd.CombinedOutput() - Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", string(output)) - Expect(string(output)).To(ContainSubstring("-Xms512m")) + return string(output), err + } + + It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { + // Reproduce the manifest pattern: + // JAVA_OPTS: > + // -javaagent:$HOME/BOOT-INF/lib/agent.jar + // -XX:+UseZGC + // YAML '>' folds newlines to spaces, but CF may deliver them as literal newlines + multilineJavaOpts := "-javaagent:$HOME/BOOT-INF/lib/agent.jar\n-XX:+UseZGC\n-XX:+AlwaysPreTouch" + + output, err := runScript(multilineJavaOpts, "-javaagent:somepath.jar $JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-XX:+UseZGC")) + }) + + It("handles pipe character in JAVA_OPTS (e.g. javaagent options) without sed error", func() { + // Reproduce the manifest pattern: + // JAVA_OPTS: > + // -javaagent:$HOME/BOOT-INF/lib/jfr-exporter.jar=enableExecutorMBeans|disableMyFeature + pipeJavaOpts := "-javaagent:$HOME/BOOT-INF/lib/jfr-exporter.jar=enableExecutorMBeans|disableMyFeature" + + output, err := runScript(pipeJavaOpts, "-javaagent:somepath.jar $JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("enableExecutorMBeans|disableMyFeature")) }) }) }) From a31c74e9bfdbcf185f8459bcf8a81fc580d67254 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Wed, 29 Apr 2026 13:53:29 +0000 Subject: [PATCH 3/3] test: add coverage for $HOME and $DEPS_DIR expansion in opts file content --- src/java/frameworks/java_opts_writer_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 76babe0e2..e092ec5df 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -105,5 +105,17 @@ var _ = Describe("Java Opts Writer", func() { Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) Expect(output).To(ContainSubstring("enableExecutorMBeans|disableMyFeature")) }) + + It("expands $HOME in opts file content", func() { + output, err := runScript("", "-javaagent:$HOME/BOOT-INF/lib/agent.jar") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-javaagent:/home/vcap/app/BOOT-INF/lib/agent.jar")) + }) + + It("expands $DEPS_DIR in opts file content", func() { + output, err := runScript("", "-Djava.security.properties=$DEPS_DIR/0/security.properties") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-Djava.security.properties=" + depsDir + "/0/security.properties")) + }) }) })