Skip to content

Assume presence of PT_CHERI_PCC - #2695

Open
bsdjhb wants to merge 12 commits into
CTSRD-CHERI:devfrom
bsdjhb:assume_PT_CHERI_PCC
Open

Assume presence of PT_CHERI_PCC#2695
bsdjhb wants to merge 12 commits into
CTSRD-CHERI:devfrom
bsdjhb:assume_PT_CHERI_PCC

Conversation

@bsdjhb

@bsdjhb bsdjhb commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator
  • csu: Replace references to the captable with the GOT instead
  • csu: Remove unused rodata_cap_out argument from crt_init_globals
  • csu: Assume PT_CHERI_PCC and bounded code pointers for pure-cap
  • libc/csu: Assume bounded code pointers for pure-cap
  • rtld: Assume PT_CHERI_PCC and bounded code pointers for pure-cap
  • sys: Assume PT_CHERI_PCC and bounded code pointers for pure-cap

@bsdjhb
bsdjhb requested a review from jrtc27 July 27, 2026 20:29
@bsdjhb

bsdjhb commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

This can land after the TLS ABI change from the next branch.

Comment thread lib/csu/common-cheri/crt_init_globals.c Outdated
crt_init_rela(code_cap, data_cap);
#endif
#else
/* Attempt to bound the data capability to only the writable segment */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really wish we could decide we didn't care about hybrid as much and remove all of this. I guess lld doesn't know how to set the bounds in caprelocs for code pointers that appear in hybrid binaries? Alternatively we could just use more naive bounds by coming up with an equivalent data_cap as is used for the pure-cap case by just bounding to the entire file mapping and not worrying about permissions as much. Probably that is fine to do actually since caprelocs also have proper bounds for data symbols? (Though I guess there is always the size == 0 case?) Still, it's doubtful we gain much by narrowing the bounds of data_cap/rodata_cap narrower than just the bounds of the entire mapping for hybrid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLD can set whatever bounds it likes, and knows hybrid from purecap, but the problem is it has no way to know what the start and end of the address space is going to be, as that depends on the execution environment.

