<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vignan Nallani</title>
    <description>The latest articles on DEV Community by Vignan Nallani (@vignannallani).</description>
    <link>https://dev.to/vignannallani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003483%2F59e5e8f6-fe79-4f0b-ba99-073499fc8054.png</url>
      <title>DEV Community: Vignan Nallani</title>
      <link>https://dev.to/vignannallani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vignannallani"/>
    <language>en</language>
    <item>
      <title>An infinite loop hiding in a string search: debugging objectIsQueried in KubeStellar</title>
      <dc:creator>Vignan Nallani</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:27:15 +0000</pubDate>
      <link>https://dev.to/vignannallani/an-infinite-loop-hiding-in-a-string-search-debugging-objectisqueried-in-kubestellar-2d29</link>
      <guid>https://dev.to/vignannallani/an-infinite-loop-hiding-in-a-string-search-debugging-objectisqueried-in-kubestellar-2d29</guid>
      <description>&lt;p&gt;Some bugs crash loudly. The more interesting ones just &lt;em&gt;hang&lt;/em&gt; — no panic, no error, no stack trace, just a goroutine quietly spinning forever on input that looks completely ordinary. I ran into one of those recently while reading through &lt;a href="https://github.com/kubestellar/kubestellar" rel="noopener noreferrer"&gt;KubeStellar&lt;/a&gt;, a CNCF Sandbox project for multi-cluster configuration management. Here's the hunt, the root cause, and the one-character-deep mistake that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the function was supposed to do
&lt;/h2&gt;

&lt;p&gt;Deep in KubeStellar's status-combination code (&lt;code&gt;pkg/status/combinedstatus-resolution.go&lt;/code&gt;) there's a small helper called &lt;code&gt;objectIsQueried&lt;/code&gt;. Its job is simple: given a query string and an object name, decide whether the object appears in the query &lt;em&gt;as a whole word&lt;/em&gt; — not as a fragment buried inside a longer identifier. So &lt;code&gt;"foo"&lt;/code&gt; should match &lt;code&gt;"select foo from bar"&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;"foobar"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To do that, it walks the string looking for each occurrence of the object, and for each one asks a second helper, &lt;code&gt;isWholeWord&lt;/code&gt;, whether that position is bounded by non-alphanumeric characters. Reasonable design. Here's the original loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;objectIsQueried&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// search from idx onward&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isWholeWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it quickly and it looks fine. That's exactly what made it dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: relative vs. absolute indices
&lt;/h2&gt;

&lt;p&gt;The bug lives entirely in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strings.Index&lt;/code&gt; returns the offset of &lt;code&gt;obj&lt;/code&gt; &lt;strong&gt;relative to the slice it was given&lt;/strong&gt; — and the slice here is &lt;code&gt;(*query)[idx:]&lt;/code&gt;, not the whole string. But the code then turns around and uses that relative offset as if it were an absolute position in the original &lt;code&gt;query&lt;/code&gt;, both when calling &lt;code&gt;isWholeWord(query, idx, ...)&lt;/code&gt; and when advancing &lt;code&gt;idx += len(obj)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As long as the first match happens to sit at the start of the string, relative and absolute agree, and everything works. The moment the search slice starts somewhere in the middle, the two diverge — and the loop starts using a wrong index that points back into territory it already searched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it spin
&lt;/h2&gt;

&lt;p&gt;The clearest way to see the failure is to trace the input &lt;code&gt;query = "foobar foo"&lt;/code&gt;, &lt;code&gt;obj = "foo"&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;idx = 0.&lt;/strong&gt; &lt;code&gt;strings.Index("foobar foo", "foo")&lt;/code&gt; → &lt;code&gt;0&lt;/code&gt;. &lt;code&gt;isWholeWord&lt;/code&gt; at position 0? The character after &lt;code&gt;"foo"&lt;/code&gt; is &lt;code&gt;b&lt;/code&gt; — alphanumeric — so it's embedded in &lt;code&gt;"foobar"&lt;/code&gt;, not a whole word. &lt;code&gt;false&lt;/code&gt;. Advance: &lt;code&gt;idx += 3&lt;/code&gt; → &lt;strong&gt;idx = 3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;idx = 3.&lt;/strong&gt; Now we search the slice &lt;code&gt;"bar foo"&lt;/code&gt;. &lt;code&gt;strings.Index("bar foo", "foo")&lt;/code&gt; → &lt;code&gt;4&lt;/code&gt; &lt;em&gt;relative to that slice&lt;/em&gt;. The code stores &lt;code&gt;4&lt;/code&gt; as if it were absolute. &lt;code&gt;isWholeWord&lt;/code&gt; at absolute position 4 is wrong, returns &lt;code&gt;false&lt;/code&gt;. Advance: &lt;code&gt;idx += 3&lt;/code&gt; → &lt;strong&gt;idx = 7&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;idx = 7.&lt;/strong&gt; Search the slice &lt;code&gt;"foo"&lt;/code&gt;. &lt;code&gt;strings.Index("foo", "foo")&lt;/code&gt; → &lt;code&gt;0&lt;/code&gt; relative. Stored as absolute &lt;code&gt;0&lt;/code&gt;. We're back to checking &lt;code&gt;"foobar"&lt;/code&gt; again. &lt;code&gt;false&lt;/code&gt;. Advance → &lt;strong&gt;idx = 3&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we're looping: 3 → 7 → 3 → 7, forever. The function never returns. The exact trigger is &lt;em&gt;"the first occurrence of the object is embedded in a longer word, and a later occurrence is standalone"&lt;/em&gt; — an input that's not exotic at all once real query strings are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The fix is to keep the two notions of position separate: a &lt;code&gt;base&lt;/code&gt; that tracks where the current search slice begins in absolute terms, and a &lt;code&gt;rel&lt;/code&gt; that's the relative result from &lt;code&gt;strings.Index&lt;/code&gt;. The absolute position is just &lt;code&gt;base + rel&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;objectIsQueried&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// strings.Index returns a relative offset within (*query)[base:], so we&lt;/span&gt;
        &lt;span class="c"&gt;// must add base to get the absolute position before passing it to&lt;/span&gt;
        &lt;span class="c"&gt;// isWholeWord (which operates on the original string).&lt;/span&gt;
        &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isWholeWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine lines changed, five removed. The slice-based search is still efficient (slicing a string in Go shares the underlying storage — no copy), but now every index handed to &lt;code&gt;isWholeWord&lt;/code&gt; is a true absolute position, and &lt;code&gt;base&lt;/code&gt; only ever moves forward. No more oscillation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning it with a regression test
&lt;/h2&gt;

&lt;p&gt;A fix you can't prove is a fix you'll lose. The change that actually matters long-term is the regression test, which encodes the exact input that used to hang so nobody can quietly reintroduce the bug later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Key regression case: first occurrence embedded, second standalone.&lt;/span&gt;
&lt;span class="c"&gt;// The original code looped forever on this input.&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"foobar foo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;alongside the ordinary cases (&lt;code&gt;"foo"&lt;/code&gt; in &lt;code&gt;"select foo from bar"&lt;/code&gt; → true, &lt;code&gt;"foobar"&lt;/code&gt; → false, punctuation boundaries like &lt;code&gt;"(foo)"&lt;/code&gt; → true, empty query → false), plus a separate table-driven test for &lt;code&gt;isWholeWord&lt;/code&gt; itself. The point isn't coverage for its own sake — it's that the one input that broke the function is now a named, permanent test case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you slice before searching, every offset is relative.&lt;/strong&gt; &lt;code&gt;strings.Index(s[base:], x)&lt;/code&gt; does not return a position in &lt;code&gt;s&lt;/code&gt;. This is the kind of mistake that's invisible on a quick read and obvious the moment you trace a concrete input — which is the whole argument for tracing concrete inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hang is a bug, even without a crash.&lt;/strong&gt; No panic fired here. The only symptom would be a wedged goroutine. Code that loops on an external &lt;code&gt;Index&lt;/code&gt; result should always have a strictly-monotonic advance you can point to — here, proving &lt;code&gt;base&lt;/code&gt; always increases is the proof the loop terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading unfamiliar code is a skill worth practicing in public.&lt;/strong&gt; I found this by reading, not by hitting it in production. Open source is one of the few places you can do that on a real, used codebase and have your fix reviewed by people who maintain it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is up as a proposed change against KubeStellar — issue &lt;a href="https://github.com/kubestellar/kubestellar/issues/3848" rel="noopener noreferrer"&gt;#3848&lt;/a&gt; for the report, PR &lt;a href="https://github.com/kubestellar/kubestellar/pull/3849" rel="noopener noreferrer"&gt;#3849&lt;/a&gt; for the fix and tests. If you spend time in Go codebases, the relative-vs-absolute slice trap is worth keeping in your peripheral vision; it hides well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Vignan Nallani. Found a bug like this, or want to talk through how you'd have traced it differently? I'm always up for it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
