From f3d235d101147ccffeb04c989bc9bd792e3c89a4 Mon Sep 17 00:00:00 2001 From: luozhixian Date: Fri, 10 Jul 2026 11:23:36 +0800 Subject: [PATCH 1/5] fix(mmap): sysinfo reports 0 memory and msync rejects MS_INVALIDATE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs that prevented LTP mmap2 from running: 1. sysinfo() totalram/freeram were hardcoded to 0, causing mmap2 to report "Available memory: 0MB" and exit early. Fix: populate them from the buddy allocator (ksyscall). 2. msync(MS_SYNC|MS_INVALIDATE) returned EOPNOTSUPP because the invalidate flag was rejected early in msync_range() and FileSharedRuntime::msync_impl(). Fix: remove the early rejection and implement the full semantic — write back dirty pages, then invalidate the page cache range so subsequent reads re-fetch from disk (mm/memspace, mm/filemap, mm/pagecache, fs/kvfs). After these fixes mmap2 passes single-run (./mmap2 without -x). Looping mode (-x N) still fails after several iterations due to a separate ext4 extent-tree writeback coherency issue (tracked in docs/known-issues/ltp-mmap2-sysinfo-msync.md). --- core/ksyscall/Cargo.toml | 1 + core/ksyscall/src/sys.rs | 8 ++++++++ fs/kvfs/src/address_space.rs | 13 +++++++++++++ fs/kvfs/src/file.rs | 5 +++++ mm/filemap/src/shared.rs | 22 +++++++++++++--------- mm/memspace/src/aspace.rs | 3 --- mm/pagecache/src/lib.rs | 31 +++++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+), 12 deletions(-) diff --git a/core/ksyscall/Cargo.toml b/core/ksyscall/Cargo.toml index abc551edf..6f3817fbc 100644 --- a/core/ksyscall/Cargo.toml +++ b/core/ksyscall/Cargo.toml @@ -20,6 +20,7 @@ tee_ta_sign = ["tee_task_iface?/tee_ta_sign"] ebpf = ["dep:posix-bpf", "posix-bpf/ebpf", "posix-fs/ebpf"] [dependencies] +kalloc.workspace = true kaddr_layout.workspace = true kfd_objects.workspace = true kfutex.workspace = true diff --git a/core/ksyscall/src/sys.rs b/core/ksyscall/src/sys.rs index 8f658a808..dc3dbfa62 100644 --- a/core/ksyscall/src/sys.rs +++ b/core/ksyscall/src/sys.rs @@ -9,8 +9,10 @@ //! - Process information queries //! - Hostname management +use kalloc::global_allocator; use kbuild_config::ARCH; use kerrno::KResult; +use khal::mem::PAGE_SIZE_4K; use kprocess::{current_user_process, current_user_process_fs_context}; use kvfs::{Filename, NodePermission}; use linux_raw_sys::{ @@ -99,6 +101,12 @@ pub fn sys_sysinfo(info: UserPtr) -> KResult { }; kinfo.procs = kprocess::system_view::process_count() as _; kinfo.mem_unit = 1; + + let alloc = global_allocator(); + let total_pages = alloc.used_pages() + alloc.available_pages(); + kinfo.totalram = (total_pages * PAGE_SIZE_4K) as _; + kinfo.freeram = (alloc.available_pages() * PAGE_SIZE_4K) as _; + info.write_vm(kinfo)?; Ok(0) } diff --git a/fs/kvfs/src/address_space.rs b/fs/kvfs/src/address_space.rs index 5ce17d3db..8a22f373f 100644 --- a/fs/kvfs/src/address_space.rs +++ b/fs/kvfs/src/address_space.rs @@ -574,6 +574,19 @@ impl AddressSpace { Ok(()) } + /// Drops cached pages in `[offset, offset + len)` after writeback. + pub fn invalidate_pages(&self, offset: u64, len: usize) -> VfsResult<()> { + if len == 0 { + return Ok(()); + } + let start_page = (offset / PAGE_SIZE_4K as u64) as PageIndex; + let end = offset.saturating_add(len as u64); + let end_page = end.div_ceil(PAGE_SIZE_4K as u64) as PageIndex; + self.page_cache + .invalidate_page_range(start_page, end_page)?; + Ok(()) + } + pub(crate) fn writeback_cached_folios( &self, control: &mut WritebackControl, diff --git a/fs/kvfs/src/file.rs b/fs/kvfs/src/file.rs index e0bad5041..ea755c3f1 100644 --- a/fs/kvfs/src/file.rs +++ b/fs/kvfs/src/file.rs @@ -585,6 +585,11 @@ impl VfsFile { self.mapping().writepages_range(start, len, data_only) } + /// Drops cached pages in `[start, start + len)` after writeback. + pub fn invalidate_pages(&self, start: u64, len: usize) -> VfsResult<()> { + self.mapping().invalidate_pages(start, len) + } + /// Returns the maximum regular-file size allowed for this open file. pub fn max_file_size(&self) -> u64 { self.path().max_file_size() diff --git a/mm/filemap/src/shared.rs b/mm/filemap/src/shared.rs index ba8bffabd..a012c82bf 100644 --- a/mm/filemap/src/shared.rs +++ b/mm/filemap/src/shared.rs @@ -339,20 +339,24 @@ impl FileSharedRuntime { range: VirtAddrRange, policy: MsyncPolicy, ) -> KResult { - if policy.has_invalidate() { - return Err(KError::OperationNotSupported); - } - if !policy.is_sync() { + if !policy.is_sync() && !policy.has_invalidate() { return Ok(MsyncRuntimeResult::Synced); } let object_start = vma .file_offset_for(range.start) .ok_or(KError::InvalidInput)?; - self.0.source.file().writeback_mapping_range( - object_start, - range.size(), - policy.is_data_only(), - )?; + let file = self.0.source.file(); + + // Write back dirty pages so data reaches disk before we + // potentially drop cached copies below. + file.writeback_mapping_range(object_start, range.size(), policy.is_data_only())?; + + // MS_INVALIDATE: discard cached pages so subsequent reads + // re-fetch from disk. + if policy.has_invalidate() { + file.invalidate_pages(object_start, range.size())?; + } + Ok(MsyncRuntimeResult::Synced) } } diff --git a/mm/memspace/src/aspace.rs b/mm/memspace/src/aspace.rs index 5b5bfa60f..6b3917446 100644 --- a/mm/memspace/src/aspace.rs +++ b/mm/memspace/src/aspace.rs @@ -1208,9 +1208,6 @@ impl MmSpace { if size == 0 { return Ok(()); } - if policy.has_invalidate() { - return Err(KError::OperationNotSupported); - } let range = VirtAddrRange::from_start_size(start, size); let overlapped_vmas = self.vmas.collect_overlapping(range); diff --git a/mm/pagecache/src/lib.rs b/mm/pagecache/src/lib.rs index 7885701ef..9fa802ed3 100644 --- a/mm/pagecache/src/lib.rs +++ b/mm/pagecache/src/lib.rs @@ -929,6 +929,37 @@ impl Mapping { Ok(dropped_folios.into_iter().map(|(index, _)| index).collect()) } + /// Drops cached folios in the page index range `[start, end)`. + /// + /// Callers must write back dirty pages before calling this method. + pub fn invalidate_page_range( + &self, + start: PageIndex, + end: PageIndex, + ) -> KResult> { + if start >= end { + return Ok(Vec::new()); + } + let dropped_folios = { + let mut inner = self.inner.lock(); + let dropped = inner + .pages + .range(start..end) + .map(|(index, folio)| (*index, folio.clone())) + .collect::>(); + for (index, _) in &dropped { + inner.pages.remove(index); + } + dropped + }; + assert!( + dropped_folios.iter().all(|(_, f)| !f.lock().is_dirty()), + "invalidate_page_range: dirty folios in eviction range — call writeback first" + ); + self.notify_evicted_folios(&dropped_folios); + Ok(dropped_folios.into_iter().map(|(index, _)| index).collect()) + } + /// Drops all cached folios during final mapping teardown. /// /// Unlike ordinary invalidation, final teardown may discard dirty folios -- Gitee From 3cc2d991aae83dc609f1ed4ff95655ab392f1a0d Mon Sep 17 00:00:00 2001 From: luozhixian Date: Fri, 10 Jul 2026 14:02:57 +0800 Subject: [PATCH 2/5] update Signed-off-by: luozhixian --- core/ksyscall/src/sys.rs | 12 ++++++------ fs/kvfs/src/address_space.rs | 8 +++++++- mm/pagecache/src/lib.rs | 14 +++++--------- posix/mm/docs/security.md | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/core/ksyscall/src/sys.rs b/core/ksyscall/src/sys.rs index dc3dbfa62..343f833b1 100644 --- a/core/ksyscall/src/sys.rs +++ b/core/ksyscall/src/sys.rs @@ -9,10 +9,9 @@ //! - Process information queries //! - Hostname management -use kalloc::global_allocator; use kbuild_config::ARCH; use kerrno::KResult; -use khal::mem::PAGE_SIZE_4K; +use khal::mem; use kprocess::{current_user_process, current_user_process_fs_context}; use kvfs::{Filename, NodePermission}; use linux_raw_sys::{ @@ -102,10 +101,11 @@ pub fn sys_sysinfo(info: UserPtr) -> KResult { kinfo.procs = kprocess::system_view::process_count() as _; kinfo.mem_unit = 1; - let alloc = global_allocator(); - let total_pages = alloc.used_pages() + alloc.available_pages(); - kinfo.totalram = (total_pages * PAGE_SIZE_4K) as _; - kinfo.freeram = (alloc.available_pages() * PAGE_SIZE_4K) as _; + let alloc = kalloc::global_allocator(); + let avail = alloc.available_pages(); + let total_pages = alloc.used_pages() + avail; + kinfo.totalram = total_pages.saturating_mul(mem::PAGE_SIZE_4K) as _; + kinfo.freeram = avail.saturating_mul(mem::PAGE_SIZE_4K) as _; info.write_vm(kinfo)?; Ok(0) diff --git a/fs/kvfs/src/address_space.rs b/fs/kvfs/src/address_space.rs index 8a22f373f..eb8939376 100644 --- a/fs/kvfs/src/address_space.rs +++ b/fs/kvfs/src/address_space.rs @@ -581,7 +581,13 @@ impl AddressSpace { } let start_page = (offset / PAGE_SIZE_4K as u64) as PageIndex; let end = offset.saturating_add(len as u64); - let end_page = end.div_ceil(PAGE_SIZE_4K as u64) as PageIndex; + // Avoid div_ceil overflow when end == u64::MAX (saturating_add + // can saturate to u64::MAX but div_ceil adds rhs-1 before division). + let end_page = if end == u64::MAX { + u64::MAX / PAGE_SIZE_4K as u64 + 1 + } else { + end.div_ceil(PAGE_SIZE_4K as u64) + } as PageIndex; self.page_cache .invalidate_page_range(start_page, end_page)?; Ok(()) diff --git a/mm/pagecache/src/lib.rs b/mm/pagecache/src/lib.rs index 9fa802ed3..72e2d3eea 100644 --- a/mm/pagecache/src/lib.rs +++ b/mm/pagecache/src/lib.rs @@ -921,7 +921,7 @@ impl Mapping { dropped }; // Callers must writeback before invalidation. - assert!( + debug_assert!( dropped_folios.iter().all(|(_, f)| !f.lock().is_dirty()), "invalidate_from_page: dirty folios in eviction range — call writeback first" ); @@ -932,13 +932,9 @@ impl Mapping { /// Drops cached folios in the page index range `[start, end)`. /// /// Callers must write back dirty pages before calling this method. - pub fn invalidate_page_range( - &self, - start: PageIndex, - end: PageIndex, - ) -> KResult> { + pub fn invalidate_page_range(&self, start: PageIndex, end: PageIndex) -> KResult<()> { if start >= end { - return Ok(Vec::new()); + return Ok(()); } let dropped_folios = { let mut inner = self.inner.lock(); @@ -952,12 +948,12 @@ impl Mapping { } dropped }; - assert!( + debug_assert!( dropped_folios.iter().all(|(_, f)| !f.lock().is_dirty()), "invalidate_page_range: dirty folios in eviction range — call writeback first" ); self.notify_evicted_folios(&dropped_folios); - Ok(dropped_folios.into_iter().map(|(index, _)| index).collect()) + Ok(()) } /// Drops all cached folios during final mapping teardown. diff --git a/posix/mm/docs/security.md b/posix/mm/docs/security.md index e2827877d..4d9165141 100644 --- a/posix/mm/docs/security.md +++ b/posix/mm/docs/security.md @@ -90,7 +90,7 @@ | `munmap` 范围非法 | 长度为 0 或下游地址空间拒绝 | 返回错误 | 不修改地址空间 | | `mprotect` grow flag 不支持 | `PROT_GROWSDOWN` 或 `PROT_GROWSUP` | 返回错误 | protect 失败,不修改 PTE/VMA | | `MADV_DONTNEED` 范围非法 | 地址未映射、未页对齐、或 `addr + len` 溢出 | 返回错误 | advice 失败,不修改对象 | -| `msync` flag 不支持 | unknown flags、`MS_ASYNC | MS_SYNC`、或 `MS_INVALIDATE` | 返回错误 | sync 失败,不修改对象 | +| `msync` flag 不支持 | unknown flags 或 `MS_ASYNC | MS_SYNC` 组合 | 返回错误 | sync 失败,不修改对象 | | shared file writeback 失败 | VFS `AddressSpaceOperations::writepages()` 失败 | 返回错误,dirty bit 保留 | syscall 失败,调用者可重试 | ## 故障管理 -- Gitee From 33c6bfe45abe8ccb9efe857b1233b1d945545ebe Mon Sep 17 00:00:00 2001 From: luozhixian Date: Fri, 10 Jul 2026 14:19:01 +0800 Subject: [PATCH 3/5] pte zapping Signed-off-by: luozhixian --- mm/filemap/src/shared.rs | 14 +++++++++----- mm/memspace/src/aspace.rs | 12 ++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/mm/filemap/src/shared.rs b/mm/filemap/src/shared.rs index a012c82bf..0272456f2 100644 --- a/mm/filemap/src/shared.rs +++ b/mm/filemap/src/shared.rs @@ -339,6 +339,7 @@ impl FileSharedRuntime { range: VirtAddrRange, policy: MsyncPolicy, ) -> KResult { + // MS_ASYNC without invalidate: no-op (no async writeback infra yet). if !policy.is_sync() && !policy.has_invalidate() { return Ok(MsyncRuntimeResult::Synced); } @@ -347,12 +348,15 @@ impl FileSharedRuntime { .ok_or(KError::InvalidInput)?; let file = self.0.source.file(); - // Write back dirty pages so data reaches disk before we - // potentially drop cached copies below. - file.writeback_mapping_range(object_start, range.size(), policy.is_data_only())?; + // MS_SYNC: write back dirty pages to disk. + if policy.is_sync() { + file.writeback_mapping_range(object_start, range.size(), policy.is_data_only())?; + } - // MS_INVALIDATE: discard cached pages so subsequent reads - // re-fetch from disk. + // MS_INVALIDATE: discard cached pages and zap PTEs so subsequent + // reads re-fetch from disk. When combined with MS_SYNC dirty data + // is first written back above; without MS_SYNC dirty data in the + // range is intentionally discarded. if policy.has_invalidate() { file.invalidate_pages(object_start, range.size())?; } diff --git a/mm/memspace/src/aspace.rs b/mm/memspace/src/aspace.rs index 6b3917446..d907b18b6 100644 --- a/mm/memspace/src/aspace.rs +++ b/mm/memspace/src/aspace.rs @@ -1213,6 +1213,8 @@ impl MmSpace { let overlapped_vmas = self.vmas.collect_overlapping(range); let mut cursor = start; let mut saw_hole = overlapped_vmas.is_empty(); + // Collect (range, page_size) pairs for PTE zapping after sync. + let mut zap_ranges: Vec<(VirtAddrRange, PageSize)> = Vec::new(); for vma in overlapped_vmas { if vma.start() > cursor { @@ -1222,6 +1224,9 @@ impl MmSpace { if !overlap.is_empty() { let runtime = self.runtime_for_vma(&vma).ok_or(KError::BadAddress)?; runtime.msync(&vma, overlap, policy)?; + if policy.has_invalidate() { + zap_ranges.push((overlap, vma.backing().page_size())); + } } cursor = cursor.max(vma.end()); if cursor >= range.end { @@ -1235,6 +1240,13 @@ impl MmSpace { if saw_hole { return Err(KError::NoMemory); } + + // MS_INVALIDATE: after writeback + page cache eviction, zap the + // page table entries so that subsequent accesses trigger page + // faults and re-read the latest data from the backing store. + if !zap_ranges.is_empty() { + self.zap_present_ranges(zap_ranges)?; + } Ok(()) } -- Gitee From bd98549ea6334895efbdbe8610d972e32b5c97d5 Mon Sep 17 00:00:00 2001 From: luozhixian Date: Fri, 10 Jul 2026 15:17:06 +0800 Subject: [PATCH 4/5] update Signed-off-by: luozhixian --- fs/kvfs/src/address_space.rs | 19 ------------------- fs/kvfs/src/file.rs | 5 ----- mm/filemap/src/shared.rs | 26 +++++++++----------------- mm/memspace/src/aspace.rs | 14 ++------------ mm/pagecache/src/lib.rs | 27 --------------------------- 5 files changed, 11 insertions(+), 80 deletions(-) diff --git a/fs/kvfs/src/address_space.rs b/fs/kvfs/src/address_space.rs index eb8939376..5ce17d3db 100644 --- a/fs/kvfs/src/address_space.rs +++ b/fs/kvfs/src/address_space.rs @@ -574,25 +574,6 @@ impl AddressSpace { Ok(()) } - /// Drops cached pages in `[offset, offset + len)` after writeback. - pub fn invalidate_pages(&self, offset: u64, len: usize) -> VfsResult<()> { - if len == 0 { - return Ok(()); - } - let start_page = (offset / PAGE_SIZE_4K as u64) as PageIndex; - let end = offset.saturating_add(len as u64); - // Avoid div_ceil overflow when end == u64::MAX (saturating_add - // can saturate to u64::MAX but div_ceil adds rhs-1 before division). - let end_page = if end == u64::MAX { - u64::MAX / PAGE_SIZE_4K as u64 + 1 - } else { - end.div_ceil(PAGE_SIZE_4K as u64) - } as PageIndex; - self.page_cache - .invalidate_page_range(start_page, end_page)?; - Ok(()) - } - pub(crate) fn writeback_cached_folios( &self, control: &mut WritebackControl, diff --git a/fs/kvfs/src/file.rs b/fs/kvfs/src/file.rs index ea755c3f1..e0bad5041 100644 --- a/fs/kvfs/src/file.rs +++ b/fs/kvfs/src/file.rs @@ -585,11 +585,6 @@ impl VfsFile { self.mapping().writepages_range(start, len, data_only) } - /// Drops cached pages in `[start, start + len)` after writeback. - pub fn invalidate_pages(&self, start: u64, len: usize) -> VfsResult<()> { - self.mapping().invalidate_pages(start, len) - } - /// Returns the maximum regular-file size allowed for this open file. pub fn max_file_size(&self) -> u64 { self.path().max_file_size() diff --git a/mm/filemap/src/shared.rs b/mm/filemap/src/shared.rs index 0272456f2..6660aa9d4 100644 --- a/mm/filemap/src/shared.rs +++ b/mm/filemap/src/shared.rs @@ -339,28 +339,20 @@ impl FileSharedRuntime { range: VirtAddrRange, policy: MsyncPolicy, ) -> KResult { - // MS_ASYNC without invalidate: no-op (no async writeback infra yet). - if !policy.is_sync() && !policy.has_invalidate() { + // MS_ASYNC (and MS_INVALIDATE) are no-ops at the VMA level — + // Linux never actually evicts or zaps pages for MS_INVALIDATE + // because the page cache is globally coherent. + if !policy.is_sync() { return Ok(MsyncRuntimeResult::Synced); } let object_start = vma .file_offset_for(range.start) .ok_or(KError::InvalidInput)?; - let file = self.0.source.file(); - - // MS_SYNC: write back dirty pages to disk. - if policy.is_sync() { - file.writeback_mapping_range(object_start, range.size(), policy.is_data_only())?; - } - - // MS_INVALIDATE: discard cached pages and zap PTEs so subsequent - // reads re-fetch from disk. When combined with MS_SYNC dirty data - // is first written back above; without MS_SYNC dirty data in the - // range is intentionally discarded. - if policy.has_invalidate() { - file.invalidate_pages(object_start, range.size())?; - } - + self.0.source.file().writeback_mapping_range( + object_start, + range.size(), + policy.is_data_only(), + )?; Ok(MsyncRuntimeResult::Synced) } } diff --git a/mm/memspace/src/aspace.rs b/mm/memspace/src/aspace.rs index d907b18b6..f12d3d524 100644 --- a/mm/memspace/src/aspace.rs +++ b/mm/memspace/src/aspace.rs @@ -1213,8 +1213,6 @@ impl MmSpace { let overlapped_vmas = self.vmas.collect_overlapping(range); let mut cursor = start; let mut saw_hole = overlapped_vmas.is_empty(); - // Collect (range, page_size) pairs for PTE zapping after sync. - let mut zap_ranges: Vec<(VirtAddrRange, PageSize)> = Vec::new(); for vma in overlapped_vmas { if vma.start() > cursor { @@ -1222,11 +1220,10 @@ impl MmSpace { } let overlap = VirtAddrRange::new(cursor.max(vma.start()), range.end.min(vma.end())); if !overlap.is_empty() { + // TODO: when mlock is implemented, check VM_LOCKED and + // return EBUSY for MS_INVALIDATE as Linux does. let runtime = self.runtime_for_vma(&vma).ok_or(KError::BadAddress)?; runtime.msync(&vma, overlap, policy)?; - if policy.has_invalidate() { - zap_ranges.push((overlap, vma.backing().page_size())); - } } cursor = cursor.max(vma.end()); if cursor >= range.end { @@ -1240,13 +1237,6 @@ impl MmSpace { if saw_hole { return Err(KError::NoMemory); } - - // MS_INVALIDATE: after writeback + page cache eviction, zap the - // page table entries so that subsequent accesses trigger page - // faults and re-read the latest data from the backing store. - if !zap_ranges.is_empty() { - self.zap_present_ranges(zap_ranges)?; - } Ok(()) } diff --git a/mm/pagecache/src/lib.rs b/mm/pagecache/src/lib.rs index 72e2d3eea..9caf31cbf 100644 --- a/mm/pagecache/src/lib.rs +++ b/mm/pagecache/src/lib.rs @@ -929,33 +929,6 @@ impl Mapping { Ok(dropped_folios.into_iter().map(|(index, _)| index).collect()) } - /// Drops cached folios in the page index range `[start, end)`. - /// - /// Callers must write back dirty pages before calling this method. - pub fn invalidate_page_range(&self, start: PageIndex, end: PageIndex) -> KResult<()> { - if start >= end { - return Ok(()); - } - let dropped_folios = { - let mut inner = self.inner.lock(); - let dropped = inner - .pages - .range(start..end) - .map(|(index, folio)| (*index, folio.clone())) - .collect::>(); - for (index, _) in &dropped { - inner.pages.remove(index); - } - dropped - }; - debug_assert!( - dropped_folios.iter().all(|(_, f)| !f.lock().is_dirty()), - "invalidate_page_range: dirty folios in eviction range — call writeback first" - ); - self.notify_evicted_folios(&dropped_folios); - Ok(()) - } - /// Drops all cached folios during final mapping teardown. /// /// Unlike ordinary invalidation, final teardown may discard dirty folios -- Gitee From fa12d6f051c3ce6220897c13fc3854aba10f9ce6 Mon Sep 17 00:00:00 2001 From: luozhixian Date: Fri, 10 Jul 2026 15:19:28 +0800 Subject: [PATCH 5/5] update Signed-off-by: luozhixian --- mm/pagecache/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm/pagecache/src/lib.rs b/mm/pagecache/src/lib.rs index 9caf31cbf..7885701ef 100644 --- a/mm/pagecache/src/lib.rs +++ b/mm/pagecache/src/lib.rs @@ -921,7 +921,7 @@ impl Mapping { dropped }; // Callers must writeback before invalidation. - debug_assert!( + assert!( dropped_folios.iter().all(|(_, f)| !f.lock().is_dirty()), "invalidate_from_page: dirty folios in eviction range — call writeback first" ); -- Gitee