Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SMPolicyFindByIdentifiersIpv4andSMPolicyFindByIdentifiersIpv6filter on the slice:sNssaiis a*models.Snssai, taken fromAppSessionContextReqData.SliceInfo.policyContext.SliceInfois a value
models.Snssai.reflect.DeepEqualis documented false for values of distinct typeswhatever they hold, so the guard
continued on every session it walked.SliceInfoon anAppSessionContextlets an application function that knows its own S-NSSAI saywhich 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.
Snssaiis safe to compare this way once the outer mismatch is gone: itsSdis a*string, andDeepEqualfollows pointers rather than comparing identity, so two logically equal slices built fromdifferent allocations still match.
The released address was compared by pointer identity
In the
UE_IP_CHarm:Both are
*stringoff a decoded request body, so they are never the same pointer however theaddresses 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 readthem 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 controlrequest trigger that this PCF already advertises in the
0x40780fbitmask 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.
SmPolicyDataMurather than a new mutex, because finding a session by address is one operation andnot 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
PolicyMuonthe same struct, documented there as guarding
PolicyDecisionand what is reachable through it — notPolicyContext, so the two do not overlap by contract.In this tree there is no cycle: every
SmPolicyDataMucritical section is a leaf. I checked whathappens once #392 lands as well, rather than leaving it as an open question. Its
PolicyMuwraps thewhole of
updateSmPolicyContextProcedure, so this PR'sSmPolicyDataMu.Lock()in theUE_IP_CHcase would nest inside it, giving
PolicyMu→SmPolicyDataMu. That is consistent with every otherSmPolicyDataMusite — the four finders, both map-mutation sites, and the poll loop, which releasesSmPolicyDataMubefore it touches anythingPolicyMuguards — so the combined order is the sameeverywhere 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.goandproducer/smpolicy.go, at differenthunks; 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_CHtests reach the assertion only once the slice comparison works, and the slice test needs asession 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.gofix stands alone and I will land itfirst and rebase this onto it.
Verification
GOWORK=off go test ./... -race: all packages pass.pre-commit run --all-fileswithGOWORK=offexported — without it the Go hooks fail before theyreach this diff, because the ambient root
go.workin my checkout pulls in a sibling module thatneeds 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). Runnatively on darwin/arm64.
pointer comparison fails the release test; removing the lock makes
-racereportDATA RACEbetween the write in the trigger arm and the read inside
GetIpv4Address()in the finder.TestUeIpChangeReleaseClearsTheStoredAddresspasses for the wrong reason — the finder returns nilbecause 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