Skip to content

fix: let an application function bind to the session it is asking about - #395

Open
midwell wants to merge 1 commit into
omec-project:mainfrom
midwell:fix/ue-ip-change-binding
Open

midwell wants to merge 1 commit into
omec-project:mainfrom
midwell:fix/ue-ip-change-binding

Conversation

@midwell

@midwell midwell commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

An application function cannot bind to a PDU session. Two independent defects in the binding path,
either of which alone is enough, and one race that becomes reachable as soon as the stored address
can move.

The slice comparison discards every session

SMPolicyFindByIdentifiersIpv4 and SMPolicyFindByIdentifiersIpv6 filter on the slice:

if sNssai != nil && !reflect.DeepEqual(sNssai, policyContext.SliceInfo) {
    continue
}

sNssai is a *models.Snssai, taken from AppSessionContextReqData.SliceInfo. policyContext.SliceInfo
is a value models.Snssai. reflect.DeepEqual is documented false for values of distinct types
whatever they hold, so the guard continued on every session it walked.

SliceInfo on an AppSessionContext lets an application function that knows its own S-NSSAI say
which of a subscriber's sessions it means, so an AF integrated with a single slice — the ordinary
case — could not bind to anything, whatever address it named. Both sites now dereference before
comparing, and the filter still rejects a different slice, which a test pins alongside the DNN filter
so the fix is not mistaken for removing the check.

Snssai is safe to compare this way once the outer mismatch is gone: its Sd is a *string, and
DeepEqual follows pointers rather than comparing identity, so two logically equal slices built from
different allocations still match.

The released address was compared by pointer identity

In the UE_IP_CH arm:

if request.RelIpv4Address == smPolicyContext.Ipv4Address {

Both are *string off a decoded request body, so they are never the same pointer however the
addresses compare. The release was dead in every case, and a report that only released an address
left the session bound to one the SMF had already given back — which is in the allocation pool again
and can be handed to a different subscriber, at which point the stale key binds the wrong session.
Now compared by value, and guarded against the empty string so releasing nothing releases nothing.

The address fields had no lock in common with the finders

The two address fields are now written under SmPolicyDataMu, which is what the four finders read
them under. The update handler previously held nothing.

Nothing sends an SM policy association update to this PCF today, so the write is unreachable and the
race is latent. It stops being latent the moment an SMF reports UE_IP_CH — a defined policy control
request trigger that this PCF already advertises in the 0x40780f bitmask it sets on every decision,
and one I intend to make an SMF actually send in a follow-up. Session binding and that report are two
HTTP handlers on two goroutines.

SmPolicyDataMu rather than a new mutex, because finding a session by address is one operation and
not two: the finders walk the map and compare the addresses in the same critical section, so
protecting the walk and the comparison with different things would be no protection. Its doc comment
now says it covers the addresses.

One thing worth a maintainer's eye: #392 is open against this repo and introduces a PolicyMu on
the same struct, documented there as guarding PolicyDecision and what is reachable through it — not
PolicyContext, so the two do not overlap by contract.

In this tree there is no cycle: every SmPolicyDataMu critical section is a leaf. I checked what
happens once #392 lands as well, rather than leaving it as an open question. Its PolicyMu wraps the
whole of updateSmPolicyContextProcedure, so this PR's SmPolicyDataMu.Lock() in the UE_IP_CH
case would nest inside it, giving PolicyMuSmPolicyDataMu. That is consistent with every other
SmPolicyDataMu site — the four finders, both map-mutation sites, and the poll loop, which releases
SmPolicyDataMu before it touches anything PolicyMu guards — so the combined order is the same
everywhere and there is still no cycle. Whichever of the two merges second should keep it that way.

The two also overlap at file level, in context/ue.go and producer/smpolicy.go, at different
hunks; whichever merges second needs a straightforward rebase. #390 and #391 are disjoint from both.

Scope

Two root causes in one pull request, because they are prerequisites for each other's tests: the
UE_IP_CH tests reach the assertion only once the slice comparison works, and the slice test needs a
session the update path can move. Splitting them leaves one half's tests failing without the other
half. If you would rather they were separate, the context/ue.go fix stands alone and I will land it
first and rebase this onto it.

Verification

  • GOWORK=off go test ./... -race: all packages pass.
  • pre-commit run --all-files with GOWORK=off exported — without it the Go hooks fail before they
    reach this diff, because the ambient root go.work in my checkout pulls in a sibling module that
    needs a newer Go than the workspace pins. Nine hooks, all pass (gitleaks, gci, staticcheck,
    go test -race, golangci-lint v2.13.2, yamlfmt, end-of-file, trailing whitespace, REUSE). Run
    natively on darwin/arm64.
  • Each fix mutation-verified on its own. Reverting the dereference fails three tests; reverting the
    pointer comparison fails the release test; removing the lock makes -race report DATA RACE
    between the write in the trigger arm and the read inside GetIpv4Address() in the finder.
  • The mutations were run in both orders, which mattered: with the slice comparison still broken,
    TestUeIpChangeReleaseClearsTheStoredAddress passes for the wrong reason — the finder returns nil
    because the guard skipped everything, not because the address was cleared. It pins the pointer
    defect only once the slice fix is in place.

🤖 Generated with Claude Code

Session binding matches an AppSessionContext against a session's stored UE
address and slice. Two defects in that path, either of which alone stops a bind
from ever succeeding, and one race that appears as soon as the address can move.

SMPolicyFindByIdentifiersIpv4 and its IPv6 twin compare the caller's *Snssai
against SmPolicyContextData.SliceInfo, which is an Snssai. reflect.DeepEqual is
false for values of distinct types whatever they hold, so the guard discarded
every session it walked. SliceInfo on an AppSessionContext lets an application
function that knows its own S-NSSAI say which of a subscriber's sessions it
means, so an AF integrated with one slice -- the ordinary case -- could not bind
to anything, whatever address it supplied. Both are dereferenced before the
comparison, and the filter still rejects a different slice.

The UE_IP_CH arm compared the released address by pointer identity rather than
by value. Both sides are *string off a decoded request body, so they are never
the same pointer however the addresses compare, and the release was dead in
every case. A report that only released an address left the session bound to one
the SMF had already given back -- and that address is in the pool again, so it
can be handed to a different subscriber and the stale key becomes valid for the
wrong session.

Those two address fields are also now written under SmPolicyDataMu, which is
what the four finders read them under. Nothing sends an SM policy update today,
so the write is unreachable and the race is latent; it stops being latent the
moment an SMF reports UE_IP_CH, which is a defined trigger the PCF already
advertises. Session binding and the SMF's report are two HTTP handlers on two
goroutines, and they had no lock in common: the finders held SmPolicyDataMu, the
update handler held nothing. The lock's contract is documented to cover the
addresses now, because finding a session by address is one operation -- the walk
and the comparison have to be protected by the same thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Edvin Lindqvist <edvin.lindqvist@forsway.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant