Repository: kmesh-net/kmesh

Issue:
[OSPP 2025] Kmesh-daemon upgrades traffic without disruption
Kmesh is a high-performance and low overhead service mesh data plane based on eBPF and programmable kernel. Kmesh-Damon, acts as a hypervisor for the usespace and is responsible for communicating with the control plane and sending down the configuration of the Kmesh. In the previous version of Kmesh, it has been realized that kmesh-daemon restarts without breaking the established links, and is also able to govern the traffic according to the previous policy. Now we would like to cover the Kmesh-daemon upgrade scenario with this convenient feature. When Kmesh-daemon is upgraded, the traffic can be managed without interruption. The bottleneck is how to compare the structure of the bpf map before and after the upgrade.

Hints from issue comments:
@LiZhenCheng9527: ## OUTOUT

1. function code
1.1. bpf map structure comparison code
1.2. Method of determining the upgrade
2. Document
2.1. proposal
2.2. userguide
3. Test Code
3.1. Unit test for main functional functions
3.2. E2E test of feature@072020127: /assign

Solution patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5446c364..bf55cb09 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -14,7 +14,7 @@ on:
 
 jobs:
   build:
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     strategy:
       matrix:
         go-version: [ '1.23' ]
diff --git a/Makefile b/Makefile
index 61a4409d..fdd046eb 100644
--- a/Makefile
+++ b/Makefile
@@ -97,6 +97,8 @@ kmesh-bpf:
 	$(QUIET) make -C bpf/deserialization_to_bpf_map
 	
 	$(QUIET) $(GO) generate bpf/kmesh/bpf2go/bpf2go.go
+
+	$(QUIET) $(GO) run hack/gen_bpf_specs.go
 kmesh-ko:
 	$(QUIET) find $(ROOT_DIR)/mk -name "*.pc" | xargs sed -i "s#^prefix=.*#prefix=${ROOT_DIR}#g"
 	$(call printlog, BUILD, "kernel")
diff --git a/bpf/include/inner_map_defs.h b/bpf/include/inner_map_defs.h
index 028f59ca..f2bd28c8 100644
--- a/bpf/include/inner_map_defs.h
+++ b/bpf/include/inner_map_defs.h
@@ -10,8 +10,8 @@
 typedef enum { MAP_TYPE_64, MAP_TYPE_192, MAP_TYPE_296, MAP_TYPE_1600, MAP_TYPE_MAX } map_in_map_type;
 
 #define MAP_GET_TYPE(idx)                (__u8)((__u32)(idx) >> 24)
-#define MAP_GET_INDEX(idx)               (__u32)((__u32)(idx)&0xFFFFFF)
-#define MAP_GEN_OUTER_KEY(map_type, pos) ((__u32)((((__u8)(map_type)&0xFF) << 24) + ((__u32)(pos)&0xFFFFFF)))
+#define MAP_GET_INDEX(idx)               (__u32)((__u32)(idx) & 0xFFFFFF)
+#define MAP_GEN_OUTER_KEY(map_type, pos) ((__u32)((((__u8)(map_type) & 0xFF) << 24) + ((__u32)(pos) & 0xFFFFFF)))
 
 #define MAP_VAL_SIZE_64   64
 #define MAP_VAL_SIZE_192  192
diff --git a/bpf/kmesh/ads/include/kmesh_common.h b/bpf/kmesh/ads/include/kmesh_common.h
index b1a35d91..d59f02d0 100644
--- a/bpf/kmesh/ads/include/kmesh_common.h
+++ b/bpf/kmesh/ads/include/kmesh_common.h
@@ -84,7 +84,7 @@ enum kmesh_l7_msg_type { MSG_UNKNOW = 0, MSG_REQUEST, MSG_MID_REPONSE, MSG_FINAL
 enum kmesh_strncmp_type { STRNCMP_FAILED = 0, STRNCMP_PREFIX, STRNCMP_EXACT };
 
 #define KMESH_PROTO_TYPE_WIDTH (8)
-#define GET_RET_PROTO_TYPE(n)  ((n)&0xff)
+#define GET_RET_PROTO_TYPE(n)  ((n) & 0xff)
 #define GET_RET_MSG_TYPE(n)    (((n) >> KMESH_PROTO_TYPE_WIDTH) & 0xff)
 
 #define CHECK_MODULE_NAME_NULL(ret) ((ret) == -EINVAL)
diff --git a/hack/gen_bpf2go_exec.sh b/hack/gen_bpf2go_exec.sh
index a2dfd779..90af0e58 100755
--- a/hack/gen_bpf2go_exec.sh
+++ b/hack/gen_bpf2go_exec.sh
@@ -8,5 +8,6 @@ kmesh_exec() {
 	set_enhanced_kernel_env
 	prepare
 	go generate bpf/kmesh/bpf2go/bpf2go.go
+	go run hack/gen_bpf_specs.go
 }
 kmesh_exec
diff --git a/hack/gen_bpf_specs.go b/hack/gen_bpf_specs.go
new file mode 100644
index 00000000..ff7a8404
--- /dev/null
+++ b/hack/gen_bpf_specs.go
@@ -0,0 +1,485 @@
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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 main
+
+import (
+	"bytes"
+	"fmt"
+	"go/format"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strings"
+	"text/template"
+)
+
+type entry struct {
+	OutputDir string // e.g. kernelnative/normal or kernelnative/enhanced or dualengine or general
+	GoPkg     string // go package name passed in --go-package
+	Symbol    string // the symbol token (KmeshCgroupSock etc)
+}
+
+type pkgInfo struct {
+	Alias      string
+	ImportPath string
+	OutputDir  string
+	Entries    []entry
+}
+
+type symbolGen struct {
+	BaseName       string // e.g. KmeshSockops
+	NormalPkgAlias string // alias that provides normal symbol (may be "")
+	NormalSymbol   string // e.g. KmeshSockops
+	CompatPkgAlias string // alias that provides compat symbol (may be "")
+	CompatSymbol   string // e.g. KmeshSockopsCompat
+}
+
+func main() {
+	root, err := repoRoot()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed detect repo root: %v\n", err)
+		os.Exit(2)
+	}
+
+	files := collectGoFiles(root)
+
+	// regexes
+	genRe := regexp.MustCompile(`//\s*go:generate\s+(?:.*\b)bpf2go\b(.*)`)
+	outDirRe := regexp.MustCompile(`--output-dir\s+([^\s]+)`)
+	goPkgRe := regexp.MustCompile(`--go-package\s+([^\s]+)`)
+	symbolRe := regexp.MustCompile(`\b([A-Za-z0-9_]+)\s+[^\s]+\.(?:c|C)\b`)
+
+	var entries []entry
+	for _, f := range files {
+		b, _ := os.ReadFile(f)
+		for _, line := range strings.Split(string(b), "\n") {
+			m := genRe.FindStringSubmatch(line)
+			if m == nil {
+				continue
+			}
+			rest := m[1]
+			out := ""
+			if mm := outDirRe.FindStringSubmatch(rest); mm != nil {
+				out = mm[1]
+			}
+			pkg := ""
+			if mm := goPkgRe.FindStringSubmatch(rest); mm != nil {
+				pkg = mm[1]
+			}
+			if mm := symbolRe.FindStringSubmatch(rest); mm != nil {
+				sym := mm[1]
+				entries = append(entries, entry{OutputDir: out, GoPkg: pkg, Symbol: sym})
+			}
+		}
+	}
+
+	if len(entries) == 0 {
+		fmt.Println("no bpf2go generate lines found, nothing to do")
+		return
+	}
+
+	byDir := map[string][]entry{}
+	for _, e := range entries {
+		dir := strings.Trim(e.OutputDir, `"'`)
+		byDir[dir] = append(byDir[dir], e)
+	}
+
+	keys := make([]string, 0, len(byDir))
+	for k := range byDir {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	modulePrefix := detectModulePath(root)
+	if modulePrefix == "" {
+		modulePrefix = "kmesh.net/kmesh"
+	}
+
+	sanitizeRe := regexp.MustCompile(`[^A-Za-z0-9_]`)
+	sanitize := func(s string) string {
+		out := sanitizeRe.ReplaceAllString(s, "_")
+		out = strings.Trim(out, "_")
+		if out == "" {
+			out = "pkg"
+		}
+		if out[0] >= '0' && out[0] <= '9' {
+			out = "pkg_" + out
+		}
+		return out
+	}
+
+	baseBpf2go := filepath.ToSlash(filepath.Join(root, "bpf", "kmesh", "bpf2go"))
+
+	var pkgsDefault []pkgInfo
+	var pkgsEnhanced []pkgInfo
+
+	for _, k := range keys {
+		list := byDir[k]
+		pattern := strings.Trim(k, `"'`)
+
+		var realDirs []string
+		if strings.ContainsAny(pattern, "*?[]") {
+			globFull := filepath.ToSlash(filepath.Join(baseBpf2go, pattern))
+			matches, _ := filepath.Glob(globFull)
+			for _, m := range matches {
+				if fi, err := os.Stat(m); err == nil && fi.IsDir() {
+					rel, err := filepath.Rel(baseBpf2go, m)
+					if err == nil {
+						realDirs = append(realDirs, filepath.ToSlash(rel))
+					}
+				}
+			}
+			if len(realDirs) == 0 {
+				realDirs = append(realDirs, pattern)
+			}
+		} else {
+			realDirs = append(realDirs, pattern)
+		}
+
+		for _, real := range realDirs {
+			if strings.Contains(real, "$ENHANCED_KERNEL") {
+				rdDefault := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "normal")
+				aliasDef := sanitize(rdDefault)
+				importDef := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdDefault))
+				pkgsDefault = append(pkgsDefault, pkgInfo{Alias: aliasDef, ImportPath: importDef, OutputDir: rdDefault, Entries: list})
+
+				rdEnh := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "enhanced")
+				aliasEnh := sanitize(rdEnh)
+				importEnh := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdEnh))
+				pkgsEnhanced = append(pkgsEnhanced, pkgInfo{Alias: aliasEnh, ImportPath: importEnh, OutputDir: rdEnh, Entries: list})
+			} else {
+				alias := sanitize(real)
+				importPath := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", real))
+				pi := pkgInfo{Alias: alias, ImportPath: importPath, OutputDir: real, Entries: list}
+				pkgsDefault = append(pkgsDefault, pi)
+				pkgsEnhanced = append(pkgsEnhanced, pi)
+			}
+		}
+	}
+
+	sort.Slice(pkgsDefault, func(i, j int) bool { return pkgsDefault[i].ImportPath < pkgsDefault[j].ImportPath })
+	sort.Slice(pkgsEnhanced, func(i, j int) bool { return pkgsEnhanced[i].ImportPath < pkgsEnhanced[j].ImportPath })
+
+	// build per-bucket symbol gens
+	knDefault := filterPkgsByPrefix(pkgsDefault, "kernelnative")
+	deDefault := filterPkgsByPrefix(pkgsDefault, "dualengine")
+	genDefault := filterPkgsByPrefix(pkgsDefault, "general")
+
+	knEnhanced := filterPkgsByPrefix(pkgsEnhanced, "kernelnative")
+
+	symsKnDefault := buildSymbolGen(knDefault)
+	symsDeDefault := buildSymbolGen(deDefault)
+	symsGenDefault := buildSymbolGen(genDefault)
+
+	symsKnEnhanced := buildSymbolGen(knEnhanced)
+
+	// prepare output dir
+	outDir := filepath.Join(root, "pkg", "bpf", "restart")
+	if err := os.MkdirAll(outDir, 0755); err != nil {
+		fmt.Fprintf(os.Stderr, "mkdir target dir: %v\n", err)
+		os.Exit(2)
+	}
+
+	// template
+	tplText := `/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+{{- range .Pkgs }}
+	{{ .Alias }} "{{ .ImportPath }}"
+{{- end }}
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+{{- range .SymsKn }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} has both normal and compat variants.
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	// Symbol {{ .BaseName }} only normal
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} only compat
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+
+{{- end }}
+	} else if config.DualEngineEnabled() {
+{{- range .SymsDe }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} has both normal and compat variants (dualengine).
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	// Symbol {{ .BaseName }} only normal (dualengine)
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	// Symbol {{ .BaseName }} only compat (dualengine)
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+{{- end }}
+	}
+
+{{- range .SymsGen }}
+{{- if and .NormalPkgAlias .CompatPkgAlias }}
+	// General Symbol {{ .BaseName }} has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}Compat"] = coll.Maps
+		}
+	} else {
+		if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+			return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err)
+		} else {
+			specs["{{ .BaseName }}"] = coll.Maps
+		}
+	}
+{{- else if .NormalPkgAlias }}
+	if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load General {{ .NormalSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}"] = coll.Maps
+	}
+{{- else if .CompatPkgAlias }}
+	if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil {
+		return nil, fmt.Errorf("load General Compat {{ .CompatSymbol }} spec: %w", err)
+	} else {
+		specs["{{ .BaseName }}Compat"] = coll.Maps
+	}
+{{- end }}
+{{- end }}
+
+	return specs, nil
+}
+`
+
+	funcMap := template.FuncMap{}
+	tpl, err := template.New("out").Funcs(funcMap).Parse(tplText)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "parse tpl: %v\n", err)
+		os.Exit(2)
+	}
+
+	// render default (non-enhanced) file
+	if err := renderVariant(tpl, pkgsDefault, symsKnDefault, symsDeDefault, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader.go"), "!enhanced"); err != nil {
+		fmt.Fprintf(os.Stderr, "write default: %v\n", err)
+		os.Exit(2)
+	}
+
+	// render enhanced file
+	if err := renderVariant(tpl, pkgsEnhanced, symsKnEnhanced, []symbolGen{}, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader_enhanced.go"), "enhanced"); err != nil {
+		fmt.Fprintf(os.Stderr, "write enhanced: %v\n", err)
+		os.Exit(2)
+	}
+}
+
+func collectGoFiles(root string) []string {
+	files := []string{}
+	_ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if fi.IsDir() {
+			if fi.Name() == "vendor" || strings.HasPrefix(path, filepath.Join(root, ".git")) {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+		if strings.HasSuffix(path, ".go") {
+			files = append(files, path)
+		}
+		return nil
+	})
+	return files
+}
+
+func detectModulePath(root string) string {
+	modf := filepath.Join(root, "go.mod")
+	b, err := os.ReadFile(modf)
+	if err != nil {
+		return ""
+	}
+	for _, line := range strings.Split(string(b), "\n") {
+		line = strings.TrimSpace(line)
+		if strings.HasPrefix(line, "module ") {
+			return strings.TrimSpace(strings.TrimPrefix(line, "module "))
+		}
+	}
+	return ""
+}
+
+func repoRoot() (string, error) {
+	dir, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	return dir, nil
+}
+
+func filterPkgsByPrefix(pkgs []pkgInfo, prefix string) []pkgInfo {
+	out := []pkgInfo{}
+	for _, p := range pkgs {
+		trim := strings.TrimPrefix(strings.TrimSpace(p.OutputDir), "/")
+		if strings.HasPrefix(trim, prefix) {
+			out = append(out, p)
+		}
+	}
+	sort.Slice(out, func(i, j int) bool { return out[i].ImportPath < out[j].ImportPath })
+	return out
+}
+
+func buildSymbolGen(pkgs []pkgInfo) []symbolGen {
+	m := map[string]*symbolGen{}
+	for _, p := range pkgs {
+		for _, e := range p.Entries {
+			sym := e.Symbol
+			base := sym
+			isCompat := false
+			if strings.HasSuffix(sym, "Compat") {
+				base = strings.TrimSuffix(sym, "Compat")
+				isCompat = true
+			}
+			if _, ok := m[base]; !ok {
+				m[base] = &symbolGen{BaseName: base}
+			}
+			sg := m[base]
+			if isCompat {
+				sg.CompatPkgAlias = p.Alias
+				sg.CompatSymbol = sym
+			} else {
+				sg.NormalPkgAlias = p.Alias
+				sg.NormalSymbol = sym
+			}
+		}
+	}
+	out := make([]symbolGen, 0, len(m))
+	for _, v := range m {
+		out = append(out, *v)
+	}
+	sort.Slice(out, func(i, j int) bool { return out[i].BaseName < out[j].BaseName })
+	return out
+}
+
+func renderVariant(tpl *template.Template, pkgs []pkgInfo, symsKn, symsDe, symsGen []symbolGen, outPath, buildTag string) error {
+	// combine pkgs into template data (unique by import path)
+	uniq := map[string]pkgInfo{}
+	for _, p := range pkgs {
+		uniq[p.ImportPath] = p
+	}
+	list := make([]pkgInfo, 0, len(uniq))
+	for _, v := range uniq {
+		list = append(list, v)
+	}
+	sort.Slice(list, func(i, j int) bool { return list[i].ImportPath < list[j].ImportPath })
+
+	buf := &bytes.Buffer{}
+	err := tpl.Execute(buf, map[string]interface{}{
+		"Pkgs":    list,
+		"SymsKn":  symsKn,
+		"SymsDe":  symsDe,
+		"SymsGen": symsGen,
+	})
+	if err != nil {
+		return fmt.Errorf("execute tpl: %w", err)
+	}
+
+	var out []byte
+	if buildTag != "" {
+		header := fmt.Sprintf("//go:build %s\n// +build %s\n\n", buildTag, buildTag)
+		out = append([]byte(header), buf.Bytes()...)
+	} else {
+		out = buf.Bytes()
+	}
+
+	src, err := format.Source(out)
+	if err != nil {
+		return fmt.Errorf("gofmt failed: %w\nraw output:\n%s\n", err, string(out))
+	}
+	if err := os.WriteFile(outPath, src, 0644); err != nil {
+		return fmt.Errorf("write out: %w", err)
+	}
+	return nil
+}
diff --git a/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h b/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
index 20c8c86b..f6569a51 100644
--- a/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
+++ b/kernel/ko_src/kmesh/kmesh_parse_protocol_data.h
@@ -27,10 +27,10 @@ enum kmesh_strncmp_type { STRNCMP_FAILED = 0, STRNCMP_PREFIX, STRNCMP_EXACT };
 
 #define KMESH_PROTO_TYPE_WIDTH (8)
 