Comment thread lib/csu/common-cheri/crt_init_globals.c Outdated
#ifdef __CHERI_PURE_CAPABILITY__
handle_relocs:
#ifdef CHERI_INIT_RELA
crt_init_rela(code_cap, cheri_perms_clear(cheri_ddc_get(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A wider data_cap would let me move this back out of the purecap vs hybrid #ifdef since it could just use data_cap directly for hybrid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to? It's not like hybrid has any kind of coherent principled design to it...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a commit at the end of the branch that takes this approach.

Comment thread sys/kern/link_elf.c Outdated
caddr_t pcc_cap;
Elf_Addr addr;

/* XXX: RISC-V kernels may not have phdrs in memory. */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need this for the non-EFI boot case for RISC-V kernels where the phdrs aren't available as module data. :(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point PT_CHERI_PCC is a solely dynamic linking thing for bounding the result of symbol resolution, so really belongs in .dynamic, which is mapped...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, with the MODINFO_PHDR thing, we do now have phdrs, just not when direct-booting in QEMU (which I'm still doing). Possibly that is also true on FPGA as well though?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, FPGAs also direct boot like QEMU, and as long as that's a supported boot flow I don't think it's ok to say you can't load modules there...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modules always have phdrs though, it is only the kernel image itself that does not due to the "first byte must be an instruction" requirement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but you're not going to get very far if your loaded module can't reference any function symbols in the kernel due to not having PCC bounds information for them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hence the XXX comment here that permits this fallback still (which I've removed in userspace).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my point is just we need a proper solution eventually, and it being in .dynamic would be one approach

Comment thread lib/csu/aarch64c/crt1_c.c Outdated
Comment thread lib/csu/aarch64c/crt1_c.c Outdated
Comment thread lib/libc/csu/aarch64/reloc.c Outdated
ptr = (uintptr_t)cheri_perms_and(code_cap,
function_pointer_permissions_mask);
ptr = cheri_address_set(ptr, r->object);
if (use_code_bounds && r->size != 0)

@jrtc27 jrtc27 Jul 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for now since we'd also need to do the Clang header, but this pattern is probably one to kill off (the "throw your hands up and don't set bounds because it's too hard to do properly without a linker, and/or symbols were missing sizes" approach, as I understand).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I still see quite a few linker warnings for various kernel modules for zero-sized symbols. Perhaps I should make lld warnings fatal for the kernel and see how painful that is to resolve.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but those are cases where the size being zero doesn't matter. Note that our Morello fragment handling doesn't special-case zero, it's only actual grotty caprelocs.

Comment thread libexec/rtld-elf/aarch64/reloc.c Outdated
Comment thread libexec/rtld-elf/cheri/cheri_reloc.c Outdated
Comment thread libexec/rtld-elf/rtld.c Outdated
if (obj->npcc_caps == 0)
return (true);
if (obj->npcc_caps == 0) {
_rtld_error("missing required PT_CHERI_PCC");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not an error, that just means there's no code:

jrtc27@morello:~$ echo 'int x = 42;' | cc -x c - -o - -shared -nostdlib | llvm-readelf -Wl - | grep PCC
jrtc27@morello:~$ echo 'int x = 42; void f(void) {}' | cc -x c - -o - -shared -nostdlib | llvm-readelf -Wl - | grep PCC
  CHERI_PCC      0x0003a0 0x00000000000003a0 0x00000000000003a0 0x000060 0x010060 R E 0x20

Not something you'd normally get, thanks to run_cxa_finalize in crtbeginS, but if you don't need any crt bits for some data-only object it can be valid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I can gate this on if there is any PT_LOAD with execute permission?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need it though? Won't we just fall back on returning NULL for executable symbols if you somehow manage to build a module with an old toolchain?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For userland, yes. I think it is a bit friendlier to users to get a better error message. I've made some squash commits in the updated branch (will push after some local testing) for both the kernel and userspace to only require it if there is an executable segment. The changes are not very invasive as we are already walking phdrs to count PT_CHERI_PCC and can just check the PT_LOADs in the same loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I wanted to be particularly awkward, I think I'd argue that an object that exports no symbols shouldn't need a PT_CHERI_PCC phdr, even if it has code (such as solely using constructor-y things)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah, that too. I know we don't in the kernel (in part because then you'd need to pass a set of code capabilities for a binary with more than one segment), but forgot we do in rtld for dynamic binaries.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's in map_object(). I wonder why we even bother setting that in map_object()... we don't invoke the entry point for DSOs. Oh, I guess for direct exec mode we do use map_object() for the binary, and then we can end up using that. Blech. The kernel does not use narrow bounds here, perhaps map_object() should be doing the same and giving the equivalent bounds to prog_cap for the entry point?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, are we not setting the bounds on the executable entry point for the normal non-direct exec case? That's a bug...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(In that csu will end up running with overly-wide bounds, only narrowing them thanks to passing a function pointer to main to __libc_start1)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in the current form of this series, we are now consistent in always having wide bounds on entry to CSU. Static binaries require this so that they can process relocations. The only way we could set bounds though would be to depend on PT_CHERI_PCC, but I wanted to see if I could make PT_CHERI_PCC only required when resolving dynamic symbols and this is the one case that isn't associated with a relative relocation that is able to encode the bounds.

One might argue that for CHERI we find a way to encode the entry point as a relative relocation so that we can set the bounds there, but that gets a bit squishy. One approach might be to adopt new ELF "classes" (ELF64C, etc.) for purecap where the entry field in the ELF header is capability-sized so it can hold a relative relocation (either a fragment style or cbuildcap-style). That has a lot of cost. Another approach might be to add some sort of ancillary data to store the bounds, but then you might as well just mandate at least one PT_CHERI_PCC covering the entry point? It can also only ever apply to dynamic binaries and not static binaries anyway. It seems simplest to say that the CSU must always explicitly narrow bounds at some point (at the latest main() must use narrow bounds, but narrow bounds are encouraged earlier than that). The CSU code already needs to do this for the static case anyway.

Comment thread sys/kern/link_elf.c Outdated
}

if (ef->npcc_caps == 0) {
printf("%s: Missing PT_CHERI_PCC segment\n", ef->lf.filename);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I'm not aware of any modules actually doing it, even firmware modules where the only code in them is a function pointed to by their struct moduledata's evhand, I think you can have a module with no code in it (and if that means you leave evhand NULL rather than leave a symbolic relocation for it then module_register will use modevent_nop for you)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think firmware modules just point to a function in the kernel itself, not their own module handler, but presumably I could similarly gate this on if there are any executable PT_LOADs?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sys/tools/fw_stub.awk generates a handler that calls firmware_(un)register

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bleh. We should probably just be defining a data structure passed to a centralized module handler similar to driver_module_handler in subr_bus.c, but that's an upstream problem.

Comment thread sys/arm64/arm64/elf_machdep.c Outdated
Comment thread libexec/rtld-elf/aarch64/reloc.c Outdated
@bsdjhb
bsdjhb force-pushed the assume_PT_CHERI_PCC branch from 8a1bd48 to 551c227 Compare July 28, 2026 17:49
@bsdjhb

bsdjhb commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

So another thing I could do in the kernel perhaps, is explicitly in the preload case where we can't find phdrs for a preloaded kernel or module, use a special variant that creates a single pcc_cap that is equivalent to the current fallback, and then remove the fallback from the loop. That is, a kernel where we can't find the phdrs would end up with npcc_caps == 1. That I think would avoid requiring PT_CHERI_PCC and just giving the proper runtime failure similar to userspace.

Comment thread lib/csu/aarch64c/crt1_c.c Outdated
Comment thread lib/csu/aarch64c/crt1_c.c Outdated
bsdjhb added 4 commits July 29, 2026 12:35
- Use GOT instead of captable.

- Remove various CHERI-MIPS-specific details such as no-jump-tables
  and claiming that all function calls go via the GOT.  Calls to
  non-preemptible functions do not use the GOT.
The fragment-based ABI was added in Morello LLVM in commit 94e1dbac
prior to CheriBSD 22.05.
The new ABI was introduced in Morello LLVM commit b2549f5d49be prior
to PT_CHERI_PCC.
@jrtc27

jrtc27 commented Jul 30, 2026

Copy link
Copy Markdown
Member

So another thing I could do in the kernel perhaps, is explicitly in the preload case where we can't find phdrs for a preloaded kernel or module, use a special variant that creates a single pcc_cap that is equivalent to the current fallback, and then remove the fallback from the loop. That is, a kernel where we can't find the phdrs would end up with npcc_caps == 1. That I think would avoid requiring PT_CHERI_PCC and just giving the proper runtime failure similar to userspace.

That's cute, yeah, do it once at boot and then you never need to handle the special case again

bsdjhb added 8 commits July 31, 2026 14:59
This is now replaced with PT_CHERI_PCC bounds and the bounds on relative
relocations.
init_cap_from_fragment already clears FUNC_PTR_REMOVE_PERMS when it
uses the pcc_cap as the base capability.
- Don't bother walking ELF program headers at all for pure-cap always
  using the PT_CHERI_PCC case.

- Always bound code capabilities built from Morello fragments for pure-cap.
…ions

PT_CHERI_PCC is now only used to set bounds on dynamically-exported
symbols.
For RISC-V kernels without program headers, create a fallback PCC cap
that spans the entire kernel.
The main change is that data_cap and rodata_cap now have bounds of the
entire executable image.  The change primarily aims to reduce code
duplication with pure-capability.
@bsdjhb
bsdjhb force-pushed the assume_PT_CHERI_PCC branch from 551c227 to d17026b Compare July 31, 2026 20:25
#endif
#else
/* Attempt to bound the data capability to only the writable segment */
/* Bound the data capability to the executable image */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be even simpler (and almost identical code for both cases), if we just used DDC as the data_cap and made no promises about bounds for hybrid. I'm tempted to just do that on the assumption that "if you are doing hybrid, you need to do all the bounds on your own".

}
}

for (; phnum > 0; phdr++, phnum--) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check was wrong when I added it. :( In the normal case this is checking the phdrs of the binary which doesn't indicate anything about if rtld itself has "new" relative relocations. This was only correct in the direct-exec case. Oh well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants