From bf19758e054120c1f1b7341c09740f86787d3144 Mon Sep 17 00:00:00 2001 From: Tai An Date: Wed, 22 Jul 2026 00:28:02 -0700 Subject: [PATCH] fix(ollama): cap num_ctx so it cannot wrap negative when cast to int32 (#11032) * fix(ollama): cap num_ctx so it cannot wrap negative when cast to int32 applyOllamaOptions copied a client-supplied options.num_ctx straight into cfg.ContextSize with only a > 0 check. That value is later cast to int32 before it reaches the backend (core/backend/options.go), so a num_ctx above math.MaxInt32 silently wrapped into a negative context size that was then sent to the LoadModel gRPC call. Both /api/chat and /api/generate share applyOllamaOptions, so both endpoints were affected. Cap num_ctx at math.MaxInt32 so the later cast stays positive, and add internal regression coverage for the overflow, in-range, and unset cases. num_ctx remains an intentional user override, so this does not re-impose the hardware-aware auto context clamp; that policy choice is left to maintainers. Fixes #11022 Signed-off-by: Tai An * fix(ollama): clamp num_ctx to model context ceiling, not just int32 Per review on #11032: capping only at math.MaxInt32 still let an unauthenticated request replace the hardware/model-derived context limit with ~2.1B tokens, so a real backend could attempt a catastrophic KV-cache allocation. Treat any existing positive cfg.ContextSize as the server ceiling and clamp num_ctx down to it (smaller values still honored), while retaining the int32-safe bound when no smaller ceiling exists. Shared by /api/chat and /api/generate via applyOllamaOptions. Add regression coverage proving num_ctx=2,000,000,000 cannot replace an existing 4096/8192 ceiling. Signed-off-by: Tai An --------- Signed-off-by: Tai An --- core/http/endpoints/ollama/helpers.go | 23 ++++++- .../endpoints/ollama/helpers_internal_test.go | 65 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 core/http/endpoints/ollama/helpers_internal_test.go diff --git a/core/http/endpoints/ollama/helpers.go b/core/http/endpoints/ollama/helpers.go index b6c1f88f2..3eece8ef5 100644 --- a/core/http/endpoints/ollama/helpers.go +++ b/core/http/endpoints/ollama/helpers.go @@ -3,6 +3,7 @@ package ollama import ( "encoding/json" "fmt" + "math" "github.com/labstack/echo/v4" "github.com/mudler/LocalAI/core/config" @@ -57,7 +58,25 @@ func applyOllamaOptions(opts *schema.OllamaOptions, cfg *config.ModelConfig) { cfg.StopWords = append(cfg.StopWords, opts.Stop...) } if opts.NumCtx > 0 { - cfg.ContextSize = &opts.NumCtx + numCtx := opts.NumCtx + // The model configuration / hardware-aware defaults have already + // populated cfg.ContextSize by the time we get here. Treat any + // existing positive value as the server-side ceiling: an + // unauthenticated client must never be able to *raise* the context + // window, since that drives KV-cache allocation and an oversized + // value (e.g. 2,000,000,000) can trigger a catastrophic OOM. A + // smaller num_ctx is still honored. See issue #11022. + if cfg.ContextSize != nil && *cfg.ContextSize > 0 && numCtx > *cfg.ContextSize { + numCtx = *cfg.ContextSize + } + // Regardless of the ceiling, keep the value int32-safe: ContextSize is + // cast to int32 before it reaches the backend (core/backend/options.go), + // so a value above math.MaxInt32 would silently wrap into a negative + // context size when no smaller ceiling exists. + if numCtx > math.MaxInt32 { + numCtx = math.MaxInt32 + } + cfg.ContextSize = &numCtx } } @@ -80,4 +99,4 @@ func ollamaMessagesToOpenAI(messages []schema.OllamaMessage) []schema.Message { result = append(result, openAIMsg) } return result -} +} \ No newline at end of file diff --git a/core/http/endpoints/ollama/helpers_internal_test.go b/core/http/endpoints/ollama/helpers_internal_test.go new file mode 100644 index 000000000..356c19a5b --- /dev/null +++ b/core/http/endpoints/ollama/helpers_internal_test.go @@ -0,0 +1,65 @@ +package ollama + +import ( + "math" + + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/core/schema" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// These specs pin the num_ctx handling for issue #11022. /api/chat and +// /api/generate share applyOllamaOptions, so exercising the helper covers both +// endpoints. A client-supplied options.num_ctx must not be able to *raise* the +// model/hardware-derived context ceiling already in cfg.ContextSize (that value +// drives KV-cache allocation, so an oversized request is a DoS), and it must +// stay int32-safe because ContextSize is cast to int32 before it reaches the +// backend, where an out-of-range value would silently wrap negative. +var _ = Describe("applyOllamaOptions num_ctx clamping (issue #11022)", func() { + It("caps num_ctx at math.MaxInt32 so the later int32 cast cannot wrap negative", func() { + cfg := &config.ModelConfig{} + applyOllamaOptions(&schema.OllamaOptions{NumCtx: math.MaxInt32 + 1}, cfg) + + Expect(cfg.ContextSize).ToNot(BeNil()) + Expect(*cfg.ContextSize).To(Equal(math.MaxInt32)) + Expect(int32(*cfg.ContextSize)).To(BeNumerically(">", 0), + "capped context size must stay positive after the int32 cast") + }) + + It("does not let an oversized num_ctx raise an existing context ceiling", func() { + for _, ceiling := range []int{4096, 8192} { + existing := ceiling + cfg := &config.ModelConfig{ContextSize: &existing} + applyOllamaOptions(&schema.OllamaOptions{NumCtx: 2000000000}, cfg) + + Expect(cfg.ContextSize).ToNot(BeNil()) + Expect(*cfg.ContextSize).To(Equal(ceiling), + "an in-range but oversized num_ctx must be clamped to the server ceiling, not replace it") + } + }) + + It("still honors a num_ctx smaller than the existing ceiling", func() { + existing := 8192 + cfg := &config.ModelConfig{ContextSize: &existing} + applyOllamaOptions(&schema.OllamaOptions{NumCtx: 2048}, cfg) + + Expect(cfg.ContextSize).ToNot(BeNil()) + Expect(*cfg.ContextSize).To(Equal(2048)) + }) + + It("passes an in-range num_ctx through unchanged", func() { + cfg := &config.ModelConfig{} + applyOllamaOptions(&schema.OllamaOptions{NumCtx: 4096}, cfg) + + Expect(cfg.ContextSize).ToNot(BeNil()) + Expect(*cfg.ContextSize).To(Equal(4096)) + }) + + It("leaves ContextSize untouched when num_ctx is not set", func() { + cfg := &config.ModelConfig{} + applyOllamaOptions(&schema.OllamaOptions{}, cfg) + + Expect(cfg.ContextSize).To(BeNil()) + }) +}) \ No newline at end of file