fix: task params nil params panic (#25697)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2026-09-18 11:27:13 +08:00
committed by GitHub
parent 16a6c438e6
commit cd04311b14
15 changed files with 287 additions and 32 deletions

6
go.mod
View File

@@ -12,6 +12,7 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.18.16
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.4
github.com/aws/smithy-go v1.24.2
github.com/beevik/etree v1.1.0
github.com/benbjohnson/clock v1.0.0
github.com/bitly/go-simplejson v0.5.0
github.com/c-bata/go-prompt v0.2.4
@@ -51,6 +52,7 @@ require (
github.com/lestrrat-go/jwx v1.2.25
github.com/lestrrat/go-jwx v0.0.0-20180221005942-b7d4802280ae
github.com/libvirt/libvirt-go-xml v5.2.0+incompatible
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c
github.com/mark3labs/mcp-go v0.48.0
github.com/mattn/go-sqlite3 v1.14.19
github.com/mdlayher/arp v0.0.0-20190313224443-98a83c8a2717
@@ -114,7 +116,7 @@ require (
moul.io/http2curl/v2 v2.3.0
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260904120449-8aa2a5d6ba7a
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c
yunion.io/x/jsonutils v1.0.1-0.20260916031541-8e0c8de6eb2e
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900
yunion.io/x/pkg v1.10.4-0.20260722094907-0da70a14584f
@@ -190,7 +192,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/basgys/goxml2json v1.1.1-0.20181031222924-996d9fc8d313 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/benbjohnson/immutable v0.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.2.2 // indirect
@@ -287,7 +288,6 @@ require (
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lestrrat/go-pdebug v0.0.0-20180220043741-569c97477ae8 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect

4
go.sum
View File

@@ -1782,8 +1782,8 @@ yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260904120449-8aa2a5d6ba7a/go.mod h1:F
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005 h1:3sWwcjGXGjG9mLBWa7AyLq+QSi0udTAx21pfVQRFMBE=
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws=
yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634=
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c h1:FthZM1upg4/yF4vmYheo/J7a738sGPjWPKk5kY88sH0=
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c/go.mod h1:VK4Z93dgiKgAijcSqbMKmGaBMJuHulR16Hz4K015ZPo=
yunion.io/x/jsonutils v1.0.1-0.20260916031541-8e0c8de6eb2e h1:0M7SVtySPc+zyHm4BqJ5XZQx2dDL2fwHccR/QZrbUMg=
yunion.io/x/jsonutils v1.0.1-0.20260916031541-8e0c8de6eb2e/go.mod h1:VK4Z93dgiKgAijcSqbMKmGaBMJuHulR16Hz4K015ZPo=
yunion.io/x/log v0.0.0-20190514041436-04ce53b17c6b/go.mod h1:+gauLs73omeJAPlsXcevLsJLKixV+sR/E7WSYTSx1fE=
yunion.io/x/log v0.0.0-20190629062853-9f6483a7103d/go.mod h1:LC6f/4FozL0iaAbnFt2eDX9jlsyo3WiOUPm03d7+U4U=
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91 h1:inY5o3LDa/zgsIZuPN0HmpzKIsu/lLgsBmMttuDPGj4=

View File

@@ -951,7 +951,22 @@ func (task *STask) ClearPendingUsage(index int) error {
}
func (task *STask) GetParams() *jsonutils.JSONDict {
return task.Params
result := jsonutils.NewDict()
if task.Params == nil {
return result
}
copied := task.Params.DeepCopy()
copyParams, ok := copied.(*jsonutils.JSONDict)
if !ok || copyParams == nil {
return result
}
paramsJsonMap, _ := copyParams.GetMap()
for k, v := range paramsJsonMap {
if !strings.HasPrefix(k, "__") {
result.Set(k, v)
}
}
return result
}
func (task *STask) GetUserCred() mcclient.TokenCredential {

View File

@@ -0,0 +1,144 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package taskman
import (
"testing"
"yunion.io/x/jsonutils"
)
func TestSTaskGetParams(t *testing.T) {
newParams := func(pairs map[string]jsonutils.JSONObject) *jsonutils.JSONDict {
params := jsonutils.NewDict()
for k, v := range pairs {
params.Set(k, v)
}
return params
}
nested := jsonutils.NewDict()
nested.Set("ip", jsonutils.NewString("10.0.0.1"))
stages := jsonutils.NewArray(
jsonutils.NewDict(),
jsonutils.NewString("on_init"),
)
cases := []struct {
name string
params *jsonutils.JSONDict
want *jsonutils.JSONDict
}{
{
name: "nil_params",
params: nil,
want: jsonutils.NewDict(),
},
{
name: "empty_params",
params: jsonutils.NewDict(),
want: jsonutils.NewDict(),
},
{
name: "keep_public_keys",
params: newParams(map[string]jsonutils.JSONObject{
"parent_task_id": jsonutils.NewString("task-1"),
"auto_start": jsonutils.JSONTrue,
}),
want: newParams(map[string]jsonutils.JSONObject{
"parent_task_id": jsonutils.NewString("task-1"),
"auto_start": jsonutils.JSONTrue,
}),
},
{
name: "drop_internal_keys",
params: newParams(map[string]jsonutils.JSONObject{
"__stages": stages,
"__pending_usage__": jsonutils.NewDict(),
"__request_context": jsonutils.NewDict(),
"__parent_task_notifyurl": jsonutils.NewString("http://notify"),
}),
want: jsonutils.NewDict(),
},
{
name: "mixed_keys",
params: newParams(map[string]jsonutils.JSONObject{
"desc": nested,
"__stages": stages,
"_private": jsonutils.NewString("keep"),
"__": jsonutils.NewString("drop"),
"guest_id": jsonutils.NewString("g-1"),
}),
want: newParams(map[string]jsonutils.JSONObject{
"desc": nested,
"_private": jsonutils.NewString("keep"),
"guest_id": jsonutils.NewString("g-1"),
}),
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
task := &STask{}
task.Params = c.params
got := task.GetParams()
if got == nil {
t.Fatal("GetParams() returned nil, want empty dict")
}
if !got.Equals(c.want) {
t.Fatalf("GetParams() = %s, want %s", got, c.want)
}
})
}
}
func TestSTaskGetParamsDoesNotMutateOriginal(t *testing.T) {
orig := jsonutils.NewDict()
orig.Set("guest_id", jsonutils.NewString("g-1"))
orig.Set("__stages", jsonutils.NewArray(jsonutils.NewString("on_init")))
nested := jsonutils.NewDict()
nested.Set("ip", jsonutils.NewString("10.0.0.1"))
orig.Set("desc", nested)
task := &STask{STaskBase: STaskBase{Params: orig}}
got := task.GetParams()
got.Set("guest_id", jsonutils.NewString("g-2"))
got.Set("extra", jsonutils.JSONTrue)
desc, err := got.Get("desc")
if err != nil {
t.Fatalf("get desc: %v", err)
}
descDict, ok := desc.(*jsonutils.JSONDict)
if !ok {
t.Fatalf("desc type %T, want *jsonutils.JSONDict", desc)
}
descDict.Set("ip", jsonutils.NewString("10.0.0.2"))
if got.Contains("__stages") {
t.Fatal("GetParams() leaked internal key __stages")
}
guestId, _ := orig.GetString("guest_id")
if guestId != "g-1" {
t.Fatalf("original guest_id = %s, want g-1", guestId)
}
if !orig.Contains("__stages") {
t.Fatal("original __stages was removed")
}
origIp, _ := orig.GetString("desc", "ip")
if origIp != "10.0.0.1" {
t.Fatalf("original nested ip = %s, want 10.0.0.1", origIp)
}
}

View File

@@ -356,6 +356,7 @@ func (self *SBaremetalGuestDriver) RequestStartOnHost(ctx context.Context, guest
if params.Length() > 0 {
config.Add(params, "params")
}
log.Debugf("RequestStartOnHost config: %s", config.String())
headers := task.GetTaskRequestHeader()
url := fmt.Sprintf("/baremetals/%s/servers/%s/start", host.Id, guest.Id)
_, err := host.BaremetalSyncRequest(ctx, "POST", url, headers, config)

2
vendor/modules.txt vendored
View File

@@ -2672,7 +2672,7 @@ yunion.io/x/cloudmux/pkg/multicloud/zstack/provider
yunion.io/x/executor/apis
yunion.io/x/executor/client
yunion.io/x/executor/server
# yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c
# yunion.io/x/jsonutils v1.0.1-0.20260916031541-8e0c8de6eb2e
## explicit; go 1.18
yunion.io/x/jsonutils
# yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91

View File

@@ -20,10 +20,18 @@ import (
"time"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/sortedmap"
"yunion.io/x/pkg/util/timeutils"
)
func normalizeJSONObject(obj JSONObject) JSONObject {
if gotypes.IsNil(obj) {
return JSONNull
}
return obj
}
type JSONPair struct {
key string
val JSONObject
@@ -40,7 +48,7 @@ func NewDict(objs ...JSONPair) *JSONDict {
func NewArray(objs ...JSONObject) *JSONArray {
arr := JSONArray{data: make([]JSONObject, 0, len(objs))}
for _, o := range objs {
arr.data = append(arr.data, o)
arr.data = append(arr.data, normalizeJSONObject(o))
}
return &arr
}
@@ -74,7 +82,7 @@ func NewBool(val bool) *JSONBool {
}
func (this *JSONDict) Set(key string, value JSONObject) {
this.data = sortedmap.Add(this.data, key, value)
this.data = sortedmap.Add(this.data, key, normalizeJSONObject(value))
}
func (this *JSONDict) Remove(key string) bool {
@@ -130,12 +138,12 @@ func (this *JSONDict) Add(o JSONObject, keys ...string) error {
}
func (this *JSONArray) SetAt(idx int, obj JSONObject) {
this.data[idx] = obj
this.data[idx] = normalizeJSONObject(obj)
}
func (this *JSONArray) Add(objs ...JSONObject) {
for _, o := range objs {
this.data = append(this.data, o)
this.data = append(this.data, normalizeJSONObject(o))
}
}

View File

@@ -20,4 +20,7 @@ import (
const (
TAG_DEPRECATED_BY = reflectutils.TAG_DEPRECATED_BY
// maxParseDepth is the maximum nesting level accepted by the parser
maxParseDepth = 10000
)

View File

@@ -15,6 +15,7 @@
package jsonutils
import (
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/sortedmap"
)
@@ -51,7 +52,14 @@ func (arr *JSONArray) Equals(json JSONObject) bool {
return false
}
for i, v := range arr.data {
if !v.Equals(arr2.data[i]) {
oth := arr2.data[i]
if gotypes.IsNil(v) || gotypes.IsNil(oth) {
if gotypes.IsNil(v) != gotypes.IsNil(oth) {
return false
}
continue
}
if !v.Equals(oth) {
return false
}
}

View File

@@ -26,6 +26,10 @@ const (
ErrInvalidHex = errors.Error("invalid hex")
ErrInvalidRune = errors.Error("invalid 4 byte rune")
ErrInvalidNodeId = errors.Error("invalid node id")
ErrDuplicateNodeId = errors.Error("duplicate node id")
ErrNestedTooDeep = errors.Error("json nested too deep")
ErrTypeMismatch = errors.Error("unmarshal type mismatch")
ErrArrayLengthMismatch = errors.Error("unmarshal array length mismatch")
ErrInterfaceUnsupported = errors.Error("do not known how to deserialize json into this interface type")

View File

@@ -15,6 +15,7 @@
package jsonutils
import (
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/sortedmap"
)
@@ -41,6 +42,10 @@ func (self *JSONString) Interface() interface{} {
func (self *JSONArray) Interface() interface{} {
ret := make([]interface{}, len(self.data))
for i := 0; i < len(self.data); i += 1 {
if gotypes.IsNil(self.data[i]) {
ret[i] = nil
continue
}
ret[i] = self.data[i].Interface()
}
return ret
@@ -51,7 +56,12 @@ func (self *JSONDict) Interface() interface{} {
for iter := sortedmap.NewIterator(self.data); iter.HasMore(); iter.Next() {
k, v := iter.Get()
mapping[k] = v.(JSONObject).Interface()
jo, _ := v.(JSONObject)
if gotypes.IsNil(jo) {
mapping[k] = nil
continue
}
mapping[k] = jo.Interface()
}
return mapping

View File

@@ -259,7 +259,7 @@ func (s *sJsonParseSession) parseJSONValue(str []byte, offset int) (JSONObject,
return nil, i, errors.Wrap(e, "parseString")
} else if quote {
return &JSONString{data: val}, i, nil
} else if val[0] == '<' && val[len(val)-1] == '>' {
} else if len(val) > 1 && val[0] == '<' && val[len(val)-1] == '>' {
// Pointer <nnnn>
val = val[1 : len(val)-1]
ival, err := strconv.ParseInt(val, 10, 64)
@@ -476,7 +476,11 @@ func (s *sJsonParseSession) parseDict(str []byte, offset int) (sortedmap.SSorted
}
if key == jsonPointerKey {
// node id
nodeId = int(val.(*JSONInt).data)
jval, ok := val.(*JSONInt)
if !ok {
return smap, i, nodeId, errors.Wrap(ErrInvalidNodeId, jsonPointerKey)
}
nodeId = int(jval.data)
} else {
smap = sortedmap.Add(smap, key, val)
}
@@ -552,12 +556,20 @@ func (s *sJsonParseSession) parseArray(str []byte, offset int) ([]JSONObject, in
}
func (this *JSONDict) parse(s *sJsonParseSession, str []byte, offset int) (int, error) {
e := s.enter()
if e != nil {
return offset, errors.Wrap(e, "enter")
}
defer s.leave()
smap, i, nodeId, e := s.parseDict(str, offset)
if e == nil {
this.nodeId = nodeId
this.data = smap
if this.nodeId > 0 {
s.saveNode(nodeId, this)
e = s.saveNode(nodeId, this)
if e != nil {
return i, errors.Wrap(e, "saveNode")
}
}
return i, nil
}
@@ -594,14 +606,19 @@ func (this *JSONDict) prettyString(level int) string {
buffer.WriteByte('"')
buffer.WriteString(k)
buffer.WriteString("\":")
_, okdict := v.(*JSONDict)
_, okarray := v.(*JSONArray)
if okdict || okarray {
buffer.WriteByte('\n')
buffer.WriteString(v.prettyString(level + 2))
} else {
if gotypes.IsNil(v) {
buffer.WriteByte(' ')
buffer.WriteString(v.String())
buffer.WriteString("null")
} else {
_, okdict := v.(*JSONDict)
_, okarray := v.(*JSONArray)
if okdict || okarray {
buffer.WriteByte('\n')
buffer.WriteString(v.prettyString(level + 2))
} else {
buffer.WriteByte(' ')
buffer.WriteString(v.String())
}
}
idx++
}
@@ -614,6 +631,11 @@ func (this *JSONDict) prettyString(level int) string {
}
func (this *JSONArray) parse(s *sJsonParseSession, str []byte, offset int) (int, error) {
e := s.enter()
if e != nil {
return offset, errors.Wrap(e, "enter")
}
defer s.leave()
val, i, e := s.parseArray(str, offset)
if e == nil {
this.data = val
@@ -639,7 +661,12 @@ func (this *JSONArray) prettyString(level int) string {
buffer.WriteString(",")
}
buffer.WriteByte('\n')
buffer.WriteString(v.prettyString(level + 1))
if gotypes.IsNil(v) {
buffer.WriteString(tab)
buffer.WriteString(" null")
} else {
buffer.WriteString(v.prettyString(level + 1))
}
}
if len(this.data) > 0 {
buffer.WriteByte('\n')

View File

@@ -48,7 +48,7 @@ func (s *sJsonMarshalSession) marshalSlice(val reflect.Value, info *reflectutils
objs := make([]JSONObject, 0)
for i := 0; i < val.Len(); i += 1 {
val := s.marshalValue(val.Index(i), nil, omitEmpty)
if val != nil {
if !gotypes.IsNil(val) {
objs = append(objs, val)
}
}
@@ -84,7 +84,7 @@ func (s *sJsonMarshalSession) marshalMap(val reflect.Value, info *reflectutils.S
for i := 0; i < len(keys); i += 1 {
key := keys[i]
val := s.marshalValue(val.MapIndex(key), nil, omitEmpty)
if val != nil {
if !gotypes.IsNil(val) {
objPairs = append(objPairs, JSONPair{key: fmt.Sprintf("%s", key), val: val})
}
}
@@ -133,7 +133,7 @@ func (s *sJsonMarshalSession) struct2JSONPairs(val reflect.Value, omitEmpty bool
continue
}
val := s.marshalValue(fields[i].Value, jsonInfo, omitEmpty)
if val != nil {
if !gotypes.IsNil(val) {
objPair := JSONPair{key: key, val: val}
objPairs = append(objPairs, objPair)
}

View File

@@ -15,7 +15,7 @@
package jsonutils
import (
"fmt"
"yunion.io/x/pkg/errors"
)
type sNodeReferer struct {
@@ -25,6 +25,9 @@ type sNodeReferer struct {
type sJsonParseSession struct {
objectMap map[int]*sNodeReferer
// depth is the current nesting level of the object/array being parsed
depth int
}
func newJsonParseSession() *sJsonParseSession {
@@ -33,6 +36,20 @@ func newJsonParseSession() *sJsonParseSession {
}
}
// enter records entering one more nesting level, it fails if the nesting
// level exceeds maxParseDepth
func (s *sJsonParseSession) enter() error {
if s.depth >= maxParseDepth {
return ErrNestedTooDeep
}
s.depth++
return nil
}
func (s *sJsonParseSession) leave() {
s.depth--
}
func (s *sJsonParseSession) saveReferer(nodeId int, ptr *sJSONPointer) {
if nr, ok := s.objectMap[nodeId]; ok {
nr.pointers = append(nr.pointers, ptr)
@@ -43,10 +60,10 @@ func (s *sJsonParseSession) saveReferer(nodeId int, ptr *sJSONPointer) {
}
}
func (s *sJsonParseSession) saveNode(nodeId int, node JSONObject) {
func (s *sJsonParseSession) saveNode(nodeId int, node JSONObject) error {
if nr, ok := s.objectMap[nodeId]; ok {
if nr.node != nil {
panic(fmt.Sprintf("nodeId %d alreayd exists: %s != %s", nodeId, nr.node, node))
return errors.Wrapf(ErrDuplicateNodeId, "node id %d", nodeId)
} else {
nr.node = node
}
@@ -56,4 +73,5 @@ func (s *sJsonParseSession) saveNode(nodeId int, node JSONObject) {
pointers: nil,
}
}
return nil
}

View File

@@ -18,9 +18,18 @@ import (
"fmt"
"strings"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/sortedmap"
)
func writeJSONObject(sb *strings.Builder, v JSONObject) {
if gotypes.IsNil(v) {
sb.WriteString("null")
return
}
v.buildString(sb)
}
type writeSource interface {
buildString(sb *strings.Builder)
}
@@ -46,6 +55,10 @@ func (this *JSONBool) buildString(sb *strings.Builder) {
}
func (this *JSONDict) buildString(sb *strings.Builder) {
if this == nil {
sb.WriteString("null")
return
}
sb.WriteByte('{')
var idx = 0
if this.nodeId > 0 {
@@ -63,19 +76,23 @@ func (this *JSONDict) buildString(sb *strings.Builder) {
sb.WriteString(quoteString(k))
sb.WriteByte(':')
v.buildString(sb)
writeJSONObject(sb, v)
idx++
}
sb.WriteByte('}')
}
func (this *JSONArray) buildString(sb *strings.Builder) {
if this == nil {
sb.WriteString("null")
return
}
sb.WriteByte('[')
for idx, v := range this.data {
if idx > 0 {
sb.WriteByte(',')
}
v.buildString(sb)
writeJSONObject(sb, v)
}
sb.WriteByte(']')
}