Skip to content

Commit ecb0aec

Browse files
author
Vineeth Rao Kanaparthi
committed
Honor max_output_length for custom plugin stdout capture
Custom plugin stdout was read into a hardcoded 4 KiB buffer (maxCustomPluginBufferBytes = 1024*4) and only then truncated to the configured max_output_length, so the effective limit was min(4096, max_output_length). Any plugin configured with max_output_length > 4096 was silently capped at 4096 bytes, and a JSON-emitting plugin would be cut mid-token into invalid output. Drive the stdout read limit from max_output_length, bounded by a 1 MiB safety ceiling against a runaway plugin. stderr keeps a small fixed cap since it is only used for diagnostic logging and never becomes the plugin output. Adds a regression test and fixture asserting that a plugin emitting more than 4 KiB is captured up to max_output_length rather than the old fixed buffer size. Signed-off-by: Vineeth Rao Kanaparthi <vkanaparthi@twitter.com>
1 parent 8a5df6d commit ecb0aec

3 files changed

Lines changed: 72 additions & 5 deletions

File tree

pkg/custompluginmonitor/plugin/plugin.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,18 @@ import (
3333
"k8s.io/node-problem-detector/pkg/util/tomb"
3434
)
3535

36-
// maxCustomPluginBufferBytes is the max bytes that a custom plugin is allowed to
37-
// send to stdout/stderr. Any bytes exceeding this value will be truncated.
38-
const maxCustomPluginBufferBytes = 1024 * 4
36+
// maxCustomPluginStdoutCeilingBytes is a hard safety ceiling on how many bytes
37+
// of stdout NPD reads from a custom plugin, bounding memory use from a runaway
38+
// plugin. The per-plugin max_output_length is what actually governs the message
39+
// size; this ceiling only caps the (misconfigured) case where max_output_length
40+
// itself exceeds it. It must be >= the largest supported max_output_length so a
41+
// plugin's configured limit is never silently truncated by the read buffer.
42+
const maxCustomPluginStdoutCeilingBytes = 1024 * 1024
43+
44+
// maxCustomPluginStderrBytes caps stderr. run() uses stderr only for diagnostic
45+
// logging (logPluginStderr) and never includes it in the returned output, so it
46+
// does not need to honor max_output_length and keeps a small fixed cap.
47+
const maxCustomPluginStderrBytes = 1024 * 4
3948

4049
type Plugin struct {
4150
config cpmtypes.CustomPluginConfig
@@ -210,14 +219,22 @@ func (p *Plugin) run(rule cpmtypes.CustomRule) (exitStatus cpmtypes.Status, outp
210219
stderrErr error
211220
)
212221

222+
// Capture enough stdout to honor the configured max_output_length, bounded
223+
// by a hard safety ceiling. Previously this was a fixed 4 KiB buffer that
224+
// silently truncated plugins configured with a larger max_output_length.
225+
stdoutCapture := int64(*p.config.PluginGlobalConfig.MaxOutputLength)
226+
if stdoutCapture > maxCustomPluginStdoutCeilingBytes {
227+
stdoutCapture = maxCustomPluginStdoutCeilingBytes
228+
}
229+
213230
wg.Add(2)
214231
go func() {
215232
defer wg.Done()
216-
stdout, stdoutErr = readFromReader(stdoutPipe, maxCustomPluginBufferBytes)
233+
stdout, stdoutErr = readFromReader(stdoutPipe, stdoutCapture)
217234
}()
218235
go func() {
219236
defer wg.Done()
220-
stderr, stderrErr = readFromReader(stderrPipe, maxCustomPluginBufferBytes)
237+
stderr, stderrErr = readFromReader(stderrPipe, maxCustomPluginStderrBytes)
221238
}()
222239
// This will wait for the reads to complete. If the execution times out, the pipes
223240
// will be closed and the wait group unblocks.

pkg/custompluginmonitor/plugin/plugin_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package plugin
1818

1919
import (
2020
"runtime"
21+
"strings"
2122
"testing"
2223
"time"
2324

@@ -121,3 +122,44 @@ func TestNewPluginRun(t *testing.T) {
121122
})
122123
}
123124
}
125+
126+
// TestPluginRunCaptureRespectsMaxOutputLength verifies that a plugin emitting
127+
// more than the old hardcoded 4 KiB capture buffer is captured up to the
128+
// configured max_output_length, not silently truncated at 4096 bytes.
129+
func TestPluginRunCaptureRespectsMaxOutputLength(t *testing.T) {
130+
if runtime.GOOS == "windows" {
131+
t.Skip("fixture is a shell script; this path is exercised on non-Windows platforms")
132+
}
133+
134+
ruleTimeout := 1 * time.Second
135+
// Larger than the old 4096-byte capture buffer, and smaller than the
136+
// 16384 bytes the fixture emits, so the result must be exactly this many
137+
// bytes if (and only if) capture honors max_output_length.
138+
maxOutputLength := 8192
139+
140+
conf := cpmtypes.CustomPluginConfig{}
141+
// Set before ApplyConfiguration so it is not overwritten by the default,
142+
// and use a fresh pointer so we don't mutate the package-level default.
143+
conf.PluginGlobalConfig.MaxOutputLength = &maxOutputLength
144+
if err := (&conf).ApplyConfiguration(); err != nil {
145+
t.Fatalf("Failed to apply configuration: %v", err)
146+
}
147+
p := Plugin{config: conf}
148+
149+
rule := cpmtypes.CustomRule{
150+
Path: "./test-data/large-stdout-with-ok-exit-status.sh",
151+
Timeout: &ruleTimeout,
152+
}
153+
gotStatus, gotOutput := p.run(rule)
154+
155+
if gotStatus != cpmtypes.OK {
156+
t.Errorf("exit status: got %v, want %v", gotStatus, cpmtypes.OK)
157+
}
158+
if len(gotOutput) != maxOutputLength {
159+
t.Errorf("output length: got %d, want %d (must be capped at max_output_length, not a fixed 4 KiB buffer)",
160+
len(gotOutput), maxOutputLength)
161+
}
162+
if want := strings.Repeat("a", maxOutputLength); gotOutput != want {
163+
t.Errorf("output content mismatch: got %d bytes, want %d 'a' bytes", len(gotOutput), len(want))
164+
}
165+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
3+
# Emits 16384 bytes of stdout (larger than the old hardcoded 4 KiB capture
4+
# buffer and larger than the test's max_output_length) with a success exit
5+
# code. Used to verify that plugin output capture honors max_output_length
6+
# rather than a fixed internal buffer size.
7+
head -c 16384 /dev/zero | tr '\0' 'a'
8+
exit 0

0 commit comments

Comments
 (0)