CVE-2026-8779
Description
A vulnerability was determined in omec-project amf up to 2.1.3-dev. Impacted is the function NGSetupRequest of the file ngap/handler.go. Executing a manipulation of the argument InformationElement can lead to memory corruption. The attack can be launched remotely. The exploit has been publicly disclosed and may be utilized. Upgrading to version 2.2.0 is recommended to address this issue. The affected component should be upgraded. The same pull request fixes multiple security issues.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A malformed NGSetupRequest with no valid InformationElement causes a nil pointer dereference, crashing the AMF in omec-project AMF up to v2.1.3-dev.
Vulnerability
The vulnerability resides in the function HandleNGSetupRequest within the file ngap/handler.go of the omec-project Access and Mobility Management Function (AMF). A malformed NGSetupRequest message that contains no valid InformationElement triggers an invalid memory address or nil pointer dereference, leading to memory corruption. The issue affects AMF versions up to and including v2.1.3-dev. The AMF is a 5G core network control plane function that terminates N2 and NAS signaling, manages registration, connection, and mobility, among other tasks [1][2].
Exploitation
An attacker with network access to the AMF can remotely send a crafted NGSetupRequest message lacking valid InformationElements. The specific hex payload provided in the issue (e.g., 00 15 00 2D 00 00 00 FF FF 00 06 00 F9 38 90 00 00 00 52 40 02 81 00 00 65 00 0C 01 90 72 58 C0 0F 93 89 00 00 A6 C0 00 15 40 01 20 00 01 00 01 80) causes the AMF to panic and crash with a segmentation violation. No authentication or special privileges are required, and the exploit has been publicly disclosed [2].
Impact
Successful exploitation results in a denial-of-service condition by crashing the AMF process. The crash terminates NAS signaling, registration, mobility management, and other critical 5G core functions served by the AMF. The confidentiality and integrity of data are not directly compromised, but network availability is severely impacted. The race window is not a factor; the crash occurs immediately upon handling the malformed message [2][3].
Mitigation
The issue has been fixed in AMF release v2.2.0, which includes nil guards for the affected NGAP handlers via pull request #666 [3][4]. The advisory also confirms that version 2.2.1 contains the fix [2]. Users should upgrade to v2.2.0 or later. No workaround is available for unpatched versions, and upgrading the affected component is recommended. The vulnerability is not listed on the CISA KEV catalog as of the publication date [1][2][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
134bc6724acc9Address multiple cases of crashes due to malformed NGAP input (#666)
12 files changed · +431 −29
.github/workflows/main.yml+1 −0 modified@@ -115,6 +115,7 @@ jobs: repository-projects: read security-events: write statuses: read + vulnerability-alerts: read uses: omec-project/.github/.github/workflows/scorecard-analysis.yml@0bce626c84023cadf638ceff2af19c73a7201bf8 # v0.0.26 with: branch_name: ${{ github.ref }}
gmm/handler.go+14 −2 modified@@ -571,7 +571,13 @@ func HandleRegistrationRequest(ctx ctxt.Context, ue *context.AmfUe, anType model if err != nil { return fmt.Errorf("decode SUCI failed: %w", err) } - ue.PlmnId = util.PlmnIdStringToModels(plmnId) + plmnID, err := util.PlmnIdStringToModels(plmnId) + if err != nil { + err = fmt.Errorf("invalid SUCI: %w", err) + ue.GmmLog.Errorln(err) + return err + } + ue.PlmnId = plmnID ue.GmmLog.Debugf("SUCI: %s", ue.Suci) case nasMessage.MobileIdentity5GSType5gGuti: guamiFromUeGutiTmp, guti := nasConvert.GutiToString(mobileIdentity5GSContents) @@ -1521,7 +1527,13 @@ func HandleIdentityResponse(ue *context.AmfUe, identityResponse *nasMessage.Iden if err != nil { return fmt.Errorf("decode SUCI failed: %w", err) } - ue.PlmnId = util.PlmnIdStringToModels(plmnId) + plmnID, err := util.PlmnIdStringToModels(plmnId) + if err != nil { + err = fmt.Errorf("invalid SUCI: %w", err) + ue.GmmLog.Errorln(err) + return err + } + ue.PlmnId = plmnID ue.GmmLog.Debugf("get SUCI: %s", ue.Suci) case nasMessage.MobileIdentity5GSType5gGuti: if ue.MacFailed {
gmm/handler_test.go+15 −0 modified@@ -24,6 +24,21 @@ import ( "go.uber.org/zap" ) +func TestHandleIdentityResponseRejectsUndecodableSuci(t *testing.T) { + ue := &context.AmfUe{GmmLog: zap.NewNop().Sugar()} + identityResponse := nasMessage.NewIdentityResponse(0) + identityResponse.SetLen(1) + identityResponse.Buffer[0] = nasMessage.MobileIdentity5GSTypeSuci + + err := HandleIdentityResponse(ue, identityResponse) + if err == nil { + t.Fatal("expected malformed SUCI identity response to fail") + } + if !strings.Contains(err.Error(), "decode SUCI failed") { + t.Fatalf("expected SUCI decode failure, got %v", err) + } +} + func newFuzzUE(fd *FuzzData) *context.AmfUe { ue := &context.AmfUe{ GmmLog: zap.NewNop().Sugar(),
metrics/telemetry.go+21 −0 modified@@ -12,7 +12,9 @@ package metrics import ( + "encoding/hex" "net/http" + "unicode/utf8" "github.com/omec-project/amf/logger" "github.com/prometheus/client_golang/prometheus" @@ -71,10 +73,29 @@ func InitMetrics() { // IncrementNgapMsgStats increments message level stats func IncrementNgapMsgStats(amfID, msgType, direction, result, reason string) { + amfID = sanitizeLabelValue(amfID) + msgType = sanitizeLabelValue(msgType) + direction = sanitizeLabelValue(direction) + result = sanitizeLabelValue(result) + reason = sanitizeLabelValue(reason) amfStats.ngapMsg.WithLabelValues(amfID, msgType, direction, result, reason).Inc() } +// sanitizeLabelValue ensures a string is valid UTF-8 for use as Prometheus label value +func sanitizeLabelValue(s string) string { + if utf8.ValidString(s) { + return s + } + // If not valid UTF-8, convert to hex representation with prefix + // to make it clear this is sanitized data + return "hex:" + hex.EncodeToString([]byte(s)) +} + // SetGnbSessProfileStats maintains Session profile info func SetGnbSessProfileStats(id, ip, state, tac string, count uint64) { + id = sanitizeLabelValue(id) + ip = sanitizeLabelValue(ip) + state = sanitizeLabelValue(state) + tac = sanitizeLabelValue(tac) amfStats.gnbSessionProfile.WithLabelValues(id, ip, state, tac).Set(float64(count)) }
nas/nas_security/security.go+4 −0 modified@@ -185,6 +185,10 @@ func FetchUeContextWithMobileIdentity(payload []byte) *context.AmfUe { } } else if msg.GmmHeader.GetMessageType() == nas.MsgTypeDeregistrationRequestUEOriginatingDeregistration { mobileIdentity5GSContents := msg.DeregistrationRequestUEOriginatingDeregistration.GetMobileIdentity5GSContents() + if len(mobileIdentity5GSContents) == 0 { + logger.CommLog.Errorln("MobileIdentity5GSContents is empty") + return nil + } if nasMessage.MobileIdentity5GSType5gGuti == nasConvert.GetTypeOfIdentity(mobileIdentity5GSContents[0]) { _, guti = nasConvert.GutiToString(mobileIdentity5GSContents) logger.CommLog.Debugf("Guti received in Deregistraion Request Message: %v", guti)
ngap/dispatcher.go+39 −3 modified@@ -30,6 +30,11 @@ import ( var tracer = otel.Tracer("amf/ngap") func DispatchLb(ctx ctxt.Context, sctplbMsg *sdcoreAmfServer.SctplbMessage, Amf2RanMsgChan chan *sdcoreAmfServer.AmfMessage) { + if sctplbMsg == nil { + logger.NgapLog.Errorln("dispatchLb received nil SCTP LB message") + return + } + logger.NgapLog.Infof("dispatchLb GnbId:%v GnbIp: %v %T", sctplbMsg.GnbId, sctplbMsg.GnbIpAddr, Amf2RanMsgChan) var ran *context.AmfRan amfSelf := context.AMF_Self() @@ -53,6 +58,11 @@ func DispatchLb(ctx ctxt.Context, sctplbMsg *sdcoreAmfServer.SctplbMessage, Amf2 logger.NgapLog.Infoln("dispatchLb, Create new Amf RAN with GnbIpAddress", sctplbMsg.GnbIpAddr) } + if ran == nil { + logger.NgapLog.Errorln("dispatchLb could not resolve or create RAN context") + return + } + if len(sctplbMsg.Msg) == 0 { logger.NgapLog.Infof("dispatchLb, Message of size 0 - ", sctplbMsg.GnbId) ran.Log.Infoln("RAN close the connection") @@ -122,6 +132,11 @@ func DispatchLb(ctx ctxt.Context, sctplbMsg *sdcoreAmfServer.SctplbMessage, Amf2 } func Dispatch(conn net.Conn, msg []byte) { + if conn == nil { + logger.NgapLog.Errorln("dispatch received nil connection") + return + } + var ran *context.AmfRan amfSelf := context.AMF_Self() @@ -175,6 +190,15 @@ func NgapMsgHandler(ue *context.AmfUe, msg context.NgapMsg) { } func DispatchNgapMsg(ctx ctxt.Context, ran *context.AmfRan, pdu *ngapType.NGAPPDU, sctplbMsg *sdcoreAmfServer.SctplbMessage) { + if pdu == nil { + if ran != nil && ran.Log != nil { + ran.Log.Errorln("DispatchNgapMsg received nil NGAP PDU") + } else { + logger.NgapLog.Errorln("DispatchNgapMsg received nil NGAP PDU") + } + return + } + var code int64 switch pdu.Present { case ngapType.NGAPPDUPresentInitiatingMessage: @@ -219,7 +243,11 @@ func DispatchNgapMsg(ctx ctxt.Context, ran *context.AmfRan, pdu *ngapType.NGAPPD case ngapType.NGAPPDUPresentInitiatingMessage: initiatingMessage := pdu.InitiatingMessage if initiatingMessage == nil { - ran.Log.Errorln("Initiating Message is nil") + if ran != nil && ran.Log != nil { + ran.Log.Errorln("Initiating Message is nil") + } else { + logger.NgapLog.Errorln("Initiating Message is nil") + } return } @@ -281,7 +309,11 @@ func DispatchNgapMsg(ctx ctxt.Context, ran *context.AmfRan, pdu *ngapType.NGAPPD case ngapType.NGAPPDUPresentSuccessfulOutcome: successfulOutcome := pdu.SuccessfulOutcome if successfulOutcome == nil { - ran.Log.Errorln("successful Outcome is nil") + if ran != nil && ran.Log != nil { + ran.Log.Errorln("successful Outcome is nil") + } else { + logger.NgapLog.Errorln("successful Outcome is nil") + } return } metrics.IncrementNgapMsgStats(context.AMF_Self().NfId, @@ -316,7 +348,11 @@ func DispatchNgapMsg(ctx ctxt.Context, ran *context.AmfRan, pdu *ngapType.NGAPPD case ngapType.NGAPPDUPresentUnsuccessfulOutcome: unsuccessfulOutcome := pdu.UnsuccessfulOutcome if unsuccessfulOutcome == nil { - ran.Log.Errorln("unsuccessful Outcome is nil") + if ran != nil && ran.Log != nil { + ran.Log.Errorln("unsuccessful Outcome is nil") + } else { + logger.NgapLog.Errorln("unsuccessful Outcome is nil") + } return } metrics.IncrementNgapMsgStats(context.AMF_Self().NfId,
ngap/dispatcher_test.go+41 −0 added@@ -0,0 +1,41 @@ +// Copyright (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package ngap + +import ( + ctxt "context" + "testing" + + "github.com/omec-project/amf/context" + "github.com/omec-project/amf/protos/sdcoreAmfServer" + "go.uber.org/zap" +) + +func TestDispatchLbIgnoresMissingRanContext(t *testing.T) { + msg := &sdcoreAmfServer.SctplbMessage{ + Msg: []byte{0x00}, + } + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("DispatchLb panicked with missing RAN identity: %v", recovered) + } + }() + + DispatchLb(ctxt.Background(), msg, make(chan *sdcoreAmfServer.AmfMessage, 1)) +} + +func TestDispatchNgapMsgIgnoresNilPdu(t *testing.T) { + ran := &context.AmfRan{Log: zap.NewNop().Sugar()} + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("DispatchNgapMsg panicked with nil PDU: %v", recovered) + } + }() + + DispatchNgapMsg(ctxt.Background(), ran, nil, nil) +}
ngap/handler.go+125 −21 modified@@ -32,6 +32,22 @@ import ( mi "github.com/omec-project/util/metricinfo" ) +func findRanUeByRanNgapID(ran *context.AmfRan, ranUENGAPID *ngapType.RANUENGAPID) *context.RanUe { + if ranUENGAPID == nil { + ran.Log.Errorln("RANUENGAPID is nil") + return nil + } + return ran.RanUeFindByRanUeNgapID(ranUENGAPID.Value) +} + +func findRanUeByAmfNgapID(ran *context.AmfRan, aMFUENGAPID *ngapType.AMFUENGAPID) *context.RanUe { + if aMFUENGAPID == nil { + ran.Log.Errorln("AMFUENGAPID is nil") + return nil + } + return context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) +} + func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context.RanUe, *ngapType.AMFUENGAPID) { amfSelf := context.AMF_Self() @@ -82,11 +98,11 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context ran.Log.Debugln("decode IE 5G-S-TMSI") } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) if ranUe == nil { var err error - if fiveGSTMSI != nil { + if fiveGSTMSI != nil && rANUENGAPID != nil { servedGuami := amfSelf.ServedGuamiList[0] // <5G-S-TMSI> := <AMF Set ID><AMF Pointer><5G-TMSI> @@ -134,7 +150,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeHandoverCancel: ngapMsg := initiatingMessage.Value.HandoverCancel for i := 0; i < len(ngapMsg.ProtocolIEs.List); i++ { @@ -151,7 +167,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeUEContextReleaseRequest: ngapMsg := initiatingMessage.Value.UEContextReleaseRequest for i := 0; i < len(ngapMsg.ProtocolIEs.List); i++ { @@ -173,9 +189,9 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) if ranUe == nil { - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) } case ngapType.ProcedureCodeNASNonDeliveryIndication: ngapMsg := initiatingMessage.Value.NASNonDeliveryIndication @@ -193,7 +209,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeLocationReportingFailureIndication: case ngapType.ProcedureCodeErrorIndication: case ngapType.ProcedureCodeUERadioCapabilityInfoIndication: @@ -212,7 +228,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeHandoverNotification: ngapMsg := initiatingMessage.Value.HandoverNotify @@ -266,7 +282,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodePathSwitchRequest: ngapMsg := initiatingMessage.Value.PathSwitchRequest @@ -282,7 +298,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) case ngapType.ProcedureCodeLocationReport: case ngapType.ProcedureCodeUplinkUEAssociatedNRPPaTransport: case ngapType.ProcedureCodeUplinkRANConfigurationTransfer: @@ -300,7 +316,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) case ngapType.ProcedureCodeCellTrafficTrace: case ngapType.ProcedureCodeUplinkRANStatusTransfer: case ngapType.ProcedureCodeUplinkNonUEAssociatedNRPPaTransport: @@ -329,7 +345,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) case ngapType.ProcedureCodePDUSessionResourceRelease: ngapMsg := successfulOutcome.Value.PDUSessionResourceReleaseResponse @@ -347,7 +363,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeUERadioCapabilityCheck: case ngapType.ProcedureCodeAMFConfigurationUpdate: @@ -367,7 +383,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeUEContextModification: ngapMsg := successfulOutcome.Value.UEContextModificationResponse @@ -385,7 +401,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodePDUSessionResourceSetup: ngapMsg := successfulOutcome.Value.PDUSessionResourceSetupResponse @@ -403,7 +419,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodePDUSessionResourceModify: ngapMsg := successfulOutcome.Value.PDUSessionResourceModifyResponse @@ -437,7 +453,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) } case ngapType.NGAPPDUPresentUnsuccessfulOutcome: unsuccessfulOutcome := message.UnsuccessfulOutcome @@ -463,7 +479,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeUEContextModification: ngapMsg := unsuccessfulOutcome.Value.UEContextModificationFailure @@ -481,7 +497,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context aMFUENGAPID = ie.Value.AMFUENGAPID } } - ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) + ranUe = findRanUeByRanNgapID(ran, rANUENGAPID) case ngapType.ProcedureCodeHandoverResourceAllocation: ngapMsg := unsuccessfulOutcome.Value.HandoverFailure @@ -497,7 +513,7 @@ func FetchRanUeContext(ran *context.AmfRan, message *ngapType.NGAPPDU) (*context } } } - ranUe = context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) + ranUe = findRanUeByAmfNgapID(ran, aMFUENGAPID) } } return ranUe, aMFUENGAPID @@ -581,6 +597,13 @@ func HandleNGSetupRequest(ran *context.AmfRan, message *ngapType.NGAPPDU) { if pagingDRX != nil { ran.Log.Debugf("PagingDRX[%d]", pagingDRX.Value) } + if supportedTAList == nil { + ran.Log.Warnln("NG-Setup failure: SupportedTAList is missing") + cause.Present = ngapType.CausePresentMisc + cause.Misc = &ngapType.CauseMisc{Value: ngapType.CauseMiscPresentUnspecified} + ngap_message.SendNGSetupFailure(ran, cause) + return + } // Clearing any existing contents of ran.SupportedTAList if len(ran.SupportedTAList) != 0 { @@ -809,6 +832,10 @@ func HandleNGReset(ran *context.AmfRan, message *ngapType.NGAPPDU) { } } } + if resetType == nil { + ran.Log.Errorln("ResetType is missing") + return + } printAndGetCause(ran, cause) @@ -850,6 +877,7 @@ func HandleNGReset(ran *context.AmfRan, message *ngapType.NGAPPDU) { if ueAssociatedLogicalNGConnectionItem.RANUENGAPID != nil { ran.Log.Warnf("RanUeNgapID[%d]", ueAssociatedLogicalNGConnectionItem.RANUENGAPID.Value) } + continue } err := ranUe.Remove() @@ -1358,6 +1386,10 @@ func HandleUERadioCapabilityCheckResponse(ran *context.AmfRan, message *ngapType ran.Log.Debugln("decode IE CriticalityDiagnostics") } } + if rANUENGAPID == nil { + ran.Log.Errorln("RanUeNgapID is missing") + return + } ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) if ranUe == nil { @@ -2245,6 +2277,14 @@ func HandlePDUSessionResourceModifyIndication(ctx ctxt.Context, ran *context.Amf ngap_message.SendErrorIndication(ran, nil, nil, nil, &criticalityDiagnostics) return } + if rANUENGAPID == nil { + ran.Log.Errorln("RanUeNgapID is missing") + return + } + if pduSessionResourceModifyIndicationList == nil { + ran.Log.Errorln("PDUSessionResourceModifyListModInd is missing") + return + } ranUe = ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) if ranUe == nil { @@ -2277,6 +2317,7 @@ func HandlePDUSessionResourceModifyIndication(ctx ctxt.Context, ran *context.Amf smContext, ok := amfUe.SmContextFindByPDUSessionID(pduSessionID) if !ok { ranUe.Log.Errorf("SmContext[PDU Session ID:%d] not found", pduSessionID) + continue } response, errResponse, _, err := consumer.SendUpdateSmContextN2Info(ctx, amfUe, smContext, models.N2SmInfoType_PDU_RES_MOD_IND, transfer) @@ -3012,6 +3053,14 @@ func HandleHandoverNotify(ctx ctxt.Context, ran *context.AmfRan, message *ngapTy } } } + if aMFUENGAPID == nil { + ran.Log.Errorln("AMFUENGAPID is missing") + return + } + if rANUENGAPID == nil { + ran.Log.Errorln("RANUENGAPID is missing") + return + } targetUe := ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) @@ -3324,12 +3373,17 @@ func HandleHandoverRequestAcknowledge(ctx ctxt.Context, ran *context.AmfRan, mes criticalityDiagnostics := buildCriticalityDiagnostics(&procedureCode, &triggeringMessage, &procedureCriticality, &iesCriticalityDiagnostics) ngap_message.SendErrorIndication(ran, nil, nil, nil, &criticalityDiagnostics) + return } if criticalityDiagnostics != nil { printCriticalityDiagnostics(ran, criticalityDiagnostics) } + if aMFUENGAPID == nil { + ran.Log.Errorln("AMFUENGAPID is missing") + return + } targetUe := context.AMF_Self().RanUeFindByAmfUeNgapID(aMFUENGAPID.Value) if targetUe == nil { ran.Log.Errorf("No UE Context[AMFUENGAPID: %d]", aMFUENGAPID.Value) @@ -4046,6 +4100,13 @@ func HandleRanConfigurationUpdate(ran *context.AmfRan, message *ngapType.NGAPPDU ran.Log.Debugf("decode IE PagingDRX = [%d]", pagingDRX.Value) } } + if supportedTAList == nil { + ran.Log.Warnln("RanConfigurationUpdate failure: Supported TA List is missing") + cause.Present = ngapType.CausePresentMisc + cause.Misc = &ngapType.CauseMisc{Value: ngapType.CauseMiscPresentUnspecified} + ngap_message.SendRanConfigurationUpdateFailure(ran, cause, nil) + return + } for i := 0; i < len(supportedTAList.List); i++ { supportedTAItem := supportedTAList.List[i] @@ -4380,6 +4441,18 @@ func HandleLocationReport(ran *context.AmfRan, message *ngapType.NGAPPDU) { } } } + if rANUENGAPID == nil { + ran.Log.Errorln("RanUeNgapID is missing") + return + } + if userLocationInformation == nil { + ran.Log.Warnln("userLocationInformation is missing") + return + } + if locationReportingRequestType == nil { + ran.Log.Warnln("LocationReportingRequestType is missing") + return + } ranUe := ran.RanUeFindByRanUeNgapID(rANUENGAPID.Value) if ranUe == nil { @@ -4400,6 +4473,10 @@ func HandleLocationReport(ran *context.AmfRan, message *ngapType.NGAPPDU) { case ngapType.EventTypePresentUePresenceInAreaOfInterest: ranUe.Log.Debugln("to report UE presence in the area of interest") + if uEPresenceInAreaOfInterestList == nil { + ran.Log.Warnln("UEPresenceInAreaOfInterestList is missing") + return + } for _, uEPresenceInAreaOfInterestItem := range uEPresenceInAreaOfInterestList.List { uEPresence := uEPresenceInAreaOfInterestItem.UEPresence.Value referenceID := uEPresenceInAreaOfInterestItem.LocationReportingReferenceID.Value @@ -4818,27 +4895,54 @@ func HandleCellTrafficTrace(ran *context.AmfRan, message *ngapType.NGAPPDU) { } func printAndGetCause(ran *context.AmfRan, cause *ngapType.Cause) (present int, value aper.Enumerated) { + if ran == nil { + return 0, 0 + } + if cause == nil { + ran.Log.Errorln("Cause is nil") + return 0, 0 + } present = cause.Present switch cause.Present { case ngapType.CausePresentRadioNetwork: + if cause.RadioNetwork == nil { + ran.Log.Errorln("Cause RadioNetwork is nil") + return present, 0 + } ran.Log.Warnf("Cause RadioNetwork[%d]", cause.RadioNetwork.Value) value = cause.RadioNetwork.Value case ngapType.CausePresentTransport: + if cause.Transport == nil { + ran.Log.Errorln("Cause Transport is nil") + return present, 0 + } ran.Log.Warnf("Cause Transport[%d]", cause.Transport.Value) value = cause.Transport.Value case ngapType.CausePresentProtocol: + if cause.Protocol == nil { + ran.Log.Errorln("Cause Protocol is nil") + return present, 0 + } ran.Log.Warnf("Cause Protocol[%d]", cause.Protocol.Value) value = cause.Protocol.Value case ngapType.CausePresentNas: + if cause.Nas == nil { + ran.Log.Errorln("Cause Nas is nil") + return present, 0 + } ran.Log.Warnf("Cause Nas[%d]", cause.Nas.Value) value = cause.Nas.Value case ngapType.CausePresentMisc: + if cause.Misc == nil { + ran.Log.Errorln("Cause Misc is nil") + return present, 0 + } ran.Log.Warnf("Cause Misc[%d]", cause.Misc.Value) value = cause.Misc.Value default: ran.Log.Errorf("Invalid Cause group[%d]", cause.Present) } - return + return present, value } func printCriticalityDiagnostics(ran *context.AmfRan, criticalityDiagnostics *ngapType.CriticalityDiagnostics) {
ngap/handler_test.go+116 −0 added@@ -0,0 +1,116 @@ +// Copyright (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package ngap + +import ( + ctxt "context" + "testing" + + "github.com/omec-project/amf/context" + "github.com/omec-project/ngap/ngapType" + "go.uber.org/zap" +) + +func TestHandleHandoverNotifyIgnoresMissingIDs(t *testing.T) { + ran := &context.AmfRan{Log: zap.NewNop().Sugar()} + pdu := &ngapType.NGAPPDU{ + Present: ngapType.NGAPPDUPresentInitiatingMessage, + InitiatingMessage: &ngapType.InitiatingMessage{ + ProcedureCode: ngapType.ProcedureCode{Value: ngapType.ProcedureCodeHandoverNotification}, + Value: ngapType.InitiatingMessageValue{ + Present: ngapType.InitiatingMessagePresentHandoverNotify, + HandoverNotify: &ngapType.HandoverNotify{}, + }, + }, + } + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("HandleHandoverNotify panicked with missing IDs: %v", recovered) + } + }() + + HandleHandoverNotify(ctxt.Background(), ran, pdu) +} + +func TestHandleHandoverRequestAcknowledgeIgnoresMissingAmfUeNgapID(t *testing.T) { + ran := &context.AmfRan{Log: zap.NewNop().Sugar()} + pdu := &ngapType.NGAPPDU{ + Present: ngapType.NGAPPDUPresentSuccessfulOutcome, + SuccessfulOutcome: &ngapType.SuccessfulOutcome{ + ProcedureCode: ngapType.ProcedureCode{Value: ngapType.ProcedureCodeHandoverResourceAllocation}, + Value: ngapType.SuccessfulOutcomeValue{ + Present: ngapType.SuccessfulOutcomePresentHandoverRequestAcknowledge, + HandoverRequestAcknowledge: &ngapType.HandoverRequestAcknowledge{ + ProtocolIEs: ngapType.ProtocolIEContainerHandoverRequestAcknowledgeIEs{ + List: []ngapType.HandoverRequestAcknowledgeIEs{ + { + Id: ngapType.ProtocolIEID{Value: ngapType.ProtocolIEIDTargetToSourceTransparentContainer}, + Value: ngapType.HandoverRequestAcknowledgeIEsValue{ + Present: ngapType.HandoverRequestAcknowledgeIEsPresentTargetToSourceTransparentContainer, + TargetToSourceTransparentContainer: &ngapType.TargetToSourceTransparentContainer{}, + }, + }, + }, + }, + }, + }, + }, + } + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("HandleHandoverRequestAcknowledge panicked with missing AMFUENGAPID: %v", recovered) + } + }() + + HandleHandoverRequestAcknowledge(ctxt.Background(), ran, pdu) +} + +func TestFetchRanUeContextReturnsNilForMissingIDs(t *testing.T) { + self := context.AMF_Self() + previousReady := self.Rcvd + self.Rcvd = true + defer func() { + self.Rcvd = previousReady + }() + + ran := &context.AmfRan{Log: zap.NewNop().Sugar()} + pdu := &ngapType.NGAPPDU{ + Present: ngapType.NGAPPDUPresentInitiatingMessage, + InitiatingMessage: &ngapType.InitiatingMessage{ + ProcedureCode: ngapType.ProcedureCode{Value: ngapType.ProcedureCodeInitialUEMessage}, + Value: ngapType.InitiatingMessageValue{ + Present: ngapType.InitiatingMessagePresentInitialUEMessage, + InitialUEMessage: &ngapType.InitialUEMessage{}, + }, + }, + } + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("FetchRanUeContext panicked on missing IDs: %v", recovered) + } + }() + + ranUe, amfID := FetchRanUeContext(ran, pdu) + if ranUe != nil || amfID != nil { + t.Fatal("expected no UE context to be resolved from a malformed InitialUEMessage") + } +} + +func TestPrintAndGetCauseIgnoresNilCauseMembers(t *testing.T) { + ran := &context.AmfRan{Log: zap.NewNop().Sugar()} + + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("printAndGetCause panicked on malformed cause: %v", recovered) + } + }() + + printAndGetCause(ran, nil) + printAndGetCause(ran, &ngapType.Cause{Present: ngapType.CausePresentProtocol}) +}
util/convert.go+11 −2 modified@@ -40,10 +40,19 @@ func SeparateAmfId(amfid string) (regionId, setId, ptrId string, err error) { return } -func PlmnIdStringToModels(plmnId string) (plmnID models.PlmnId) { +func PlmnIdStringToModels(plmnId string) (plmnID models.PlmnId, err error) { + if len(plmnId) < 5 || len(plmnId) > 6 { + return models.PlmnId{}, fmt.Errorf("invalid PLMN ID length %d", len(plmnId)) + } + for _, digit := range plmnId { + if digit < '0' || digit > '9' { + return models.PlmnId{}, fmt.Errorf("invalid PLMN ID %q", plmnId) + } + } + plmnID.Mcc = plmnId[:3] plmnID.Mnc = plmnId[3:] - return + return plmnID, nil } func TACConfigToModels(intString string) (hexString string) {
util/convert_test.go+43 −0 added@@ -0,0 +1,43 @@ +// Copyright (C) 2026 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package util + +import "testing" + +func TestPlmnIdStringToModels(t *testing.T) { + tests := []struct { + name string + input string + wantMcc string + wantMnc string + wantError bool + }{ + {name: "valid two digit mnc", input: "12345", wantMcc: "123", wantMnc: "45"}, + {name: "valid three digit mnc", input: "123456", wantMcc: "123", wantMnc: "456"}, + {name: "empty", input: "", wantError: true}, + {name: "too short", input: "1234", wantError: true}, + {name: "too long", input: "1234567", wantError: true}, + {name: "non digits", input: "12a45", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + plmnID, err := PlmnIdStringToModels(tt.input) + if tt.wantError { + if err == nil { + t.Fatal("expected error") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if plmnID.Mcc != tt.wantMcc || plmnID.Mnc != tt.wantMnc { + t.Fatalf("expected MCC/MNC %s/%s, got %s/%s", tt.wantMcc, tt.wantMnc, plmnID.Mcc, plmnID.Mnc) + } + }) + } +}
VERSION+1 −1 modified@@ -1 +1 @@ -2.1.3-dev +2.2.0
Vulnerability mechanics
Root cause
"Missing nil-pointer checks on decoded NGAP Information Elements allow dereferencing nil pointers, leading to memory corruption (panic/crash)."
Attack vector
An attacker with network access to the AMF's NGAP endpoint sends a crafted NGAP message (e.g., NGSetupRequest, HandoverNotify, or any procedure handled by FetchRanUeContext) in which one or more mandatory Information Elements—such as SupportedTAList, RANUENGAPID, AMFUENGAPID, or Cause sub-fields—are omitted or set to nil. Because the code previously accessed `.Value` on these pointers without a nil check, the malformed input triggers a nil-pointer dereference, causing the AMF process to crash. The attack requires only low-privilege network access (CVSS PR:L) and no user interaction.
Affected code
The primary vulnerable function is `NGSetupRequest` in `ngap/handler.go`, along with `FetchRanUeContext`, `HandleNGReset`, `HandleHandoverNotify`, `HandleHandoverRequestAcknowledge`, `HandleLocationReport`, `HandlePDUSessionResourceModifyIndication`, `HandleUERadioCapabilityCheckResponse`, `HandleRanConfigurationUpdate`, `printAndGetCause`, `DispatchLb`, `Dispatch`, and `DispatchNgapMsg` in the same file and in `ngap/dispatcher.go`. All these functions lacked nil-pointer checks before dereferencing decoded NGAP Information Elements.
What the fix does
The patch introduces guard functions (`findRanUeByRanNgapID`, `findRanUeByAmfNgapID`) that check whether the ID pointer is nil before accessing `.Value`. It also adds explicit nil checks for `supportedTAList` in `HandleNGSetupRequest` and `HandleRanConfigurationUpdate`, for `resetType` in `HandleNGReset`, for `rANUENGAPID`/`aMFUENGAPID` in multiple handlers, and for each Cause sub-pointer in `printAndGetCause`. When a nil is detected, the handler logs the error and returns early (or sends a failure response) instead of proceeding to a dereference. These changes close all the nil-pointer dereference paths identified in the report [CWE-119][patch_id=424444].
Preconditions
- networkAttacker must be able to send NGAP messages to the AMF's SCTP endpoint.
- authAttacker needs low-privilege access (CVSS PR:L); no special authentication required beyond network connectivity.
- inputAttacker must craft an NGAP PDU that omits or sets to nil one or more mandatory Information Elements (e.g., SupportedTAList, RANUENGAPID, AMFUENGAPID, Cause sub-fields).
Reproduction
The bundle includes a public PoC in the form of test cases in `ngap/handler_test.go` and `ngap/dispatcher_test.go`. To reproduce, send an NGAP NGSetupRequest PDU that omits the `SupportedTAList` IE, or send a HandoverNotify PDU with no `AMFUENGAPID` or `RANUENGAPID` IEs. The AMF will panic with a nil-pointer dereference. The test functions `TestHandleHandoverNotifyIgnoresMissingIDs`, `TestHandleHandoverRequestAcknowledgeIgnoresMissingAmfUeNgapID`, `TestFetchRanUeContextReturnsNilForMissingIDs`, `TestDispatchLbIgnoresMissingRanContext`, and `TestDispatchNgapMsgIgnoresNilPdu` demonstrate the crash scenarios.
Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6News mentions
0No linked articles in our index yet.