Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImmutAfterInit: misuse runtime detection #2

Merged
merged 4 commits into from
Jun 19, 2023

Conversation

00xc
Copy link
Member

@00xc 00xc commented Mar 16, 2023

  • Use SyncUnsafeCell instead of UnsafeCell + unsafe impl Sync.
  • Pass Copy types by value in ImmutAfterInitCell methods.
  • Remove a double initialization before implementing runtime detection of this condition.
  • Back ImmutAfterInitCell by an Option in order to be able to detect uninitialized cells.
  • Return Result from public API calls to handle double initialization and uninitialized access.

Copy link
Collaborator

@nicstange nicstange left a comment

Choose a reason for hiding this comment

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

FWIW, I have a very similar, conflicting commit as your a7da78a in my pending ELF PR: e2acf29

The reasoning for having the initializer functions to take references rather than values had been to not rely on compiler optimizations too much when initializing large objects like the SnpCpuidTable blob. That is, my concern had been that temporaries on the stack could perhaps get created.

@00xc
Copy link
Member Author

00xc commented Mar 17, 2023

FWIW, I have a very similar, conflicting commit as your a7da78a in my pending ELF PR: e2acf29

The reasoning for having the initializer functions to take references rather than values had been to not rely on compiler optimizations too much when initializing large objects like the SnpCpuidTable blob. That is, my concern had been that temporaries on the stack could perhaps get created.

I see. I think this is a common pattern for Rust and should not be an issue, especially on release builds.

@00xc
Copy link
Member Author

00xc commented Mar 17, 2023

TODO for me:

  • Rebase based on nicstange's changes, when/if they are merged (or at least based on e2acf29)
  • Refactor to use an initialized flag instead of an Option. The main issue is that there are no guarantees on the layout of the Option, so, in debug builds, we cannot copy into it from a pointer without first copying to the stack, and then from the stack into the Option (via something like Option::replace()), which will be in static storage for regular users of the API.

@00xc 00xc force-pushed the utils/immut branch 2 times, most recently from 7c3cf86 to c60a59d Compare May 8, 2023 14:02
@00xc
Copy link
Member Author

00xc commented May 8, 2023

I've simplified the PR:

  • No longer uses SyncUnsafeCell (it introduces a dependency on a nightly feature).
  • No longer takes advantage of the Copy trait (it is no longer used in the main branch, plus it proved to have some problems). Initialization is done by copying from a pointer as before.
  • No longer uses an Option to track state, but an AtomicBool, since it implements Sync and has a simple API.

Copy link
Collaborator

@nicstange nicstange left a comment

Choose a reason for hiding this comment

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

I like this PR, but have three suggestions though. I don't want to delay the merge any further just because of nitpicking, so it might make sense to merge it as-is and do a follow-up patch -- up to Joerg to decide.

1.) You removed the #[repr(transparent)] from ImmutAfterInitCell. Have you tried whether #[cfg_attr(not(debug_assertions), repr(transparent))] would work? This way the ImmutAfterInitCell would remain to be guaranteed a zero-cost wrapper for non-debug builds.

2.) For not(debug_assertions), I would turn the new ImmutAfterInitError into a core::convert::Infallible. That would potentially enable the compiler to optimize the error checks at call sites away for non-debug builds. Not sure compilers are clever enough today already, but when Infallible is made the never-type (!) one day, they probably would.

3.) In my personal opinion, removing the unsafe marker from the fn init() can be debated about: from my POV it serves as an indicator to users that they need to think carefully when invoking it, i.e. that there is no possible code path where an ImmutAfterInitCell can get dereferenced while still in uninitialized state. Your patch set here certainly helps a lot in establishing confidence into that, but it still doesn't provide a hard guarantee, which can be obtained only by reviewing the code. Having the unsafe{} markers at the potentially problematic call sites, would at least help to draw reviewers' attention at the right spots. In either case, the move to drop unsafe from fn init() might perhaps have deserved a separate patch on its own. Just saying ;)

00xc added 3 commits June 13, 2023 10:16
ImmutAfterInitCell states that the init() method should not be called
on an already initialized instance, but KERNEL_MAPPING is initialized
via ImmutAfterInitCell::new() first, and then via the aforementioned
init() method. At the moment this requirement is not enforced in any
way,(ImmutAfterInitCell::reinit() just calls init()), but this will
not hold in the future.

Create the global structure with ImmutAfterInitCell::uninit(), since
there should be no users of the mapping before the call to
init_kernel_mapping_info(), which happens very early in the boot
process for both the stage 2 and the proper SVSM.

Signed-off-by: Carlos López <carlos.lopez@suse.com>
Add debug-only runtime checks for ImmutAfterInitCell. This prevents
using an uninitialized cell, as well as unintended double
initialization bugs. As a consequence, ImmutAfterInitCell::init() and
reinit() no longer need to be unsafe, since checks are done before
accessing the inner type.

To keep track of the initialization state we use an AtomicBool with
relaxed ordering, for code simplicity reasons.

Signed-off-by: Carlos López <carlos.lopez@suse.com>
No safety runtime checks are performed in release builds, so mark the
error variant of the return type for failable functions as
core::convert::Infallible.

Signed-off-by: Carlos López <carlos.lopez@suse.com>
@00xc
Copy link
Member Author

00xc commented Jun 13, 2023

I like this PR, but have three suggestions though. I don't want to delay the merge any further just because of nitpicking, so it might make sense to merge it as-is and do a follow-up patch -- up to Joerg to decide.

1.) You removed the #[repr(transparent)] from ImmutAfterInitCell. Have you tried whether #[cfg_attr(not(debug_assertions), repr(transparent))] would work? This way the ImmutAfterInitCell would remain to be guaranteed a zero-cost wrapper for non-debug builds.

I thought about this, but I did not like the idea of having different layout guarantees for debug and release builds. Anyhow, for release builds the struct only has one field, I do not see how it can be a non-zero-cost wrapper.

2.) For not(debug_assertions), I would turn the new ImmutAfterInitError into a core::convert::Infallible. That would potentially enable the compiler to optimize the error checks at call sites away for non-debug builds. Not sure compilers are clever enough today already, but when Infallible is made the never-type (!) one day, they probably would.

Done in the updated PR.

3.) In my personal opinion, removing the unsafe marker from the fn init() can be debated about: from my POV it serves as an indicator to users that they need to think carefully when invoking it, i.e. that there is no possible code path where an ImmutAfterInitCell can get dereferenced while still in uninitialized state. Your patch set here certainly helps a lot in establishing confidence into that, but it still doesn't provide a hard guarantee, which can be obtained only by reviewing the code. Having the unsafe{} markers at the potentially problematic call sites, would at least help to draw reviewers' attention at the right spots.

I mean, technically the unsafe part is dereferencing the cell, not so much initializing it. I think having it being unsafe might raise awareness as you mention, but it also clutters the code in places where UB is really not possible. If we want to be extra safe we could add a dedicated method to access the cell contents and have that be unsafe.

@00xc
Copy link
Member Author

00xc commented Jun 14, 2023

@nicstange do you think we could also remove the T: Copy constraint? I don't think it is needed at all.

Remove the Copy requirement for any T under the ImmutAfterInit* types,
since it is not needed.

Signed-off-by: Carlos López <carlos.lopez@suse.com>
@00xc
Copy link
Member Author

00xc commented Jun 15, 2023

With 10b7610 we could perhaps have a slightly better initialization of the filesystem. Instead of:

svsm/src/fs/fs.rs

Lines 129 to 136 in 631ae3b

static mut FS_ROOT: SvsmFs = SvsmFs::new();
pub fn initialize_fs() {
let root_dir = Arc::new(RamDirectory::new());
unsafe {
FS_ROOT.initialize(&root_dir);
}
}

We could remove SvsmFs::initialize() and have something like:

static FS_ROOT: ImmutAfterInitCell<SvsmFs> = ImmutAfterInitCell::uninit();

pub fn initialize_fs() { 
    let root_dir = Arc::new(RamDirectory::new()); 
    FS_ROOT.init(&root_dir);
}

@joergroedel joergroedel merged commit 411e12b into coconut-svsm:main Jun 19, 2023
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.

4 participants