-#define SET_RET_PROTO_TYPE(n, type) (n) = (((n)&0xff00) | ((u32)(type)&0xff))
-#define GET_RET_PROTO_TYPE(n)       ((n)&0xff)
+#define SET_RET_PROTO_TYPE(n, type) (n) = (((n) & 0xff00) | ((u32)(type) & 0xff))
+#define GET_RET_PROTO_TYPE(n)       ((n) & 0xff)
 
-#define SET_RET_MSG_TYPE(n, type) (n) = (((n)&0xff) | (((u32)(type)&0xff) << KMESH_PROTO_TYPE_WIDTH))
+#define SET_RET_MSG_TYPE(n, type) (n) = (((n) & 0xff) | (((u32)(type) & 0xff) << KMESH_PROTO_TYPE_WIDTH))
 #define GET_RET_MSG_TYPE(n)       (((n) >> KMESH_PROTO_TYPE_WIDTH) & 0xff)
 
 #define LOG(level, fmt, ...) printk(level "Kmesh_module: " fmt, ##__VA_ARGS__)
diff --git a/pkg/bpf/ads/sock_connection.go b/pkg/bpf/ads/sock_connection.go
index 2f72f6ba..abae9067 100644
--- a/pkg/bpf/ads/sock_connection.go
+++ b/pkg/bpf/ads/sock_connection.go
@@ -140,7 +140,7 @@ func (sc *BpfSockConn) Attach() error {
 	}
 
 	progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link)
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/ads/sock_ops.go b/pkg/bpf/ads/sock_ops.go
index 579638f5..d6fa974b 100644
--- a/pkg/bpf/ads/sock_ops.go
+++ b/pkg/bpf/ads/sock_ops.go
@@ -95,7 +95,7 @@ func (sc *BpfSockOps) Attach() error {
 
 	// pin bpf_link
 	progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link)
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/bpf.go b/pkg/bpf/bpf.go
index 76d8b2e3..b8ee93ac 100644
--- a/pkg/bpf/bpf.go
+++ b/pkg/bpf/bpf.go
@@ -141,7 +141,7 @@ func StopMda() error {
 func (l *BpfLoader) Stop() {
 	var err error
 	C.deserial_uninit()
-	if restart.GetExitType() == restart.Restart {
+	if restart.GetExitType() == restart.Restart || restart.GetExitType() == restart.Update {
 		return
 	}
 
@@ -194,8 +194,9 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map {
 	case restart.Restart:
 		return versionMap
 	case restart.Update:
-		// TODO : update mode has not been fully developed and is currently consistent with normal mode
-		log.Warnf("Update mode support is under development, Will be started in Normal mode.")
+		if updateVersionMap := restart.UpdateMapHandler(versionMap, versionPath, config); updateVersionMap != nil {
+			return updateVersionMap
+		}
 	default:
 	}
 
@@ -231,9 +232,19 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map {
 		return nil
 	}
 
+	mapspecs, err := restart.LoadCompileTimeSpecs(config)
+	if err != nil {
+		log.Errorf("failed to load compile time specs: %v", err)
+		return nil
+	}
+
 	storeVersionInfo(m)
 	log.Infof("kmesh start with Normal")
 	restart.SetStartType(restart.Normal)
+	if err := restart.SnapshotSpecsbyProg(mapspecs); err != nil {
+		log.Errorf("failed to store compile time specs: %v", err)
+		return nil
+	}
 	return m
 }
 
diff --git a/pkg/bpf/restart/bpf_update.go b/pkg/bpf/restart/bpf_update.go
new file mode 100644
index 00000000..19323fb4
--- /dev/null
+++ b/pkg/bpf/restart/bpf_update.go
@@ -0,0 +1,500 @@
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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 restart
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/btf"
+
+	"kmesh.net/kmesh/daemon/options"
+	"kmesh.net/kmesh/pkg/version"
+)
+
+type StructFieldChanges struct {
+	FieldRemoved        bool // A field was present in the old struct (A) but is now missing in the new struct (B).
+	FieldAdded          bool // A new field was added to the struct.
+	FieldTypeChanged    bool // A field with the same name has a different type.
+	FieldOffsetChanged  bool // The memory offset of a field with the same name has changed.
+	NestedLayoutChanged bool // The layout of a nested struct has changed (e.g., its fields were added, removed, or changed).
+}
+
+type PersistedMemberLayout struct {
+	Name         string                 `json:"name"`
+	TypeName     string                 `json:"typeName"`
+	Offset       uint32                 `json:"offset"`
+	BitfieldSize uint32                 `json:"bitfieldsize"` // only have value when the type is bitfield
+	Nested       *PersistedStructLayout `json:"nested,omitempty"`
+}
+
+type PersistedStructLayout struct {
+	Name    string                  `json:"name"`
+	Members []PersistedMemberLayout `json:"members"`
+}
+
+type PersistedMapSpec struct {
+	Name            string                `json:"name"`
+	Type            string                `json:"type"` // MapType.String()
+	KeySize         uint32                `json:"keySize"`
+	ValueSize       uint32                `json:"valueSize"`
+	MaxEntries      uint32                `json:"maxEntries"`
+	Flags           uint32                `json:"flags"`
+	KeyStructInfo   PersistedStructLayout `json:"keyInfo"` // get from btf.Struct
+	ValueStructInfo PersistedStructLayout `json:"valueInfo"`
+}
+
+type PersistedSnapshot struct {
+	Maps map[string]map[string]PersistedMapSpec `json:"maps"`
+}
+
+const (
+	MapSpecDir      = "/mnt/kmesh_mapspecs"
+	MapSpecFilename = "mapspecs_by_prog.json"
+)
+
+// UpdateMapHandler handles the “Update” case in NewVersionMap.
+// It will migrate any BPF maps whose on‑disk pin already exists but whose
+// compiled MapSpec has changed
+func UpdateMapHandler(versionMap *ebpf.Map, kmBpfPath string, config *options.BpfConfig) *ebpf.Map {
+	persistedSpecs, err := LoadPersistedSnapshot()
+	if err != nil {
+		log.Errorf("load persisted map spec failed")
+		return nil
+	}
+	if persistedSpecs == nil {
+		log.Errorf("persisted map spec is nil")
+		return nil
+	}
+	specsbyProg, err := LoadCompileTimeSpecs(config)
+	if err != nil {
+		log.Errorf("load oldSpecsbyProg failed")
+		return nil
+	}
+	progNames := unionKeys(specsbyProg, persistedSpecs.Maps)
+	for _, progName := range progNames {
+		newMaps := specsbyProg[progName]
+		oldMaps := persistedSpecs.Maps[progName]
+		if newMaps == nil || oldMaps == nil {
+			continue
+		}
+
+		mapNames := unionKeys(newMaps, oldMaps)
+		for _, mapName := range mapNames {
+			newSpec, hasNew := newMaps[mapName]
+			oldSpec, hasOld := oldMaps[mapName]
+
+			pinPath := filepath.Join(kmBpfPath, mapName)
+
+			switch {
+			case !hasNew && hasOld: // clean up
+				oldMap, _ := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{})
+				if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) {
+					log.Warnf("failed to unpin old map: %v (continuing)", err)
+				}
+				if err := oldMap.Close(); err != nil {
+					log.Warnf("failed to close old map FD: %v (continuing)", err)
+				}
+				if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+					log.Warnf("failed to remove old map pinpath: %v (continuing)", err)
+				}
+			case hasNew && !hasOld:
+				if _, err := createEmptyMap(newSpec, pinPath, mapName, nil); err != nil {
+					log.Errorf("create new map %s/%s failed: %v", progName, mapName, err)
+				}
+			case hasNew && hasOld:
+				if _, err := migrateMap(&oldSpec, newSpec, progName, mapName, pinPath); err != nil {
+					log.Errorf("migrate map %s/%s failed: %v", progName, mapName, err)
+				}
+			}
+		}
+	}
+
+	log.Infof("kmesh start with Update")
+	updateVersionInfo(versionMap)
+	if err := SnapshotSpecsbyProg(specsbyProg); err != nil {
+		return versionMap
+	}
+	return versionMap
+}
+
+func updateVersionInfo(versionMap *ebpf.Map) {
+	key := uint32(0)
+	var value uint32
+	hash.Reset()
+	hash.Write([]byte(version.Get().GitVersion))
+	value = hash.Sum32()
+	if err := versionMap.Put(&key, &value); err != nil {
+		log.Errorf("update Version Map failed, err is %v", err)
+	}
+}
+
+func MigrateMap(
+	oldMapSpec *PersistedMapSpec,
+	newMapSpec *ebpf.MapSpec,
+	progName, mapName, pinPath string,
+) (*ebpf.Map, error) {
+	return migrateMap(oldMapSpec, newMapSpec, progName, mapName, pinPath)
+}
+
+func migrateMap(
+	oldMapSpec *PersistedMapSpec,
+	newMapSpec *ebpf.MapSpec,
+	progName, mapName, pinPath string,
+) (*ebpf.Map, error) {
+	if oldMapSpec == nil {
+		return createEmptyMap(newMapSpec, pinPath, mapName, nil)
+	}
+	if oldMapSpec.Type != newMapSpec.Type.String() ||
+		oldMapSpec.KeySize != newMapSpec.KeySize ||
+		oldMapSpec.ValueSize != newMapSpec.ValueSize ||
+		oldMapSpec.MaxEntries > newMapSpec.MaxEntries {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+
+	if needsRecreate(oldMapSpec.KeyStructInfo, newMapSpec.Key) {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+
+	if needsRecreate(oldMapSpec.ValueStructInfo, newMapSpec.Value) {
+		return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName)
+	}
+	return nil, nil
+}
+
+func needsRecreate(oldStruct PersistedStructLayout, newType btf.Type) bool {
+	if newType == nil {
+		if oldStruct.Name != "" || len(oldStruct.Members) != 0 {
+			return true
+		}
+		return false
+	}
+
+	if newStruct, ok := newType.(*btf.Struct); ok {
+		diff := diffStructInfoAgainstBTF(oldStruct, newStruct, make(map[string]bool))
+		if diff.FieldAdded || diff.FieldRemoved || diff.FieldTypeChanged ||
+			diff.FieldOffsetChanged || diff.NestedLayoutChanged {
+			return true
+		}
+		return false
+	}
+
+	newTypeName := newType.TypeName()
+	if len(oldStruct.Members) != 0 {
+		return true
+	}
+	if oldStruct.Name == "" {
+		return true
+	}
+	if oldStruct.Name != newTypeName {
+		return true
+	}
+	return false
+}
+
+func createEmptyMapWithPinnedMap(spec *ebpf.MapSpec, pinPath, mapName string) (*ebpf.Map, error) {
+	oldMap, err := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{})
+	if err != nil {
+		return createEmptyMap(spec, pinPath, mapName, nil)
+	}
+	return createEmptyMap(spec, pinPath, mapName, oldMap)
+}
+
+func DiffStructInfoAgainstBTF(
+	a PersistedStructLayout,
+	b *btf.Struct,
+	visited map[string]bool,
+) StructFieldChanges {
+	return diffStructInfoAgainstBTF(a, b, visited)
+}
+
+func diffStructInfoAgainstBTF(
+	a PersistedStructLayout,
+	b *btf.Struct,
+	visited map[string]bool,
+) StructFieldChanges {
+	diff := StructFieldChanges{}
+
+	oldMap := make(map[string]PersistedMemberLayout, len(a.Members))
+	for _, m := range a.Members {
+		oldMap[m.Name] = m
+	}
+	newMap := make(map[string]btf.Member, len(b.Members))
+	for _, m := range b.Members {
+		newMap[m.Name] = m
+	}
+
+	// check added fields (present in new but not in old)
+	for name := range newMap {
+		if _, ok := oldMap[name]; !ok {
+			diff.FieldAdded = true
+			break
+		}
+	}
+
+	// check removed / type / offset / nested changes
+	for name, map_old := range oldMap {
+		map_new, exists := newMap[name]
+		if !exists {
+			diff.FieldRemoved = true
+			break
+		}
+
+		if map_old.Offset != uint32(map_new.Offset) || map_old.BitfieldSize != uint32(map_new.BitfieldSize) {
+			diff.FieldOffsetChanged = true
+			break
+		}
+
+		if map_old.TypeName == map_new.Type.TypeName() {
+			if mbStruct, ok := map_new.Type.(*btf.Struct); ok {
+				if map_old.Nested != nil {
+					if map_old.Nested.Name != "" {
+						if !visited[map_old.Nested.Name] {
+							visited[map_old.Nested.Name] = true
+							nestedDiff := diffStructInfoAgainstBTF(*map_old.Nested, mbStruct, visited)
+							if nestedDiff.FieldAdded || nestedDiff.FieldRemoved || nestedDiff.FieldTypeChanged ||
+								nestedDiff.FieldOffsetChanged || nestedDiff.NestedLayoutChanged {
+								diff.NestedLayoutChanged = true
+								break
+							}
+						}
+						continue
+					}
+				}
+				diff.NestedLayoutChanged = true
+				break
+			} else { // if new side is not struct
+				log.Info(map_old.Nested != nil)
+				if map_old.TypeName != map_new.Type.TypeName() || map_old.Nested != nil {
+					diff.FieldTypeChanged = true
+					break
+				}
+			}
+		} else {
+			diff.FieldTypeChanged = true
+			break
+		}
+	}
+	return diff
+}
+
+func createEmptyMap(spec *ebpf.MapSpec, pinPath string, mapName string, oldMap *ebpf.Map) (*ebpf.Map, error) {
+	if oldMap == nil {
+		if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+			return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err)
+		}
+		m, err := ebpf.NewMap(spec)
+		if err != nil {
+			return nil, fmt.Errorf("new map %s: %w", mapName, err)
+		}
+		if err := os.MkdirAll(filepath.Dir(pinPath), syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR|syscall.S_IRGRP|syscall.S_IXGRP); err != nil && !os.IsExist(err) {
+			return nil, fmt.Errorf("mkdir %s: %w", filepath.Dir(pinPath), err)
+		}
+		if err := m.Pin(pinPath); err != nil {
+			return nil, fmt.Errorf("pin empty map %s: %w", pinPath, err)
+		}
+		return m, nil
+	}
+
+	tmpPinPath := pinPath + ".tmp"
+	m, err := ebpf.NewMap(spec)
+	if err != nil {
+		return nil, fmt.Errorf("new map %s: %w", mapName, err)
+	}
+	if err := os.MkdirAll(filepath.Dir(tmpPinPath), 0755); err != nil && !os.IsExist(err) {
+		m.Close()
+		return nil, fmt.Errorf("mkdir %s: %w", tmpPinPath, err)
+	}
+	if err := m.Pin(tmpPinPath); err != nil {
+		m.Close()
+		return nil, fmt.Errorf("pin tmp map %s: %w", tmpPinPath, err)
+	}
+	if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) {
+		log.Warnf("failed to unpin old map %s: %v (continuing)", pinPath, err)
+	}
+	if err := oldMap.Close(); err != nil {
+		log.Warnf("failed to close old map FD: %v (continuing)", err)
+	}
+	if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) {
+		m.Close()
+		return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err)
+	}
+	if err := os.Rename(tmpPinPath, pinPath); err != nil {
+		m.Close()
+		return nil, fmt.Errorf("rename tmp %s to %s failed: %w", tmpPinPath, pinPath, err)
+	}
+	return m, nil
+}
+
+// This function correctly handles two maps with different value types.
+func unionKeys[V1, V2 any](map1 map[string]V1, map2 map[string]V2) []string {
+	// Pre-allocate the set with a reasonable capacity.
+	set := make(map[string]struct{}, len(map1))
+
+	for k := range map1 {
+		set[k] = struct{}{}
+	}
+
+	for k := range map2 {
+		set[k] = struct{}{}
+	}
+
+	keys := make([]string, 0, len(set))
+	for k := range set {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+func buildStructInfoRecursive(t btf.Type, registry map[string]PersistedStructLayout, visited map[btf.Type]bool) PersistedStructLayout {
+	if t == nil {
+		return PersistedStructLayout{}
+	}
+
+	st, ok := t.(*btf.Struct)
+	if !ok {
+		return PersistedStructLayout{Name: t.TypeName()}
+	}
+
+	if existing, ok := registry[st.Name]; ok {
+		return existing
+	}
+
+	if visited[t] {
+		return PersistedStructLayout{Name: st.Name}
+	}
+
+	visited[t] = true
+	si := PersistedStructLayout{
+		Name:    st.Name,
+		Members: make([]PersistedMemberLayout, 0, len(st.Members)),
+	}
+
+	for _, m := range st.Members {
+		offBytes := uint32(m.Offset)
+		sizeBytes := uint32(m.BitfieldSize)
+		log.Info("sizeBytes", sizeBytes)
+		mi := PersistedMemberLayout{
+			Name:         m.Name,
+			TypeName:     m.Type.TypeName(),
+			Offset:       offBytes,
+			BitfieldSize: sizeBytes,
+			Nested:       nil,
+		}
+
+		if nested, ok := m.Type.(*btf.Struct); ok {
+			nestedInfo := buildStructInfoRecursive(nested, registry, visited)
+			mi.Nested = &nestedInfo
+			registry[nestedInfo.Name] = nestedInfo
+		}
+
+		si.Members = append(si.Members, mi)
+	}
+	registry[si.Name] = si
+	return si
+}
+
+// SnapshotSpecsbyProg takes a nested map of BPF map specifications and persists them to a file.
+// The structure of the input map `specsbyProg` is:
+//
+//		map[programName] -> map[mapName] -> *ebpf.MapSpec
+//
+//	  - The first key (programName) is the name of the BPF program collection,
+//	    e.g., "KmeshCgroupSock".
+//	  - The second key (mapName) is the name of a specific BPF map within that program,
+//	    e.g., "kmesh_endpoints".
+func SnapshotSpecsbyProg(specsbyProg map[string]map[string]*ebpf.MapSpec) error {
+	wrapper := make(map[string]map[string]PersistedMapSpec, len(specsbyProg))
+	registry := make(map[string]PersistedStructLayout)
+	visited := make(map[btf.Type]bool)
+
+	for prog, maps := range specsbyProg {
+		wrapper[prog] = make(map[string]PersistedMapSpec, len(maps))
+		for name, ms := range maps {
+			if ms == nil {
+				continue
+			}
+
+			var keyInfo PersistedStructLayout
+			if ms.Key != nil {
+				keyInfo = buildStructInfoRecursive(ms.Key, registry, visited)
+			} else {
+				keyInfo = PersistedStructLayout{Name: ""}
+			}
+
+			var valueInfo PersistedStructLayout
+			if ms.Value != nil {
+				valueInfo = buildStructInfoRecursive(ms.Value, registry, visited)
+			} else {
+				valueInfo = PersistedStructLayout{Name: ""}
+			}
+
+			pms := PersistedMapSpec{
+				Name:            ms.Name,
+				Type:            ms.Type.String(),
+				KeySize:         ms.KeySize,
+				ValueSize:       ms.ValueSize,
+				MaxEntries:      ms.MaxEntries,
+				Flags:           ms.Flags,
+				KeyStructInfo:   keyInfo,
+				ValueStructInfo: valueInfo,
+			}
+			wrapper[prog][name] = pms
+		}
+	}
+
+	snapshot := PersistedSnapshot{
+		Maps: wrapper,
+	}
+
+	if err := os.MkdirAll(MapSpecDir, 0755); err != nil {
+		return fmt.Errorf("mkdir specDir: %w", err)
+	}
+	data, err := json.MarshalIndent(snapshot, "", "  ")
+	if err != nil {
+		return fmt.Errorf("marshal snapshot: %w", err)
+	}
+	tmp := filepath.Join(MapSpecDir, MapSpecFilename+".tmp")
+	target := filepath.Join(MapSpecDir, MapSpecFilename)
+	if err := os.WriteFile(tmp, data, 0644); err != nil {
+		return fmt.Errorf("write tmp snapshot: %w", err)
+	}
+	if err := os.Rename(tmp, target); err != nil {
+		return fmt.Errorf("rename wrapper: %w", err)
+	}
+	return nil
+}
+
+func LoadPersistedSnapshot() (*PersistedSnapshot, error) {
+	path := filepath.Join(MapSpecDir, MapSpecFilename)
+	buf, err := os.ReadFile(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("read snapshot file: %w", err)
+	}
+	var snap PersistedSnapshot
+	if err := json.Unmarshal(buf, &snap); err != nil {
+		return nil, fmt.Errorf("unmarshal snapshot: %w", err)
+	}
+	return &snap, nil
+}
diff --git a/pkg/bpf/restart/new_version_mapspec_loader.go b/pkg/bpf/restart/new_version_mapspec_loader.go
new file mode 100644
index 00000000..7c9cb6f1
--- /dev/null
+++ b/pkg/bpf/restart/new_version_mapspec_loader.go
@@ -0,0 +1,171 @@
+//go:build !enhanced
+// +build !enhanced
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine"
+	general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general"
+	kernelnative_normal "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/normal"
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+		// Symbol KmeshCgroupSock has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_normal.LoadKmeshCgroupSockCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_normal.LoadKmeshCgroupSock(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err)
+			} else {
+				specs["KmeshCgroupSock"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockops has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_normal.LoadKmeshSockopsCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_normal.LoadKmeshSockops(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockops spec: %w", err)
+			} else {
+				specs["KmeshSockops"] = coll.Maps
+			}
+		}
+	} else if config.DualEngineEnabled() {
+		// Symbol KmeshCgroupSkb has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshCgroupSkbCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSkbCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSkbCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshCgroupSkb(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSkb spec: %w", err)
+			} else {
+				specs["KmeshCgroupSkb"] = coll.Maps
+			}
+		}
+		// Symbol KmeshCgroupSockWorkload has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshCgroupSockWorkloadCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockWorkloadCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockWorkloadCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshCgroupSockWorkload(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSockWorkload spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockWorkload"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSendmsg has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshSendmsgCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSendmsgCompat spec: %w", err)
+			} else {
+				specs["KmeshSendmsgCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshSendmsg(); err != nil {
+				return nil, fmt.Errorf("load KmeshSendmsg spec: %w", err)
+			} else {
+				specs["KmeshSendmsg"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockopsWorkload has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshSockopsWorkloadCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsWorkloadCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsWorkloadCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshSockopsWorkload(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockopsWorkload spec: %w", err)
+			} else {
+				specs["KmeshSockopsWorkload"] = coll.Maps
+			}
+		}
+		// Symbol KmeshXDPAuth has both normal and compat variants (dualengine).
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := dualengine.LoadKmeshXDPAuthCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshXDPAuthCompat spec: %w", err)
+			} else {
+				specs["KmeshXDPAuthCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := dualengine.LoadKmeshXDPAuth(); err != nil {
+				return nil, fmt.Errorf("load KmeshXDPAuth spec: %w", err)
+			} else {
+				specs["KmeshXDPAuth"] = coll.Maps
+			}
+		}
+	}
+	// General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecrypt"] = coll.Maps
+		}
+	}
+	// General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncrypt"] = coll.Maps
+		}
+	}
+
+	return specs, nil
+}
diff --git a/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go
new file mode 100644
index 00000000..c0c1a4fd
--- /dev/null
+++ b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go
@@ -0,0 +1,101 @@
+//go:build enhanced
+// +build enhanced
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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.
+ */
+
+// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT.
+
+package restart
+
+import (
+	"fmt"
+
+	"github.com/cilium/ebpf"
+
+	dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine"
+	general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general"
+	kernelnative_enhanced "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/enhanced"
+	"kmesh.net/kmesh/daemon/options"
+	helper "kmesh.net/kmesh/pkg/utils"
+)
+
+// Auto-generated: keeps in sync with //go:generate bpf2go lines.
+func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) {
+	specs := make(map[string]map[string]*ebpf.MapSpec)
+
+	if config.KernelNativeEnabled() {
+		// Symbol KmeshCgroupSock has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_enhanced.LoadKmeshCgroupSockCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err)
+			} else {
+				specs["KmeshCgroupSockCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_enhanced.LoadKmeshCgroupSock(); err != nil {
+				return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err)
+			} else {
+				specs["KmeshCgroupSock"] = coll.Maps
+			}
+		}
+		// Symbol KmeshSockops has both normal and compat variants.
+		if helper.KernelVersionLowerThan5_13() {
+			if coll, err := kernelnative_enhanced.LoadKmeshSockopsCompat(); err != nil {
+				return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err)
+			} else {
+				specs["KmeshSockopsCompat"] = coll.Maps
+			}
+		} else {
+			if coll, err := kernelnative_enhanced.LoadKmeshSockops(); err != nil {
+				return nil, fmt.Errorf("load KmeshSockops spec: %w", err)
+			} else {
+				specs["KmeshSockops"] = coll.Maps
+			}
+		}
+	} else if config.DualEngineEnabled() {
+	}
+	// General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkDecrypt"] = coll.Maps
+		}
+	}
+	// General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel)
+	if helper.KernelVersionLowerThan5_13() {
+		if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil {
+			return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncryptCompat"] = coll.Maps
+		}
+	} else {
+		if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil {
+			return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err)
+		} else {
+			specs["KmeshTcMarkEncrypt"] = coll.Maps
+		}
+	}
+
+	return specs, nil
+}
diff --git a/pkg/bpf/workload/skb.go b/pkg/bpf/workload/skb.go
index 88e1b180..3db97981 100644
--- a/pkg/bpf/workload/skb.go
+++ b/pkg/bpf/workload/skb.go
@@ -122,7 +122,7 @@ func (cs *BpfCroupSkbWorkload) Attach() error {
 	}
 	pinPathIn := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_ingress_prog")
 	pinPathEg := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_egress_prog")
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if cs.Link, err = utils.BpfProgUpdate(pinPathIn, cgopt); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/workload/sock_connection.go b/pkg/bpf/workload/sock_connection.go
index ea254c73..fdbe316b 100644
--- a/pkg/bpf/workload/sock_connection.go
+++ b/pkg/bpf/workload/sock_connection.go
@@ -169,7 +169,7 @@ func (sc *SockConnWorkload) Attach() error {
 	pinPathDns4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_prog")
 	pinPathDnsRecv4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_recv_prog")
 
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if sc.Link, err = utils.BpfProgUpdate(pinPath4, cgopt4); err != nil {
 			return err
 		}
diff --git a/pkg/bpf/workload/sock_ops.go b/pkg/bpf/workload/sock_ops.go
index b6211b72..02ff479d 100644
--- a/pkg/bpf/workload/sock_ops.go
+++ b/pkg/bpf/workload/sock_ops.go
@@ -112,7 +112,7 @@ func (so *BpfSockOpsWorkload) Attach() error {
 	}
 	pinPath := filepath.Join(so.Info.BpfFsPath, "cgroup_sockops_prog")
 
-	if restart.GetStartType() == restart.Restart {
+	if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update {
 		if so.Link, err = utils.BpfProgUpdate(pinPath, cgopt); err != nil {
 			return err
 		}



Test patch:
diff --git a/pkg/bpf/bpf_test.go b/pkg/bpf/bpf_test.go
index eb46e4f6..0f78fe15 100644
--- a/pkg/bpf/bpf_test.go
+++ b/pkg/bpf/bpf_test.go
@@ -27,6 +27,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/btf"
+
 	"kmesh.net/kmesh/daemon/options"
 	"kmesh.net/kmesh/pkg/bpf/factory"
 	"kmesh.net/kmesh/pkg/bpf/restart"
@@ -246,3 +249,295 @@ func Test_getNodeIPAddress(t *testing.T) {
 		})
 	}
 }
