Post

Replies

Boosts

Views

Activity

Reply to Code Signing or Xcode adding mysterious entitlements that not exist in project
So finally I found a way to fix the wrong behavior of the Xcode build and archive process ... The following script snippet patching the wrong files to the right one. # Type a script or drag a script file from your workspace to insert its path. # NOTE!!! -> Paste the script into Xcode Build Phase - custom build script # Input files: # ---> ${SOURCE_ROOT}/../../Entitlements.plist # ---> ${SOURCE_ROOT}/../../DRFXBuilder.app.xcent # Output files: # LEAVE EMPTY echo "-------- PATCH WRONG ENTITLEMENTS ---------" echo "-- build env" printenv ##-- mkdir -p ${BUILD_ROOT}/DRFXBuilder.build/Debug/DRFXBuilder.build/DerivedSources cp -pv ${SOURCE_ROOT}/../../Entitlements.plist ${BUILD_ROOT}/DRFXBuilder.build/Debug/DRFXBuilder.build/DerivedSources/Entitlements.plist cp -pv ${SOURCE_ROOT}/../../DRFXBuilder.app.xcent ${BUILD_ROOT}/DRFXBuilder.build/Debug/DRFXBuilder.build/DRFXBuilder.app.xcent ##-- mkdir -p ${BUILD_ROOT}/DRFXBuilder.build/Release/DRFXBuilder.build/DerivedSources cp -pv ${SOURCE_ROOT}/../../Entitlements.plist ${BUILD_ROOT}/DRFXBuilder.build/Release/DRFXBuilder.build/DerivedSources/Entitlements.plist cp -pv ${SOURCE_ROOT}/../../DRFXBuilder.app.xcent ${BUILD_ROOT}/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent ##-- mkdir -p ${TARGET_TEMP_DIR}/DerivedSources cp -pv ${SOURCE_ROOT}/../../Entitlements.plist ${TARGET_TEMP_DIR}/DerivedSources/Entitlements.plist cp -pv ${SOURCE_ROOT}/../../DRFXBuilder.app.xcent ${TARGET_TEMP_DIR}/DRFXBuilder.app.xcent
Topic: Code Signing SubTopic: Entitlements Tags:
Jul ’25
Reply to Code Signing or Xcode adding mysterious entitlements that not exist in project
.... AND MORE FUNNY ... I removed the Sandbox entitlement and the Xcode artificial stupidity still want to insert the entitlements !!! ProcessProductPackaging /Users/xxxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent (in target 'DRFXBuilder' from project 'DRFXBuilder') cd /Users/xxxxx/EoF/DRFXBuilder/build/xcode Entitlements: { } builtin-productPackagingUtility /Users/xxxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements -entitlements -format xml -o /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent ProcessProductPackagingDER /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent.der (in target 'DRFXBuilder' from project 'DRFXBuilder') cd /Users/xxxxx/EoF/DRFXBuilder/build/xcode /usr/bin/derq query -f xml -i /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent -o /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app-Simulated.xcent.der --raw WriteAuxiliaryFile /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DerivedSources/Entitlements.plist (in target 'DRFXBuilder' from project 'DRFXBuilder') cd /Users/xxxxx/EoF/DRFXBuilder/build/xcode write-file /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DerivedSources/Entitlements.plist ProcessProductPackaging /Users/xxxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent (in target 'DRFXBuilder' from project 'DRFXBuilder') cd /Users/xxxxx/EoF/DRFXBuilder/build/xcode Entitlements: { "com.apple.application-identifier" = "IDIDID.org.eof.tools.DRFXBuilder"; "com.apple.developer.team-identifier" = IDIDID; "com.apple.security.application-groups" = ( "group.org.eof.apps" ); "com.apple.security.assets.movies.read-write" = 1; "com.apple.security.assets.music.read-write" = 1; "com.apple.security.assets.pictures.read-write" = 1; "com.apple.security.files.downloads.read-write" = 1; "com.apple.security.files.user-selected.read-write" = 1; } builtin-productPackagingUtility /Users/xxxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements -entitlements -format xml -o /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent error: could not write entitlements file '/Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent': Permission denied (13) (in target 'DRFXBuilder' from project 'DRFXBuilder') So I research deep into /var/folders and I found com.apple.dt.Xcode and some Developer stuff. I removed this directories, and WoW the entitlements are gone in a next build. MY conclusion is: Your build software has a stupid BUG! Xcode or whatever referencing any outdated temporary stuff from /var/folders instead to use the user decisions in the project tree. That's why I got every time rejects from you App Store validation !!!!!! Regards and happy code analysis and I wait for next Xcode and developer tools update
Topic: Code Signing SubTopic: Entitlements Tags:
Jul ’25
Reply to Code Signing or Xcode adding mysterious entitlements that not exist in project
I removed the permissions for a temporary file during the build process. My entitlements file doesn't contain any of the entitlements mentioned. Why does the code sign tool or Xcode's built-in ProductPackagingUtility have to change my decisions? Is this artificial stupidity? ProcessProductPackaging /Users/xxxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent (in target 'DRFXBuilder' from project 'DRFXBuilder') cd /Users/xxxxx/EoF/DRFXBuilder/build/xcode Entitlements: { "com.apple.application-identifier" = "IDIDIDIDI.org.eof.tools.DRFXBuilder"; "com.apple.developer.team-identifier" = IDIDIDID; "com.apple.security.app-sandbox" = 1; "com.apple.security.application-groups" = ( "group.org.eof.apps" ); "com.apple.security.assets.movies.read-write" = 1; <<<<- NOT MY DECISION "com.apple.security.assets.music.read-write" = 1; <<<<- NOT MY DECISION "com.apple.security.assets.pictures.read-write" = 1; <<<<- NOT MY DECISION "com.apple.security.files.downloads.read-write" = 1; <<<<- NOT MY DECISION "com.apple.security.files.user-selected.read-write" = 1; <<<<<- YES MY DECISION } builtin-productPackagingUtility /Users/xxxx/EoF/DRFXBuilder/DRFXBuilder_sandbox.entitlements -entitlements -format xml -o /Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent error: could not write entitlements file '/Users/xxxxx/EoF/DRFXBuilder/build/xcode/DRFXBuilder.build/Release/DRFXBuilder.build/DRFXBuilder.app.xcent': Permission denied (13) (in target 'DRFXBuilder' from project 'DRFXBuilder')
Topic: Code Signing SubTopic: Entitlements Tags:
Jul ’25
Reply to DriverKit IOUserSerial Driver
Hello Kevin, Me again :) The VSPDriver (Virtual Serial Port DEXT) project is, from my point of view, finished for now. Since I discovered that it is more than complicated to get my client app including the DEXT into the AppStore (Entitlements things), hence the request: If you could use the code at Apple and build this extension into the OS, that would certainly be helpful for many other (embedded/device) developers. The corresponding client app that controls the VSP Driver Extension is in the repository on GitHub. The code is certainly interesting for you too. Maybe you can build a developer tool app from it and integrate it into the OS (maybe under /System/CoreServices :-)
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Hello Keven, the serial port driver extension executes a local echo. This works well so far. In the top instance (VSPDriver) there is a mechanism that connects two serial ports to each other. In the serial port instance (VSPSerialPort) in the method 'RxEchoAsyncEvent' it is checked whether a link exists. If so, then the TX packet should be transmitted as an echo to the other serial port as RX. According to the following pattern: Port1-TX -> Port2-RX. My problem: If I fill the Port2 RX-IOMemoryDescriptor (Buffer) with the data from Port1-TX, only one character is displayed in the terminal. No further data arrives on the terminal that is connected to Port2. Now the all-important $1 million question: What else needs to be done so that Port2 continuously sends the data to the client (e.g. Serial IO Terminal)? The workflow Port1-TX -> TxDataAvailable() -> Enque data and dispatch async -> Same Port1 object instance gets IODataQueueDispatchSource::DataAvailable event (RxEchoAsyncEvent) -> This method then calls Port2-RX 'sendResponse' and fills the buffer of rxqmd and updates the fields in the SerialPortInterface of Port2-RX accordingly. Best regards VSPSerialPort.cpp.txt VSPDriver.cpp.txt VSPUserClient.cpp.txt
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Ok, the problem with the buffer overrun has been fixed :) Is a dirty? solution: // -------------------------------------------------------------- // TxDataAvailable_Impl() // TX data ready to read from m_txqbmd segment void IMPL(VSPSerialPort, TxDataAvailable) { IOReturn ret; uint8_t* buffer; uint64_t address; uint32_t size; bool needReset = false; VSPLog(LOG_PREFIX, "----------------------------------\n"); VSPLog(LOG_PREFIX, "TxDataAvailable called.\n"); // Lock to ensure thread safety VSPAquireLock(ivars); // Reset first ivars->m_txIsComplete = false; // We working... ivars->m_hwStatus.cts = false; ivars->m_hwStatus.dsr = false; // Show me indexes be fore manipulate VSPLog(LOG_PREFIX, "TxDataAvailable: [IOSPI-TX 1] txPI: %d, txCI: %d, txqoffset: %d, txqlogsz: %d", ivars->m_spi->txPI, ivars->m_spi->txCI, ivars->m_spi->txqoffset, ivars->m_spi->txqlogsz); // skip if nothing to do if (!ivars->m_spi->txPI) { VSPLog(LOG_PREFIX, "TxDataAvailable: spi->txPI is zero, skip\n"); goto finish; } // Get address of new TX data position address = ivars->m_txseg.address + ivars->m_spi->txCI; buffer = reinterpret_cast<uint8_t*>(address); // Calculate available data in TX buffer size = ivars->m_spi->txPI - ivars->m_spi->txCI; #ifdef DEBUG // !! Debug .... VSPLog(LOG_PREFIX, "TxDataAvailable: dump buffer=0x%llx len=%u\n", (uint64_t) buffer, size); for (uint64_t i = 0; i < size; i++) { VSPLog(LOG_PREFIX, "TxDataAvailable: buffer[%02lld]=0x%02x %c\n", i, buffer[i], buffer[i]); } #endif // Loopback TX data by async response event if ((ret = this->enqueueResponse(buffer, size)) != kIOReturnSuccess) { VSPLog(LOG_PREFIX, "TxDataAvailable: Unable to enqueue response. code=%d\n", ret); goto finish; } // TX -> RX echo done ivars->m_txIsComplete = true; // We reserve 1K size from the capacity from t_txqbmd. This protects // against a buffer overflow. if ((ivars->m_spi->txPI + 1024) >= (ivars->m_txseg.length - 1024)) { ivars->m_spi->txPI = 0; ivars->m_spi->txCI = 0; } // Reset TX consumer index to end of received block. This increases the // offset for the m_txqbmd buffer. Finally the DEXT crash if to protection. // Reset values like txPI = 0 and txCI = 0 after some transmissions. else { ivars->m_spi->txCI = ivars->m_spi->txPI; } // Show me indexes be fore manipulation VSPLog(LOG_PREFIX, "TxDataAvailable: [IOSPI-TX 2] txPI: %d, txCI: %d, txqoffset: %d, txqlogsz: %d", ivars->m_spi->txPI, ivars->m_spi->txCI, ivars->m_spi->txqoffset, ivars->m_spi->txqlogsz); // reset memory memset(buffer, 0, size); VSPLog(LOG_PREFIX, "TxDataAvailable complete.\n"); finish: VSPUnlock(ivars); }
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Hi Kevin, okay got it. // Calculate available data in TX buffer size = ivars->m_spi->txPI - ivars->m_spi->txCI; // Get address of new TX data position address = ivars->m_txseg.address + ivars->m_spi->txCI; buffer = reinterpret_cast<uint8_t*>(address); /* -- do something with the buffer content -- */ // Notify OS // Reset TX consumer index to end of received block ivars->m_spi->txCI = ivars->m_spi->txPI; The part above calls TxAvailable() at every client transmission and increases txPI and txCI like an offset. That's for me unexpected. Here the next question: What happen, if the txPI and txCI reach the end of the TX memory descriptor? ;) Does the OS reset txPI and txCI to the beginning? -> it seems not to be. If I report txCI = txPI and txPI = 0, the call to TxAvailable is only executed once, but in my opinion/expectation it should start again at the beginning of the TX buffer. VSPSerialPort.cpp.txt GitHub Project Attached is the running/working code, but finally the DEX crash ;-) Best Regards
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Ah okay, yes I should file bugs. Well, a question about the step after receiving TX data. This - in case of you tip :) thx - is now working. I prepared already the response and the client receives the data. If the client try to transmit the next data [write(fd, p, s)] the driver does not get next data. TxAvailable isn't called any more. Finally the client runs in timeout. I'm sure that something is missing in my driver extension. DataAvailable() for client RX notification is called. Also TxFreeSpaceAvailable() is called to notify client for more data. But nothing come in again. What is the next step or what is additional required, that the underlaying code is ready for next data? I have RX data ready notification RxDataAvailable Notifies the system that data from the device is now available. I have TX ready for more data notification. TxFreeSpaceAvailable Notifies the system that the device is ready to accept more data. OS Log file VSPSerialPort.cpp.txt Best regards Bjoern
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Hello Kevin, Thanks alot :-) Your comments gave me insight and a bright light into my darkest days :-) That helped me a lot. Also the LOCAL thing is an important aspect. A small note in context: The IOMemoryDescriptor txqmd and txqmd can be cast in IOBufferMemoryDescriptor. Perhaps the documentation should be adapted accordingly. Since it must really be assumed that the mechanism with GetAddressRange() is not available on txqmd and rxqmd (IOMemoryDescriptor does not have this method). Perhaps a small note in the documentation of the "ConnectQueues" method that is called at the beginning (presumably by open(/dev/....) or similar) by the OS. This creates more clarity for the reading developer. GetAddressRange() must be called in TxAvailable(), because in this context the buffer address of txqmd is only known and filled there. Here too, perhaps a note in the documentation of TxAvailable(). With these two notes, a different picture of how it works would emerge. I looked at apple-oss-distributions XNU on GitHub, unfortunately the SerialDriverKit was not represented there. But, thank you for the help. I wish you all a good time. Best regards
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Hello Quinn "The Eskimo!" ;) thank you for you response. Okay, I'm inside the DEXT 'kernel' space. You suggestion is my doing. Let us restart to my first question context. My question is, how can I obtain the data in the method IOUserSerial::TxAvailable which has been called by the OS even if the user client app send data like: echo 'Welcome' > /dev/cu.serial-100000A5E ?? Second question ist, can I write you an email too? [ WHERE THE DATA 'Welcome' ? ] ------------------------ 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxDataAvailable called. 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxDataAvailable: Dump m_txqmd ------------- 2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::handleEventFlowControl: 1042 ==>0 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory called. md=0x600002c58d48 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: CreateMapping. 2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::hwSendBreak: 1083 <== 2025-02-05 08:33:00.866 Df kernel[0:e27] (IOUserSerial) IOUserSerial::hwSendBreak: 1083 locklevel = 1 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: GetAddress + GetLength. 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory: dump mapped buffer 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff=0x1025c8000 mapSize=16384 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[0]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[1]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[2]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[3]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[4]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[5]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[6]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[7]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[8]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[9]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[10]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[11]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[12]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[13]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[14]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: CopyMemory MAP> mapBuff[15]=0x00 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: TxFreeSpaceAvailable called. 2025-02-05 08:33:00.866 Df kernel[0:9d4f] () [VSPSerialPort]: RxDataAvailable called. [ SNIPPET : END ] --------------------------------------- This is the source of the CopyMemory method which is called from IOUserSerial::TxDataAvailable implementation in my driver extension: // -------------------------------------------------------------------- // CopyMemory_Impl(IOMemoryDescriptor* md) // ??? Called by TxDataAvailable() and here we get always 0x00 in MD // ??? mapped buffer of the IOMemoryDescriptors m_txqmd // IOReturn IMPL(VSPSerialPort, CopyMemory) { IOMemoryMap* map = nullptr; IOReturn ret; VSPLog(LOG_PREFIX, "CopyMemory called. md=0x%llx\n", (uint64_t)md); if (md == nullptr) { VSPLog(LOG_PREFIX, "copy_md_memory: Invalid memory descriptor (nullptr).\n"); return kIOReturnBadArgument; } VSPLog(LOG_PREFIX, "CopyMemory: CreateMapping.\n"); // Access memory of TX IOMemoryDescriptor uint64_t mapFlags = kIOMemoryMapGuardedDefault | kIOMemoryMapCacheModeDefault | kIOMemoryMapReadOnly; ret = md->CreateMapping(mapFlags, 0, 0, 0, 0, &map); if (ret != kIOReturnSuccess) { VSPLog(LOG_PREFIX, "copy_md_memory: Failed to get memory map. code=%d\n", ret); return ret; } VSPLog(LOG_PREFIX, "CopyMemory: GetAddress + GetLength.\n"); // get mapped data... const uint64_t mapSize = map->GetLength(); const uint8_t* mapBuff = (uint8_t*)(map->GetAddress()); VSPLog(LOG_PREFIX, "CopyMemory: dump mapped buffer\n"); VSPLog(LOG_PREFIX, "CopyMemory MAP> mapBuff=0x%llx mapSize=%llu\n", (uint64_t) mapBuff, mapSize); // !! Debug .... for (uint64_t i = 0; i < mapSize && i < 16; i++) { VSPLog(LOG_PREFIX, "CopyMemory MAP> mapBuff[%lld]=0x%02x %c\n", i, mapBuff[i], mapBuff[i]); } OSSafeReleaseNULL(map); return kIOReturnSuccess; } Best Regards.
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25
Reply to DriverKit IOUserSerial Driver
Hello DTS Engineer, Yes, you're right, it's confusing. I forgot to mention the purpose of the extension. Sorry. The purpose of the extension is to: Use the DEXT to create nodes in '/dev' for serial ports (/dev/(tty&cu)-serail-xxxx. A user application (UI) should control the creation and linking of the serial ports. A measurement and control simulation (UI) should use the serial ports to simulate measurement and control devices 'at device level', while a second application uses the serial ports as a 'normal' interface to use the simulated measurement and control devices with different protocols . Final goal: It should be used to check whether the entire communication path works in a software framework. In order to route the data from the user application (3) to the user application (4) and vice versa, I need to know exactly: How do I get the data from the IOMemoryDescriptor in the IOUserSerial instance? What is the mechanism for reading the incoming data reported by TxAvailable?
Topic: App & System Services SubTopic: Core OS Tags:
Feb ’25