I'm working on a procfs kernel extension for macOS and trying to implement a feature that emulates Linux’s /proc/cpuinfo similar to what FreeBSD does with its linprocfs. Since I'm trying to learn, and since not every bit of FreeBSD code can simply be copied over to XNU and be expected to work right out of the jar, I'm writing this feature from scratch, with FreeBSD and NetBSD's linux-based procfs features as a reference. Anyways...
Under Linux, $cat /proc/cpuinfo showes me something like this:
processor : 0
vendor_id : AuthenticAMD
cpu family : 25
model : 33
model name : AMD Ryzen 9 5950X 16-Core Processor
stepping : 0
microcode : 0xa201016
cpu MHz : 2195.107
cache size : 512 KB
physical id : 0
siblings : 32
core id : 0
cpu cores : 16
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 16
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb cat_l3 cdp_l3 hw_pstate ssbd mba ibrs ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local clzero irperf xsaveerptr wbnoinvd arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif umip pku ospke vaes vpclmulqdq rdpid overflow_recov succor smca
bugs : sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass
bogomips : 6787.02
TLB size : 2560 4K pages
clflush size : 64
cache_alignment : 64
address sizes : 48 bits physical, 48 bits virtual
power management: ts ttp tm hwpstate cpb eff_freq_ro [13] [14]
I’m using XNU’s i386_cpu_info structure in i386/cpuid.h to accomplish most of this, but so far I have not been able to get the ‘flags’ field to display correctly.
XNU’s i386/cpuid.h defines each feature flag as such:
#define CPUID_FEATURE_FPU _Bit(0) /* Floating point unit on-chip */
#define CPUID_FEATURE_VME _Bit(1) /* Virtual Mode Extension */
#define CPUID_FEATURE_DE _Bit(2) /* Debugging Extension */
#define CPUID_FEATURE_PSE _Bit(3) /* Page Size Extension */
#define CPUID_FEATURE_TSC _Bit(4) /* Time Stamp Counter */
...
And the i386_cpu_info structure has a field that should correspond to these definitions:
typedef struct i386_cpu_info {
...
uint64_t cpuid_features;
...
}
In my ‘main’ do_cpuinfo() function, for example, I use one of these to check if the FPU feature is supported, as such:
/*
* Check if the FPU feature is present.
*/
char *fpu, *fpu_exception;
/*
* The cpuid_info() function sets up the i386_cpu_info structure and returns a pointer to the structure.
*/
if (cpuid_info()->cpuid_features & CPUID_FEATURE_FPU) {
fpu = "yes";
fpu_exception = "yes";
} else {
fpu = "no";
fpu_exception = "no";
}
And it works just as expected. However once I start adding arrays into the mix, things start getting erratic.
I set up a char array containing the string for each flag as such:
const char *feature_flags[] = {
/* 1 */ "fpu",
/* 2 */ "vme",
/* 3 */ "de",
/* 4 */ "pse",
/* 5 */ "tsc”,
…
}
Then set up a correspnding uint64_t array containing each flag as defined in i386/cpuid.h:
uint64_t feature_list[] = {
/* 1 */ CPUID_FEATURE_FPU,
/* 2 */ CPUID_FEATURE_VME,
/* 3 */ CPUID_FEATURE_DE,
/* 4 */ CPUID_FEATURE_PSE,
/* 5 */ CPUID_FEATURE_TSC,
…
}
Then I wrote a function that is supposed to iterate over these arrays to check if a feature is present with the same method I used for the FPU detection, but instead of doing something like “cpuid_info()->cpuid_features & CPUID_FEATURE_FPU” for each flag, I want cpuid_info()->cpuid_features to get each flag from the feature_list array and, if supported, copy it to the flags variable, move the resulting string to the static ret variable, so we can then free the allocated memory and return ret.
char *
get_cpu_flags(void)
{
int i = 0;
char *flags = NULL;
static char *ret;
/*
* Allocate memory for our flag strings.
*/
flags = _MALLOC(sizeof(feature_flags), M_TEMP, M_WAITOK);
do {
/*
* If the CPU supports a feature in the feature_list[],
* move its corresponding flag from the feature_flags[]
* into the buffer.
*/
if ((cpuid_info()->cpuid_features & feature_list[i]) == feature_list[i]) {
strlcat(flags, feature_flags[i], strlen(feature_flags[i]));
// | | |
// | | * The length of the string I want to amend to the ‘flags’ variable
// | * The flag string in the array I want to amend to the ‘flags’ variable.
// * The variable I want the flag string to be amended to.
}
/*
* Move the flag strings to a static variable before freeing the allocated memory
* so we can free it before returning the resulting string.
*/
ret = flags;
/*
* Add 1 to the counter for each iteration.
*/
i++;
/*
* If the counter exceeds the number of items in the array,
* break the loop.
*/
if (i > nitems(feature_flags)) {
/*
* Free the allocated memory before breaking the loop.
*/
_FREE(&feature_flags, M_TEMP);
break;
} else {
continue;
}
} while (i < nitems(feature_flags));
return ret;
}
This function then gets called by the main do_cpuinfo() function that prints the info into userspace. To save space I’m providing a minimal example here dealing only with the ‘flags’ field:
int
procfs_docpuinfo(__unused procfsnode_t *pnp, uio_t uio, __unused vfs_context_t ctx)
{
vm_offset_t pageno, uva, kva;
int len = 0, xlen = 0;
off_t page_offset = 0;
size_t buffer_size = 0;
char *buffer;
uint32_t max_cpus = *_processor_count;
uint32_t cnt_cpus = 0;
/*
* Set up the variables required to move our data into userspace.
*/
kva = VM_MIN_KERNEL_ADDRESS; // kernel virtual address
uva = uio_offset(uio); // user virtual address
pageno = trunc_page(uva); // page number
page_offset = uva - pageno; // page offset
buffer_size = sizeof(i386_cpu_info_t) + (LBFSZ * 2); // buffer size
buffer = _MALLOC(buffer_size, M_TEMP, M_WAITOK); // buffer
char *flags = get_cpu_flags();
do {
if (cnt_cpus <= max_cpus) {
/*
* len should snprintf our flags via uiomove.
*/
len += snprintf(buffer, buffer_size, "flags\t\t\t: %s\n", flags);
/*
* Subtract the uva offset from len.
*/
xlen = len - uva;
xlen = imin(xlen, uio_resid(uio));
/*
* Copy our data into userspace.
*/
uiomove(buffer, xlen, uio);
/*
* Set len back to 0 before entering into the next loop.
*/
if (len != 0) {
len = 0;
}
/*
* Update the CPU counter.
*/
cnt_cpus++;
/*
* Continue unless the counter exceeds the
* available processor count.
*/
continue;
} else if (cnt_cpus > max_cpus) {
/*
* If the counter exceeds the processor count,
* free the associated memory and break the loop.
*/
_FREE(&buffer, M_TEMP);
break;
}
} while (cnt_cpus < max_cpus);
return 0;
}
However, this is the result I get when executing cat on cpuinfo (with the full function intact obviously and not the minimal example provided above):
processor : 0
vendor_id : AuthenticAMD
cpu family : 25
model : 1
model name : AMD Ryzen 9 5950X 16-Core Processor
microcode : 186
stepping : 0
cpu MHz : 3393.62
cache size : 512 KB
physical id : 0
siblings : 32
core id : 0
cpu cores : 16
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 16
wp : yes
flags : fpappclm syscalpre
bugs :
bogomips : 6786.62
TLB size : 2560 4K pages
clflush_size : 64
cache_alignment : 64
address sizes : 48 bits physical, 48 bits virtual
power management:
As you can see, the flags field seems to combine fragments of the flag strings together in some very minimalistic fashion rather than the full flags string.
Note: The space you see is the result of there being four categories of features (cpuid_features, cpuid_extfeatures, cpuid_leaf7_features and cpuid_leaf7_extfeatures) in XNU so I made a function like get_cpu_flags() for each category. As such in my main code the snprintf function is expecting four sets of strings with a space between them ("flags\t\t\t: %s %s %s %s\n", cpuflags, cpuextflags, leaf7flags, leaf7extflags), yet it only prints out two of them and neither is printing out correctly once in userspace.
I assume the issue arises within the get_cpu_flags() function and its sister functions, but I’m not sure what the matter actually is. If I move ‘ret = flags’ out of the loop and put _FREE(&feature_flags, M_TEMP) right after it, I get a kernel panic and this is the stack trace:
panic(cpu 18 caller 0xffffff8019a9b8c6): "address 0xffffff7fb66cd110 inside vm entry 0xffffff802abc21e0 [0xffffff7f9a410000:0xffffff8000000000), map 0xffffff802abb10f8"com.apple./System/Volumes/Data/SWE/macOS/BuildRoots/36806d33d2/
Library/Caches/com.apple.xbs/Sources/xnu/xnu-7195.141.19/osfmk/kern/kalloc.c:651
Backtrace (CPU 18), Frame : Return Address
0xffffffa0ef283690 : 0xffffff8019a8c26d mach_kernel : _handle_debugger_trap + 0x3fd
0xffffffa0ef2836e0 : 0xffffff8019bd3993 mach_kernel : _kdp_i386_trap + 0x143
0xffffffa0ef283720 : 0xffffff8019bc3f8a mach_kernel : _kernel_trap + 0x55a
0xffffffa0ef283770 : 0xffffff8019a30a2f mach_kernel : _return_from_trap + 0xff
0xffffffa0ef283790 : 0xffffff8019a8ba8d mach_kernel : _DebuggerTrapWithState + 0xad
0xffffffa0ef2838b0 : 0xffffff8019a8bd83 mach_kernel : _panic_trap_to_debugger + 0x273
0xffffffa0ef283920 : 0xffffff801a29c8da mach_kernel : _panic + 0x54
0xffffffa0ef283990 : 0xffffff8019a9b8c6 mach_kernel : _ipc_thread_port_unpin + 0x116
0xffffffa0ef2839c0 : 0xffffff8019a9bf33 mach_kernel : _kfree + 0x263
0xffffffa0ef283a10 : 0xffffff8019a9be3f mach_kernel : _kfree + 0x16f
0xffffffa0ef283a70 : 0xffffff7fb66c5713 com.stupid.filesystems.procfs : _get_cpu_flags + 0xe3
0xffffffa0ef283aa0 : 0xffffff7fb66c4f4f com.stupid.filesystems.procfs : _procfs_docpuinfo + 0x24f
0xffffffa0ef283d50 : 0xffffff7fb66ca4c0 com.stupid.filesystems.procfs : _procfs_vnop_read + 0xb0
0xffffffa0ef283d90 : 0xffffff8019d4449c mach_kernel : _utf8_normalizeOptCaseFoldAndMatchSubstring + 0x72c
0xffffffa0ef283e30 : 0xffffff801a04c3b8 mach_kernel : _read_nocancel + 0x328
0xffffffa0ef283ee0 : 0xffffff801a04c145 mach_kernel : _read_nocancel + 0xb5
0xffffffa0ef283f40 : 0xffffff801a13ed0e mach_kernel : _unix_syscall64 + 0x2ce
0xffffffa0ef283fa0 : 0xffffff8019a311f6 mach_kernel : _hndl_unix_scall64 + 0x16
Kernel Extensions in backtrace:
com.stupid.filesystems.procfs(1.0)[89308435-3658-3ED4-990A-F8AF63358857]com.apple.0xffffff7fb66c3000-com.apple.driver.0xffffff7fb66ccfff
For the record, I’m just an amateur trying to learn C and kernel programming on my own because it’s something that has fascinated me for a long time. I’m fairly new to working with character arrays and memory allocation so any advice would be deeply appreciated. I asked a similar question before, but got pointed out to me that I should be more specific and provide more extensive examples. Then I was also exclusively struggling with kernel panics but now that doesn’t seem to be the main issue so I’ve deleted my old one and filed this new question based on the tips I received before and my progress since then. I hope I managed to present my question better this time but, if not, please let me know and I’ll try to improve. 🙏
My full source code for these functions can be found here: https://github.com/somestupidgirl/procfs_kext/blob/main/kext/procfs_cpu.c
Edit 1:
A sizeof for each flag array gives me the following results:
* sizeof(feature_flags) = 480
* sizeof(feature_ext_flags) = 64
* sizeof(leaf7_feature_flags) = 368
* sizeof(leaf7_feature_ext_flags) = 96
I've been trying to do this without malloc, as has been suggested, but it always results in a kernel panic with the stack trace not telling me anything useful, really. So either I'm misunderstanding or the kernel simply won't allow it.
One thing I would also like to iterate is that I don't want 'get_cpu_flags' to do the 'snprintf' bit. What I want that function to do is gather the list of supported cpu feature flags, store them in a string, and return that string that should now contain a list of the supported cpu flags for the current cpu. Then I define a char* variable inside 'procfs_docpuinfo' with 'get_cpu_flags' that, once called by the 'snprintf' should print out the list of supported flags.
For a better reference, here is my full 'procfs_docpuinfo' function as it looks currently:
int
procfs_docpuinfo(__unused procfsnode_t *pnp, uio_t uio, __unused vfs_context_t ctx)
{
vm_offset_t pageno, uva, kva;
int len = 0, xlen = 0;
off_t page_offset = 0;
size_t buffer_size = 0;
char *buffer;
/*
* Overall processor count for the current CPU.
*
* Not to be conflated with cpu_cores (number of cores)
* as these are not the same.
*/
uint32_t max_cpus = *_processor_count;
/*
* Initialize the processor counter.
* This should always begin at 0 and
* add 1 for each loop according to the
* number of processors present.
*/
uint32_t cnt_cpus = 0;
/*
* The core id should always start at 0.
*/
int core_id = 0;
/*
* Initialize the TSC frequency variables.
*/
uint64_t freq = *_tscFreq;
int fqmhz = 0, fqkhz = 0;
/*
* Set the TSC frequency variables
*/
if (freq != 0) {
fqmhz = (freq + 4999) / 1000000;
fqkhz = ((freq + 4999) / 10000) % 100;
}
/*
* The apicid variable begins at 0 and get increased
* by 2 for each loop until the number becomes greater
* than max_cpus, in which case the loop resets the
* variable to a value of 1 and then contines increasing
* that number by 2 for each loop.
*/
int apicid = 0, initial_apicid = 0;
/*
* Here we can utilize the i386_cpu_info structure in i386/cpuid.h
* to get the information we need. The cpuid_info() function sets up
* the i386_cpu_info structure and returns a pointer to the structure.
*/
char *vendor_id = _cpuid_info()->cpuid_vendor;
uint8_t cpu_family = _cpuid_info()->cpuid_family;
uint8_t model = _cpuid_info()->cpuid_model; // FIXME
char *model_name = _cpuid_info()->cpuid_brand_string;
uint32_t microcode = _cpuid_info()->cpuid_microcode_version; // FIXME
uint32_t cache_size = _cpuid_info()->cpuid_cache_size;
uint8_t stepping = _cpuid_info()->cpuid_stepping;
uint32_t cpu_cores = _cpuid_info()->core_count;
uint32_t cpuid_level = cpu_cores;
uint32_t tlb_size = _cpuid_info()->cache_linesize * 40;
uint32_t clflush_size = _cpuid_info()->cache_linesize;
uint32_t cache_alignment = clflush_size;
uint32_t addr_bits_phys = _cpuid_info()->cpuid_address_bits_physical;
uint32_t addr_bits_virt = _cpuid_info()->cpuid_address_bits_virtual;
/*
* Check if the FPU feature is present.
*/
char *fpu, *fpu_exception;
if (_cpuid_info()->cpuid_features & CPUID_FEATURE_FPU) {
fpu = "yes";
fpu_exception = "yes";
} else {
fpu = "no";
fpu_exception = "no";
}
/*
* Get the CPU flags.
*/
char *cpuflags, *cpuextflags, *leaf7flags, *leaf7extflags;
cpuflags = get_cpu_flags();
cpuextflags = get_cpu_ext_flags();
leaf7flags = get_leaf7_flags();
leaf7extflags = get_leaf7_ext_flags();
/*
* Check for CPU write protection.
*/
char *wp;
if (get_cr0() & CR0_WP) {
wp = "yes";
} else {
wp = "no";
}
/* TODO */
//char *bugs = get_cpu_bugs();
char *bugs = "";
//char *pm = get_cpu_pm();
char *pm = "";
/*
* Set up the variables required to move our data into userspace.
*/
kva = VM_MIN_KERNEL_ADDRESS; // kernel virtual address
uva = uio_offset(uio); // user virtual address
pageno = trunc_page(uva); // page number
page_offset = uva - pageno; // page offset
buffer_size = (LBFSZ * 4); // buffer size
buffer = _MALLOC(buffer_size, M_TEMP, M_WAITOK); // buffer
do {
if (cnt_cpus <= max_cpus) {
/*
* The data which to copy over to userspace.
*/
len += snprintf(buffer, buffer_size,
"processor\t\t: %u\n"
"vendor_id\t\t: %s\n"
"cpu family\t\t: %u\n"
"model\t\t\t: %u\n"
"model name\t\t: %s\n"
"microcode\t\t: %u\n"
"stepping\t\t: %u\n"
"cpu MHz\t\t\t: %d.%02d\n"
"cache size\t\t: %d KB\n"
"physical id\t\t: %u\n"
"siblings\t\t: %u\n"
"core id\t\t\t: %d\n"
"cpu cores\t\t: %u\n"
"apicid\t\t\t: %u\n"
"initial apicid\t\t: %u\n"
"fpu\t\t\t: %s\n"
"fpu_exception\t\t: %s\n"
"cpuid level\t\t: %u\n"
"wp\t\t\t: %s\n"
"flags\t\t\t: %s %s %s %s\n"
"bugs\t\t\t: %s\n"
"bogomips\t\t: %d.%02d\n"
"TLB size\t\t: %u 4K pages\n"
"clflush_size\t\t: %u\n"
"cache_alignment\t\t: %d\n"
"address sizes\t\t: %d bits physical, %d bits virtual\n"
"power management\t: %s\n\n",
cnt_cpus, // processor
vendor_id, // vendor_id
cpu_family, // cpu family
model, // model
model_name, // model name
microcode, // microcode
stepping, // stepping
fqmhz, fqkhz, // cpu MHz
cache_size, // cache size
0, // physical id
max_cpus, // siblings
core_id, // core id
cpu_cores, // cpu cores
apicid, // apicid
initial_apicid, // initial apicid
fpu, // fpu
fpu_exception, // fpu exception
cpuid_level, // cpuid level
wp, // wp
cpuflags, // flags
cpuextflags, // flags
leaf7flags, // flags
leaf7extflags, // flags
bugs, // bugs
fqmhz * 2, fqkhz, // bogomips
tlb_size, // TLB size
clflush_size, // clflush_size
cache_alignment, // cache_alignment
addr_bits_phys, // address size physical
addr_bits_virt, // address size virtual
pm // power management
);
/*
* Subtract the uva offset from len.
*/
xlen = len - uva;
xlen = imin(xlen, uio_resid(uio));
/*
* Copy our data into userspace.
*/
uiomove(buffer, xlen, uio);
/*
* Set len back to 0 before entering into the next loop.
*/
if (len != 0) {
len = 0;
}
/*
* Reset the max_cpus variable at the end of each loop.
* Otherwise it tends to behave erratically.
*/
if (max_cpus != *_processor_count) {
max_cpus = *_processor_count;
}
/*
* Increase by 2 for each loop.
*/
apicid += 2;
if (apicid >= max_cpus) {
/* If the number exceeds max_cpus, reset to 1. */
apicid = 1;
}
/*
* The initial apicid is the same as apicid.
*/
initial_apicid = apicid;
/*
* Update the CPU counter.
*/
cnt_cpus++;
/*
* Update the core_id.
*/
core_id++;
/*
* The core_id should never exceed the number of cores.
* Start over if it does.
*/
if (core_id > cpu_cores - 1) {
core_id = 0;
}
/*
* Continue unless the counter exceeds the
* available processor count.
*/
continue;
} else if (cnt_cpus > max_cpus) {
/*
* If the counter exceeds the processor count,
* free the associated memory and break the loop.
*/
_FREE(&buffer, M_TEMP);
break;
}
} while (cnt_cpus < max_cpus);
return 0;
}
And here is my slightly updated 'get_cpu_flags' function:
STATIC char *
get_cpu_flags(void)
{
int i = 0;
int size = 0;
char *flags;
static char *ret;
size = sizeof(feature_flags);
flags = _MALLOC(size, M_TEMP, M_WAITOK);
do {
/*
* If the CPU supports a feature in the feature_list[]...
*/
if (_cpuid_info()->cpuid_features & feature_list[i]) {
/*
* ...amend its flag to 'flags'.
*/
strlcat(flags, feature_flags[i], sizeof(flags));
}
ret = flags;
/*
* Add 1 to the counter for each iteration.
*/
i++;
/*
* If the counter exceeds the number of items in the array,
* break the loop.
*/
if (i > nitems(feature_flags)) {
_FREE(&flags, M_TEMP);
break;
} else {
continue;
}
} while (i < nitems(feature_flags));
return ret;
}
The result from executing cat on cpuinfo is still pretty much the same as before. I'm still digesting all the replies I've received and hopefully I can come up with a solution that works sooner than later thanks to y'all. <3
Edit 2:
I finally managed to figure it out! The 'get_cpu_flags' function now looks like this:
STATIC char *
get_cpu_flags(void)
{
int i = 0;
int size = (sizeof(feature_flags) * 2);
char *flags[size];
do {
/*
* If the CPU supports a feature in the feature_list[]...
*/
if (_cpuid_info()->cpuid_features & feature_list[i]) {
/*
* ...amend its flag to 'flags'.
*/
strlcat(flags, feature_flags[i], sizeof(flags));
}
/*
* Add 1 to the counter for each iteration.
*/
i++;
} while (i < nitems(feature_flags));
return flags;
}
And the output from cat on the cpuinfo file now looks like this:
processor : 0
vendor_id : AuthenticAMD
cpu family : 25
model : 1
model name : AMD Ryzen 9 5950X 16-Core Processor
microcode : 186
stepping : 0
cpu MHz : 3393.63
cache size : 512 KB
physical id : 0
siblings : 32
core id : 0
cpu cores : 16
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 16
wp : yes
flags : fpuvmedepsetscmsrpaemcecx8apicsepmtrrpgemcacmovpatpse36clfshmmxfxsrssesse2httsse3pclmulqdqmonssse3fmacx16sse4.1sse4.2movbepopcntaesxsaveosxsaveavx1.0f16crdrand
bugs :
bogomips : 6786.63
TLB size : 2560 4K pages
clflush_size : 64
cache_alignment : 64
address sizes : 48 bits physical, 48 bits virtual
power management :
Thanks to chqrlie for providing the right answer, and everyone else who chimed in! I truly appreciate all of your inputs! Now the only thing left is to add a space between each flag, but now that I have a better understanding of what I'm doing it shouldn't be too big of a challenge. Thanks again everyone and peace out. <3
There is no need to allocate memory for this task: pass a pointer to a local array along with its size and use strlcat
properly:
strlcat(flags, feature_flags[i], size);
Calling strlcat()
with a size argument that is the length of the copied string is a mistake, the size argument should be the size of the destination array for strlcat
to truncate the destination.
For example:
char *get_cpu_flags(char *dest, size_t size) {
/* avoid patching dest if size is 0 */
if (!size)
return;
/* initialize dest as an empty string */
*dest = '\0';
/* repeat these tests: */
if (cpuid_info()->cpuid_features & [...]) {
/* append flag name, truncating if necessary */
strlcat(dest, " flagname", size);
}
}
[...]
/* return argument so the composed string can be passed
to snprintf directly */
return dest;
}