+
+// helper: build a simple btf.Int without relying on encoding constants
+func intType(name string, sizeBytes int) *btf.Int {
+	return &btf.Int{
+		Name: name,
+		Size: uint32(sizeBytes * 8),
+	}
+}
+
+// Test diffStructInfoAgainstBTF basic cases: FieldAdded / FieldRemoved / Offset / Nested
+func TestDiffStructInfoAgainstBTF_Basics(t *testing.T) {
+	// old StructInfo: one member "a"
+	old := restart.PersistedStructLayout{
+		Name: "S_old",
+		Members: []restart.PersistedMemberLayout{
+			{
+				Name:         "a",
+				TypeName:     "uint32",
+				Offset:       0, // we'll match against btf.Member.Offset below (no /8 used in current diff impl)
+				BitfieldSize: 0,
+			},
+		},
+	}
+
+	// New BTF struct: has "a" and new field "b" => FieldAdded == true
+	newWithAdded := &btf.Struct{
+		Name: "S_new_added",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(32),
+			},
+			{
+				Name:         "b",
+				Type:         intType("uint8", 1),
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	d := restart.DiffStructInfoAgainstBTF(old, newWithAdded, make(map[string]bool))
+	if !d.FieldAdded {
+		t.Fatalf("expected FieldAdded==true, got %+v", d)
+	}
+
+	// New BTF struct: missing "a" => FieldRemoved true
+	newRemoved := &btf.Struct{
+		Name: "S_new_removed",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	d = restart.DiffStructInfoAgainstBTF(old, newRemoved, make(map[string]bool))
+	if !d.FieldRemoved {
+		t.Fatalf("expected FieldRemoved==true, got %+v", d)
+	}
+
+	// Offset change: new has "a" but with different Offset
+	newOff := &btf.Struct{
+		Name: "S_new_off",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(8), // note: current diff code compares uint32(member.Offset) vs saved Offset
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	// To make the offset comparison hit, set old.Members[0].Offset to uint32(member.Offset)
+	oldOffsetMatch := old
+	oldOffsetMatch.Members[0].Offset = uint32(newOff.Members[0].Offset) // direct match -> no offset diff
+	d = restart.DiffStructInfoAgainstBTF(oldOffsetMatch, newOff, make(map[string]bool))
+	if d.FieldOffsetChanged {
+		t.Fatalf("did not expect FieldOffsetChanged when offsets match (got %+v)", d)
+	}
+	// Now set old offset to different value -> expect FieldOffsetChanged
+	oldOffsetMismatch := old
+	oldOffsetMismatch.Members[0].Offset = uint32(0)
+	d = restart.DiffStructInfoAgainstBTF(oldOffsetMismatch, newOff, make(map[string]bool))
+	if !d.FieldOffsetChanged {
+		t.Fatalf("expected FieldOffsetChanged true, got %+v", d)
+	}
+}
+
+func TestDiffStructInfoAgainstBTF_NestedIncompatible(t *testing.T) {
+	// old: inner { a: uint32 }, outer { x: inner, y: uint64 }
+	innerInt := intType("uint32", 4)
+	oldInner := &btf.Struct{
+		Name: "inner",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         innerInt,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	outerUint := intType("__u64", 8)
+	oldOuter := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         oldInner,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// persisted registry stores old definitions (StructInfo with Nested expanded)
+	registry := map[string]restart.PersistedStructLayout{
+		"inner": {
+			Name: "inner",
+			Members: []restart.PersistedMemberLayout{
+				{
+					Name:         "a",
+					TypeName:     "uint32",
+					Offset:       uint32(oldInner.Members[0].Offset), // use raw bit value as in your diff impl
+					BitfieldSize: 0,
+				},
+			},
+		},
+		"outer": {
+			Name: "outer",
+			Members: []restart.PersistedMemberLayout{
+				{
+					Name:         "x",
+					TypeName:     "inner",
+					Offset:       uint32(oldOuter.Members[0].Offset),
+					BitfieldSize: 0,
+					Nested: &restart.PersistedStructLayout{
+						Name: "inner",
+						Members: []restart.PersistedMemberLayout{
+							{Name: "a", TypeName: "uint32", Offset: uint32(oldInner.Members[0].Offset), BitfieldSize: 0},
+						},
+					},
+				},
+				{
+					Name:         "y",
+					TypeName:     "__u64",
+					Offset:       uint32(oldOuter.Members[1].Offset),
+					BitfieldSize: 0,
+				},
+			},
+		},
+	}
+
+	// new: inner_changed { b: uint32 }  <-- note: field name changed (a -> b)
+	newInnerChanged := &btf.Struct{
+		Name: "inner_changed",
+		Members: []btf.Member{
+			{
+				Name:         "b", // different name -> incompatible
+				Type:         intType("uint32", 4),
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	// new outer uses this new inner_changed type
+	newOuter := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         newInnerChanged,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// Compare persisted outer (StructInfo) against newOuter (btf.Struct).
+	diff := restart.DiffStructInfoAgainstBTF(registry["outer"], newOuter, make(map[string]bool))
+
+	// Expect nested change (because inner's member name changed a->b)
+	if !diff.NestedLayoutChanged && !diff.FieldTypeChanged && !diff.FieldRemoved && !diff.FieldAdded {
+		t.Fatalf("expected incompatibility detected (NestedLayoutChanged/FieldTypeChanged/FieldAdded/FieldRemoved), got %#v", diff)
+	}
+}
+
+// Test nested struct comparisons and compatibility path in migrateMap
+func TestDiffStructInfoAgainstBTF_NestedAndMigrateMap_Compatible(t *testing.T) {
+	// Build nested btf structs: inner { a:uint32 }, outer { x:inner, y:uint64 }
+	innerInt := intType("uint32", 4)
+	inner := &btf.Struct{
+		Name: "inner",
+		Members: []btf.Member{
+			{
+				Name:         "a",
+				Type:         innerInt,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+	outerUint := intType("__u64", 8)
+	outer := &btf.Struct{
+		Name: "outer",
+		Members: []btf.Member{
+			{
+				Name:         "x",
+				Type:         inner,
+				Offset:       btf.Bits(0),
+				BitfieldSize: btf.Bits(0),
+			},
+			{
+				Name:         "y",
+				Type:         outerUint,
+				Offset:       btf.Bits(32),
+				BitfieldSize: btf.Bits(0),
+			},
+		},
+	}
+
+	// persisted StructInfo registry: includes both inner and outer
+	registry := map[string]restart.PersistedStructLayout{
+		"inner": {
+			Name: "inner",
+			Members: []restart.PersistedMemberLayout{
+				{Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0},
+			},
+		},
+		"outer": {
+			Name: "outer",
+			Members: []restart.PersistedMemberLayout{
+				{Name: "x", TypeName: "inner", Offset: uint32(outer.Members[0].Offset), BitfieldSize: 0, Nested: &restart.PersistedStructLayout{
+					Name: "inner",
+					Members: []restart.PersistedMemberLayout{
+						{Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0},
+					},
+				}},
+				{Name: "y", TypeName: "__u64", Offset: uint32(outer.Members[1].Offset), BitfieldSize: 0},
+			},
+		},
+	}
+
+	// persisted map spec using outer
+	oldMapSpec := restart.PersistedMapSpec{
+		Name:            "km_nested_map",
+		Type:            ebpf.Hash.String(),
+		KeySize:         4,
+		ValueSize:       16,
+		MaxEntries:      128,
+		KeyStructInfo:   restart.PersistedStructLayout{Name: "int", Members: nil},
+		ValueStructInfo: registry["outer"],
+	}
+
+	// new compiled MapSpec that uses the same outer struct
+	newMapSpec := &ebpf.MapSpec{
+		Name:       "km_nested_map",
+		Type:       ebpf.Hash,
+		KeySize:    4,
+		ValueSize:  16,
+		MaxEntries: 128,
+		Key:        intType("int", 4), // key type non-struct in this test
+		Value:      outer,
+	}
+
+	// Call migrateMap: because the persisted value layout matches newMapSpec.Value,
+	// migrateMap should consider them compatible and return (nil, nil) (no creation).
+	m, err := restart.MigrateMap(&oldMapSpec, newMapSpec, "pkg", "mapNested", filepath.Join(t.TempDir(), "mapping"))
+	if err != nil {
+		t.Fatalf("migrateMap returned unexpected error: %v", err)
+	}
+	if m != nil {
+		t.Fatalf("expected nil map (reuse existing), got non-nil: %v", m)
+	}
+}
diff --git a/test/e2e/run_test.sh b/test/e2e/run_test.sh
index 74fa51bb..0fabb50e 100755
--- a/test/e2e/run_test.sh
+++ b/test/e2e/run_test.sh
@@ -176,6 +176,55 @@ function setup_kmesh() {
 	done
 }
 
+function set_daemonupgarde_testcase_image() {
+	local TMP_BUILD
+	TMP_BUILD="$(mktemp -d)"
+	echo "Building in temp dir: $TMP_BUILD"
+
+	git clone --depth 1 . "$TMP_BUILD" || {
+		echo "git clone failed"
+		rm -rf "$TMP_BUILD"
+		return 1
+	}
+
+	pushd "$TMP_BUILD" >/dev/null
+
+	BPF_HEADER_FILE="./bpf/include/bpf_common.h"
+	echo "Modifying BPF header file: ${BPF_HEADER_FILE}"
+
+	sed -i \
+		'/} kmesh_map64 SEC(".maps");/a\
+		\
+struct {\
+	__uint(type, BPF_MAP_TYPE_HASH);\
+	__uint(key_size, sizeof(__u32));\
+	__uint(value_size, MAP_VAL_SIZE_64);\
+	__uint(max_entries, MAP_MAX_ENTRIES);\
+	__uint(map_flags, BPF_F_NO_PREALLOC);\
+} kmesh_map64_bak_for_test SEC(".maps");' \
+		"${BPF_HEADER_FILE}"
+
+	local HUB="localhost:5000"
+	local TARGET="kmesh"
+	local TAG="test-upgrade-map-change"
+	local IMAGE="${HUB}/${TARGET}:${TAG}"
+
+	echo "Running 'make docker.push' with custom HUB and TAG in $TMP_BUILD"
+	if ! HUB=${HUB} TARGET=${TARGET} TAG=${TAG} make docker.push; then
+		echo "make docker.push failed"
+		popd >/dev/null
+		rm -rf "$TMP_BUILD"
+		return 1
+	fi
+
+	export KMESH_UPGRADE_IMAGE="${IMAGE}"
+	echo "Built and pushed image: ${IMAGE}"
+
+	popd >/dev/null
+	# rm -rf "$TMP_BUILD"
+	return 0
+}
+
 function setup_kmesh_log() {
 	# Set log of each Kmesh pods.
 	PODS=$(kubectl get pods -n kmesh-system -l app=kmesh -o jsonpath='{.items[*].metadata.name}')
@@ -346,6 +395,10 @@ while (("$#")); do
 		PARAMS+=("-istio.test.nocleanup")
 		shift
 		;;
+	--skip-build-daemonupgarde-image)
+		SKIP_BUILD_UPGARDE=true
+		shift
+		;;
 	*)
 		PARAMS+=("$1")
 		shift
@@ -367,6 +420,9 @@ if [[ -z ${SKIP_BUILD:-} ]]; then
 	setup_kind_registry
 	build_and_push_images
 	install_kmeshctl
+	if [[ -z ${SKIP_BUILD_UPGARDE:-} ]]; then
+		set_daemonupgarde_testcase_image
+	fi
 fi
 
 kubectl config use-context "kind-$NAME"
@@ -403,6 +459,6 @@ if [[ -n ${CLEANUP_REGISTRY} ]]; then
 	cleanup_docker_registry
 fi
 
-rm -rf "${TMP}"
+#rm -rf "${TMP}"
 
 exit $EXIT_CODE
diff --git a/test/e2e/upgarde_test.go b/test/e2e/upgarde_test.go
new file mode 100644
index 00000000..58d59b03
--- /dev/null
+++ b/test/e2e/upgarde_test.go
@@ -0,0 +1,137 @@
+//go:build integ
+// +build integ
+
+/*
+ * Copyright The Kmesh Authors.
+ *
+ * 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.
+ */
+
+// NOTE: THE CODE IN THIS FILE IS MAINLY REFERENCED FROM ISTIO INTEGRATION
+// FRAMEWORK(https://github.com/istio/istio/tree/master/tests/integration)
+// AND ADAPTED FOR KMESH.
+
+package kmesh
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"istio.io/istio/pkg/test/framework"
+	"istio.io/istio/pkg/test/framework/components/echo"
+	"istio.io/istio/pkg/test/framework/components/echo/util/traffic"
+	kubetest "istio.io/istio/pkg/test/kube"
+	"istio.io/istio/pkg/test/util/retry"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+// TestKmeshUpgrade performs a rolling upgrade of the kmesh daemonset image
+// while continuous traffic is flowing, and asserts there is no traffic disruption.
+func TestKmeshUpgrade(t *testing.T) {
+	framework.NewTest(t).Run(func(t framework.TestContext) {
+		configureDNSProxy(t, false)
+
+		src := apps.EnrolledToKmesh[0]
+		dst := apps.ServiceWithWaypointAtServiceGranularity
+		options := echo.CallOptions{
+			To:    dst,
+			Count: 1,
+			Check: httpValidator,
+			Port: echo.Port{
+				Name: "http",
+			},
+			Retry: echo.Retry{NoRetry: true},
+		}
+
+		g := traffic.NewGenerator(t, traffic.Config{
+			Source:   src,
+			Options:  options,
+			Interval: 50 * time.Millisecond,
+		}).Start()
+
+		upgradeKmesh(t)
+
+		g.Stop().CheckSuccessRate(t, 1)
+
+		configureDNSProxy(t, true)
+	})
+}
+
+// upgradeKmesh patches the daemonset image to the value of KMESH_UPGRADE_IMAGE and waits for rollout.
+func upgradeKmesh(t framework.TestContext) {
+	newImage := os.Getenv("KMESH_UPGRADE_IMAGE")
+	if newImage == "" {
+		newImage = "localhost:5000/kmesh"
+	}
+
+	patchData := fmt.Sprintf(`{
+		"spec": {
+			"template": {
+				"metadata": {
+					"annotations": {
+						"kmesh-upgrade-at": %q
+					}
+				},
+				"spec": {
+					"containers": [
+						{
+							"name": "kmesh",
+							"image": "%s"
+						}
+					]
+				}
+			}
+		}
+	}`, time.Now().Format(time.RFC3339), newImage)
+
+	patchKmesh_upgrade(t, patchData)
+}
+
+// patchKmesh applies a strategic merge patch to the Kmesh DaemonSet and waits for rollout completion.
+func patchKmesh_upgrade(t framework.TestContext, patchData string) {
+	patchOpts := metav1.PatchOptions{}
+	ds := t.Clusters().Default().Kube().AppsV1().DaemonSets(KmeshNamespace)
+	_, err := ds.Patch(context.Background(), KmeshDaemonsetName, types.StrategicMergePatchType, []byte(patchData), patchOpts)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := retry.UntilSuccess(func() error {
+		d, err := ds.Get(context.Background(), KmeshDaemonsetName, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+		if !daemonsetsetComplete_upgrade(d) {
+			return fmt.Errorf("rollout is not yet done")
+		}
+		return nil
+	}, retry.Timeout(120*time.Second), retry.Delay(2*time.Second)); err != nil {
+		t.Fatalf("failed to wait for Kmesh rollout status: %v", err)
+	}
+
+	if _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(t.AllClusters()[0], KmeshNamespace, "app=kmesh")); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// daemonsetsetComplete returns true when DaemonSet rollout appears complete.
+func daemonsetsetComplete_upgrade(ds *appsv1.DaemonSet) bool {
+	return ds.Status.UpdatedNumberScheduled == ds.Status.DesiredNumberScheduled &&
+		ds.Status.NumberReady == ds.Status.DesiredNumberScheduled &&
+		ds.Status.ObservedGeneration >= ds.Generation
+}