diff mbox series

[v2,4/6] qemu-option: clean up id vs. list->merge_lists

Message ID 20201109133931.979563-5-pbonzini@redhat.com (mailing list archive)
State New, archived
Headers show
Series Deprecate or forbid crazy QemuOpts cases | expand

Commit Message

Paolo Bonzini Nov. 9, 2020, 1:39 p.m. UTC
Forbid ids if the option is intended to be a singleton, as indicated by
list->merge_lists.  This avoids that "./qemu-system-x86_64 -M q35,id=ff"
uses a "pc" machine type.  Instead it errors out.  The affected options
are "qemu-img reopen -o", "qemu-io open -o", -rtc, -M, -boot, -name,
-m, -icount, -smp, -spice.

qemu_opts_create's fail_if_exists parameter is now unnecessary:

- it is unused if id is NULL

- opts_parse only passes false if reached from qemu_opts_set_defaults,
in which case this patch enforces that id must be NULL

- other callers that can pass a non-NULL id always set it to true

Assert that it is true in the only case where "fail_if_exists" matters,
i.e. "id && !lists->merge_lists".  This means that if an id is present,
duplicates are always forbidden, which was already the status quo.

Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
 util/qemu-option.c | 27 ++++++++++++++-------------
 1 file changed, 14 insertions(+), 13 deletions(-)

Comments

Markus Armbruster Nov. 9, 2020, 4:56 p.m. UTC | #1
Paolo Bonzini <pbonzini@redhat.com> writes:

> Forbid ids if the option is intended to be a singleton, as indicated by
> list->merge_lists.

Well, ->merge_lists need not imply singleton.  Perhaps we only ever use
it that way.  Careful review is called for.

>                     This avoids that "./qemu-system-x86_64 -M q35,id=ff"
> uses a "pc" machine type.

Just like any other QemuOptsList, "machine" may have any number of
QemuOpts.  The ones with non-null ID happen to be ignored silently.
Known[*] trap for the unwary.

>                            Instead it errors out.  The affected options
> are "qemu-img reopen -o",

reopen_opts in qemu-io-cmds.c

>                           "qemu-io open -o",

empty_opts in qemu-io.c

>                                              -rtc, -M, -boot, -name,
> -m, -icount, -smp,

qemu_rtc_opts, qemu_machine_opts, qemu_boot_opts, qemu_name_opts,
qemu_mem_opts, qemu_icount_opts, qemu_smp_opts in vl.c

>                    -spice.

qemu_spice_opts in spice-core.c.

Are these all singletons?

There's also machine_opts in qemu-config.c, but that's only to make
query-command-line-options lie backward-compatibly.

>
> qemu_opts_create's fail_if_exists parameter is now unnecessary:
>
> - it is unused if id is NULL
>
> - opts_parse only passes false if reached from qemu_opts_set_defaults,
> in which case this patch enforces that id must be NULL
>
> - other callers that can pass a non-NULL id always set it to true
>
> Assert that it is true in the only case where "fail_if_exists" matters,
> i.e. "id && !lists->merge_lists".  This means that if an id is present,
> duplicates are always forbidden, which was already the status quo.

Sounds like you're specializing the code (which might be good).

> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
>  util/qemu-option.c | 27 ++++++++++++++-------------
>  1 file changed, 14 insertions(+), 13 deletions(-)
>
> diff --git a/util/qemu-option.c b/util/qemu-option.c
> index c88e159f18..91f4120ce1 100644
> --- a/util/qemu-option.c
> +++ b/util/qemu-option.c
> @@ -619,7 +619,17 @@ QemuOpts *qemu_opts_create(QemuOptsList *list, const char *id,
>  {
>      QemuOpts *opts = NULL;
>  
> -    if (id) {
> +    if (list->merge_lists) {
> +        if (id) {
> +            error_setg(errp, QERR_INVALID_PARAMETER, "id");
> +            return NULL;
> +        }
> +        opts = qemu_opts_find(list, NULL);
> +        if (opts) {
> +            return opts;
> +        }

If lists>merge_lists, you no longer check id_wellformed().  Easy enough
to fix: lift the check before this conditional.

> +    } else if (id) {
> +        assert(fail_if_exists);
>          if (!id_wellformed(id)) {
>              error_setg(errp, QERR_INVALID_PARAMETER_VALUE, "id",
>                         "an identifier");
> @@ -629,17 +639,8 @@ QemuOpts *qemu_opts_create(QemuOptsList *list, const char *id,
>          }
>          opts = qemu_opts_find(list, id);
>          if (opts != NULL) {
> -            if (fail_if_exists && !list->merge_lists) {
> -                error_setg(errp, "Duplicate ID '%s' for %s", id, list->name);
> -                return NULL;
> -            } else {
> -                return opts;
> -            }
> -        }
> -    } else if (list->merge_lists) {
> -        opts = qemu_opts_find(list, NULL);
> -        if (opts) {
> -            return opts;
> +            error_setg(errp, "Duplicate ID '%s' for %s", id, list->name);
> +            return NULL;
>          }
>      }
>      opts = g_malloc0(sizeof(*opts));

Paths through the function before the patch:

    id        fail_if_exists  merge_lists  |  return
    null      don't care      false        |  new opts
    null      don't care      true         |  existing or else new opts
    non-null  false           don't care   |  existing or else new opts
    non-null  true            true         |  existing or else new opts
    non-null  true            false        |  new opts / fail if exist

Too many cases.

After the patch:

    id        fail_if_exists  merge_lists  |  return
    non-null  don't care      true         |  fail
    null      don't care      true         |  existing or else new opts
    non-null  false           false        |  abort
    non-null  true            false        |  new opts / fail if exist
    null      don't care      false        |  new opts

Still too many :)

I'm running out of time for today, and I still need to convince myself
that the changes in behavior are all okay.

> @@ -893,7 +894,7 @@ static QemuOpts *opts_parse(QemuOptsList *list, const char *params,
>       * (if unlikely) future misuse:
>       */
>      assert(!defaults || list->merge_lists);
> -    opts = qemu_opts_create(list, id, !defaults, errp);
> +    opts = qemu_opts_create(list, id, !list->merge_lists, errp);
>      g_free(id);
>      if (opts == NULL) {
>          return NULL;



[*] Known to the QemuOpts cognoscenti, whose number could be
embarrasingly close to one.
Paolo Bonzini Nov. 9, 2020, 5:17 p.m. UTC | #2
On 09/11/20 17:56, Markus Armbruster wrote:
> Just like any other QemuOptsList, "machine" may have any number of
> QemuOpts.  The ones with non-null ID happen to be ignored silently.
> Known[*] trap for the unwary.
> 
> Are these all singletons?

They are never qemu_opts_find'd with non-NULL id, so I'd say they are.

> If lists>merge_lists, you no longer check id_wellformed().  Easy enough
> to fix: lift the check before this conditional.

Intentional: we always error with INVALID_PARAMETER, so it's pointless 
to check if the id is well-formed.

> After the patch:
> 
>     id        fail_if_exists  merge_lists  |  return
>     non-null  don't care      true         |  fail
>     null      don't care      true         |  existing or else new opts
>     non-null  false           false        |  abort
>     non-null  true            false        |  new opts / fail if exist
>     null      don't care      false        |  new opts
> 
> Still too many 

Discounting the case that aborts as it's not user-controlled (it's 
"just" a matter of inspecting qemu_opts_create callers), the rest can be 
summarized as:

- merge_lists = false: singleton opts with NULL id; non-NULL id fails

- merge_lists = true: always return new opts; non-NULL id fails if dup

> [*] Known to the QemuOpts cognoscenti, whose number could be
> embarrasingly close to one.

Maybe not one, but a single hand certainly has a surplus of fingers.

Paolo
Markus Armbruster Nov. 9, 2020, 6:38 p.m. UTC | #3
Paolo Bonzini <pbonzini@redhat.com> writes:

> On 09/11/20 17:56, Markus Armbruster wrote:
>> Just like any other QemuOptsList, "machine" may have any number of
>> QemuOpts.  The ones with non-null ID happen to be ignored silently.
>> Known[*] trap for the unwary.
>> 
>> Are these all singletons?
>
> They are never qemu_opts_find'd with non-NULL id, so I'd say they are.

We also need to check qemu_opts_foreach().

>> If lists>merge_lists, you no longer check id_wellformed().  Easy enough
>> to fix: lift the check before this conditional.
>
> Intentional: we always error with INVALID_PARAMETER, so it's pointless 
> to check if the id is well-formed.

My head was about to explode, and then it farted instead.  Sorry fore
the noise!

>> After the patch:
>> 
>>     id        fail_if_exists  merge_lists  |  return
>>     non-null  don't care      true         |  fail
>>     null      don't care      true         |  existing or else new opts
>>     non-null  false           false        |  abort
>>     non-null  true            false        |  new opts / fail if exist
>>     null      don't care      false        |  new opts
>> 
>> Still too many 
>
> Discounting the case that aborts as it's not user-controlled (it's 
> "just" a matter of inspecting qemu_opts_create callers), the rest can be 
> summarized as:
>
> - merge_lists = false: singleton opts with NULL id; non-NULL id fails

Do you mean merge_lists = true here, and ...

> - merge_lists = true: always return new opts; non-NULL id fails if dup

... = false here?

>> [*] Known to the QemuOpts cognoscenti, whose number could be
>> embarrasingly close to one.
>
> Maybe not one, but a single hand certainly has a surplus of fingers.
>
> Paolo
Paolo Bonzini Nov. 9, 2020, 6:59 p.m. UTC | #4
On 09/11/20 19:38, Markus Armbruster wrote:
>> They are never qemu_opts_find'd with non-NULL id, so I'd say they are.
> 
> We also need to check qemu_opts_foreach().

Using qemu_opts_foreach means that e.g. -name id=... was not ignored 
unlike -M id=....  However, it will be an error now.  We have to check 
if the callback or its callees use the opt->id

Reminder of how the affected options are affected:

reopen_opts in qemu-io-cmds.c	qemu_opts_find(&reopen_opts, NULL)

empty_opts in qemu-io.c		qemu_opts_find(&empty_opts, NULL)

qemu_rtc_opts			qemu_find_opts_singleton("rtc")

qemu_machine_opts		qemu_find_opts_singleton("machine")

qemu_boot_opts			
	QTAILQ_FIRST(&qemu_find_opts("bootopts")->head)

qemu_name_opts			qemu_opts_foreach->parse_name
				parse_name does not use id

qemu_mem_opts			qemu_find_opts_singleton("memory")

qemu_icount_opts		qemu_opts_foreach->do_configuree_icount
				do_configure_icount->icount_configure
				icount_configure does not use id

qemu_smp_opts
	qemu_opts_find(qemu_find_opts("smp-opts"), NULL)

qemu_spice_opts			QTAILQ_FIRST(&qemu_spice_opts.head)

To preempt your question, I can add this in the commit message.  Anyway 
I think it's relatively self-explanatory for most of these that they do 
not need "id".

>> - merge_lists = false: singleton opts with NULL id; non-NULL id fails
>
> Do you mean merge_lists = true here, and ...
> 
>> - merge_lists = true: always return new opts; non-NULL id fails if dup
>
> ... = false here?

Of course.  1-1 in the brain fart competition.

Paolo
Markus Armbruster Nov. 10, 2020, 8:29 a.m. UTC | #5
Paolo Bonzini <pbonzini@redhat.com> writes:

> On 09/11/20 19:38, Markus Armbruster wrote:
>>> They are never qemu_opts_find'd with non-NULL id, so I'd say they are.
>> 
>> We also need to check qemu_opts_foreach().
>
> Using qemu_opts_foreach means that e.g. -name id=... was not ignored 
> unlike -M id=....  However, it will be an error now.  We have to check 
> if the callback or its callees use the opt->id

Yes.

> Reminder of how the affected options are affected:

In general, the patch rejects only options that used to be silently
ignored.  As we will see below, there are exceptions where we reject
options that used to work.  Do we want that?

If yes, discuss in commit message and release notes.

More below.

> reopen_opts in qemu-io-cmds.c	qemu_opts_find(&reopen_opts, NULL)

    qopts = qemu_opts_find(&reopen_opts, NULL);
    opts = qopts ? qemu_opts_to_qdict(qopts, NULL) : qdict_new();
    qemu_opts_reset(&reopen_opts);

I guess this could use qemu_find_opts_singleton().  Not sure we want it,
and even if we do, it's not this patch's job.

>
> empty_opts in qemu-io.c		qemu_opts_find(&empty_opts, NULL)

Likewise.

> qemu_rtc_opts			qemu_find_opts_singleton("rtc")
>
> qemu_machine_opts		qemu_find_opts_singleton("machine")

No surprises or funnies here.

> qemu_boot_opts			
> 	QTAILQ_FIRST(&qemu_find_opts("bootopts")->head)

Spelled "boot-opts", and used with a variable spliced on, which defeated
my grep.  It's in hw/nvram/fw_cfg.c and hw/s390x/ipl.c.

vl.c additionally has qemu_opts_find(qemu_find_opts("boot-opts"), NULL).

If the user passes multiple -boot with different ID, we merge the ones
with same ID, and then vl.c gets the (merged) one without ID, but the
other two get the (merged) one that contains the first -boot.  All three
silently ignore the ones they don't get.  Awesomely weird.  I'd call it
a bug.

Test case:

    $ upstream-qemu -S -display none -boot strict=on,id=id -boot strict=off

vl.c uses strict=off, but fw_cfg.c uses strinct=on,id=id.

Outlawing "id" with .merge_lists like this patch does turns the cases
where the two methods yield different results into errors.  This could
break working (if crazy) configurations.  That's okay; I can't see how
the craziness could be fixed without breaking crazy configurations.

I think the commit message should cover this.

I believe we could use qemu_find_opts_singleton() in all three spots.
Not this patch's job.

>
> qemu_name_opts			qemu_opts_foreach->parse_name
> 				parse_name does not use id

First, we use .merge_lists to merge -name with the same ID into a single
QemuOpts, then we use code to merge the resulting QemuOpts, ignoring ID.
Stupid.

Outlawing "id" with .merge_lists like this patch does makes the second
merge a no-op.

This is one of the cases where we reject options that used to work.

If that's wanted, follow-up patch to drop the useless second merge.

If not, unset qemu_name_opts.merge_lists in a separate patch before this
one.

> qemu_mem_opts			qemu_find_opts_singleton("memory")

No surprises or funnies here.

> qemu_icount_opts		qemu_opts_foreach->do_configuree_icount
> 				do_configure_icount->icount_configure
> 				icount_configure does not use id

Same story as for qemu_name_opts.

> qemu_smp_opts
> 	qemu_opts_find(qemu_find_opts("smp-opts"), NULL)

No surprises or funnies here.

> qemu_spice_opts			QTAILQ_FIRST(&qemu_spice_opts.head)

We use the merged one that contains the first -spice, and silently
ignore the rest.  At least we're consistent here.

This is one of the cases where we reject options that used to work.

If that's wanted, follow-up patch to replace the QTAILQ_FIRST() by
something saner.

If not, unset qemu_spice_opts.merge_lists in a separate patch before
this one, and merge like we do for qemu_name_opts.

> To preempt your question, I can add this in the commit message.  Anyway 
> I think it's relatively self-explanatory for most of these that they do 
> not need "id".

Except where they don't need it, but permit it to have an effect anyway.

One of the issues with QemuOpts is that there are too many "clever" ways
to use it.

>>> - merge_lists = false: singleton opts with NULL id; non-NULL id fails
>>
>> Do you mean merge_lists = true here, and ...
>> 
>>> - merge_lists = true: always return new opts; non-NULL id fails if dup
>>
>> ... = false here?
>
> Of course.  1-1 in the brain fart competition.

Hah!
Paolo Bonzini Nov. 10, 2020, 8:39 a.m. UTC | #6
On 10/11/20 09:29, Markus Armbruster wrote:
> As we will see below, there are exceptions where we reject
> options that used to work.  Do we want that?

I think that, as long as we gain in consistency, we do.  The special 
casing of "id" is a wart of the current parser and the less it shows 
that "id" is treated specially, the best.

Deprecating or downright rejecting working combinations is always 
walking a fine line, but here we're almost in https://xkcd.com/1172/ 
territory.

Paolo
Markus Armbruster Nov. 10, 2020, 9:54 a.m. UTC | #7
Paolo Bonzini <pbonzini@redhat.com> writes:

> On 10/11/20 09:29, Markus Armbruster wrote:
>> As we will see below, there are exceptions where we reject
>> options that used to work.  Do we want that?
>
> I think that, as long as we gain in consistency, we do.  The special
> casing of "id" is a wart of the current parser and the less it shows 
> that "id" is treated specially, the best.
>
> Deprecating or downright rejecting working combinations is always
> walking a fine line, but here we're almost in https://xkcd.com/1172/ 
> territory.

I find it hard to disagree ;)
diff mbox series

Patch

diff --git a/util/qemu-option.c b/util/qemu-option.c
index c88e159f18..91f4120ce1 100644
--- a/util/qemu-option.c
+++ b/util/qemu-option.c
@@ -619,7 +619,17 @@  QemuOpts *qemu_opts_create(QemuOptsList *list, const char *id,
 {
     QemuOpts *opts = NULL;
 
-    if (id) {
+    if (list->merge_lists) {
+        if (id) {
+            error_setg(errp, QERR_INVALID_PARAMETER, "id");
+            return NULL;
+        }
+        opts = qemu_opts_find(list, NULL);
+        if (opts) {
+            return opts;
+        }
+    } else if (id) {
+        assert(fail_if_exists);
         if (!id_wellformed(id)) {
             error_setg(errp, QERR_INVALID_PARAMETER_VALUE, "id",
                        "an identifier");
@@ -629,17 +639,8 @@  QemuOpts *qemu_opts_create(QemuOptsList *list, const char *id,
         }
         opts = qemu_opts_find(list, id);
         if (opts != NULL) {
-            if (fail_if_exists && !list->merge_lists) {
-                error_setg(errp, "Duplicate ID '%s' for %s", id, list->name);
-                return NULL;
-            } else {
-                return opts;
-            }
-        }
-    } else if (list->merge_lists) {
-        opts = qemu_opts_find(list, NULL);
-        if (opts) {
-            return opts;
+            error_setg(errp, "Duplicate ID '%s' for %s", id, list->name);
+            return NULL;
         }
     }
     opts = g_malloc0(sizeof(*opts));
@@ -893,7 +894,7 @@  static QemuOpts *opts_parse(QemuOptsList *list, const char *params,
      * (if unlikely) future misuse:
      */
     assert(!defaults || list->merge_lists);
-    opts = qemu_opts_create(list, id, !defaults, errp);
+    opts = qemu_opts_create(list, id, !list->merge_lists, errp);
     g_free(id);
     if (opts == NULL) {
         return NULL;