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

Support for D-Bus file descriptors #5992

Closed

Conversation

Projects
None yet
4 participants
@larskarlitski
Copy link
Contributor

commented Feb 28, 2017

This is based on @stefwalter's initial implementation at #4883. I've rebased it, fixed compile errors, and started to implement the missing bits. Still very much work in progress, but I'm opening this pull request to start the discussion again.

This is basic support for file descriptors passed over DBus. Several APIs like to include stuff like this in their APIs.

  • Initial implementation
  • Figure out possible fd exaustion until DBus channel is closed.
  • Figure out a test case.
  • An actual use case.
  • Documentation
  • Unit tests, and fix bugs

Fixes #4380

@larskarlitski larskarlitski referenced this pull request Feb 28, 2017

Closed

Support for DBus file descriptors #4883

2 of 6 tasks complete

@larskarlitski larskarlitski force-pushed the larskarlitski:dbus-file-descriptor branch 2 times, most recently from 11e0b8d to efe39a3 Mar 8, 2017

@larskarlitski

This comment has been minimized.

Copy link
Contributor Author

commented Mar 8, 2017

I think this is ready for a first review.

I've moved the internal channel logic into CockpitPipeChannel, because that's the one that deals with fds instead of addresses.

I've also added a small test case which opens a pipe and passes the reader to cockpit via a D-Bus message.

I've also documented that cockpit can now receive (but not sent) file descriptors over D-Bus. Do you think this warrants an extra section in the docs?

Does anyone have ideas for an actual use case that's small enough in scope to implement before 7.4?

@larskarlitski larskarlitski changed the title WIP: Support for D-Bus file descriptors Support for D-Bus file descriptors Mar 8, 2017

@larskarlitski larskarlitski removed the needswork label Mar 8, 2017

@larskarlitski larskarlitski requested a review from stefwalter Mar 9, 2017

channel.onmessage = function (event, data) {
assert.equal(data, 'Hello, fd');
channel.close();
QUnit.start();

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

I don't think you are supposed to call QUint.start() twice in the same test, the always() handler already does that. Or is that somehow "refcounted" and the extra QUnit.stop() above makes that necessary? This could do with a comment.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

Yes, start/stop are refcounted. We need this here because the done() path kicks off another async call to wait for the first message. We still need it in always() in case the dbus call fails. I'll add a comment.

<varlistentry>
<term><code>HANDLE 'h'</code></term>
<listitem><para>A javascript object that describes a cockpit channel which represents the
passed file descriptor. Pass it to <link linkend="cockpit-channels-channel">

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

Could this be more specific about how its members look like?

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

I left it intentionally opaque, because the channel is openend via an internal id which is useless anyway. I'll add that this is always a stream channel.

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

Good point about the opacity, so fine as it is.

@@ -66,6 +66,21 @@ typedef struct {

G_DEFINE_TYPE (CockpitPipeChannel, cockpit_pipe_channel, COCKPIT_TYPE_CHANNEL);

GHashTable *internal_fds;

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

How does this simple hash table keep track of which fds are for reading and which for writing? Or do we only support receiving read fds for now?

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

Based on the discussion below, this comment is moot.

@@ -1130,4 +1130,28 @@ function dbus_track_tests(channel_options, bus_name) {
});
});
});

QUnit.asyncTest("receive fd", function() {

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

This tests receiving a read fd. It would be good to also have a test for getting a write fd, unless this shouldn't be supported (for now) -- but then that should be documented.

@@ -384,6 +407,10 @@ cockpit_pipe_channel_prepare (CockpitChannel *channel)
else
self->pipe = cockpit_pipe_spawn ((const gchar **)argv, (const gchar **)env, dir, flags);
}
else if (internal && lookup_internal_fd (internal, &fd))
{
self->pipe = cockpit_pipe_new (internal, fd, fd);

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

This specifies the same fd for both reading and writing, which is error prone and misleading. Unix pipes are unidirectional, and the sender and receiver need to agree which one gets passed and which one the sender keeps. This is related to my question above how we keep track of reading and writing fds.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

Hm, good point. Is there any way to know if the fd we get from D-Bus is read- or write-only? The pipe is only for the test, but I guess this could happen in a practical use case as well.

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

The only way known to me is to check if fdopen() with a particular mode returns EINVAL or succeeds. But I guess I may have misunderstood this -- so you are saying that cockpit itself should not touch/use the fd at all, just pass it between the service and the client? That would actually make a lot of sense, I just got misled when reviewing this.

Do I understand this alright -- with this, when the client opens a cockpit.channel() on this handle, it's still the client's responsibility to know whether it can read and/or write into this? If so, then we should really have a test case that ensures that cockpit doesn't touch the fds themselves -- i. e. trying to write to a read fd throws a sensible error, and we should have a test case for the write end of a pipe only to ensure that this direction works as well.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

Do I understand this alright

Yep, that's exactly it. I'll add the test cases you suggested.

cockpit_pipe_channel_add_internal_fd (gint fd)
{
/* We are not multi-threaded. Also don't make this look like normal fd numbers */
static guint64 unique = 911111;

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

It seems to me that the keys of that GHashTable are mostly arbitrary and opaque, and we don't actually need them for anything? I must say I don't like these magic numbers. Would it be possible to use it as a set (g_hash_table_add() and g_hash_table_contains() and store the fds directly as keys, and use the fd as-is in the ID? Or can it happen that an fd gets closed and reused, but the corresponding cockpit channel doesn't get closed? (That would be an internal inconsistency and most likely lead to errors anyway?)

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

We need a unique string to refer to the channel from cockpit. Maybe a random string that doesn't look like it could be a fd is even better?

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

So an fd isn't unique enough? Once a cockpit channel gets closed because the underlying fd closes, a new one must never use the same name again, even if the next passed fd has the same number than the previously closed one?

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

No, the fd might be closed and reused before the client calls cockpit.channel(), which would give it the wrong fd.

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

s/wrong fd/wrong id/ I suppose. Understood, so let's not dwell on this further -- it's just an opaque ID, so we can change it later on if we want.

internal_fds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, internal_fd_free);

id = g_strdup_printf ("file-descriptor-%" G_GUINT64_FORMAT, unique++);
g_hash_table_replace (internal_fds, id, GINT_TO_POINTER (fd));

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

This comment would be moot when moving to a set, but I think you should use g_assert (g_hash_table_add ()) here -- like this we don't ever expect an ID to be already present.

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

I'd actually do the assertion here -- as we've discussed above, we do want the IDs to be unique even if the fds get reused, so this prevents falling into a trap in the future when someone "simplifies" the ID housekeeping.

{
g_hash_table_destroy (internal_fds);
internal_fds = NULL;
}

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

I'm not sure if that's worth the overhead -- if you keep adding and removing fds, you would always construct and delete the hash table. Seems easier and more efficient to just keep it around?

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

Indeed :)

/* And only keep the last N ready for opening channels */
while (g_queue_get_length (context->fdids) > MAX_RECEIVED_DBUS_FDS)
{
old = (const gchar *)g_queue_pop_head (context->fdids);

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

It seems a bit odd to silently kill the oldest fd instead of rejecting to receive a new one and returning an error instead? This feels much harder to handle from clients than checking for an error on sending. At least this dropping should cause a warning somewhere.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 9, 2017

Author Contributor

Client code can decide to ignore a passed fd. So we have to keep the fd open for a while, so that the cockpit channel can be created if a client does not ignore it. The problem is when to close fds that were ignored. We decided to just keep the last N fds open.

This is why we can't warn about it, because many of the instances are just normal operation. If the limit turns out too low, the client code will get an error when it establishes the channel.

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

Makes sense, thanks!

g_signal_handler_disconnect (peer->cache, peer->update_sig);
g_object_run_dispose (G_OBJECT (peer->cache));
g_object_unref (peer->cache);
}

This comment has been minimized.

Copy link
@martinpitt

martinpitt Mar 9, 2017

Member

This should be a separate commit. (Not important, though)

@larskarlitski larskarlitski force-pushed the larskarlitski:dbus-file-descriptor branch from b160634 to a878cc1 Mar 9, 2017

@larskarlitski

This comment has been minimized.

Copy link
Contributor Author

commented Mar 9, 2017

Update: implemented @martinpitt's suggestions. Thanks a lot!

@martinpitt
Copy link
Member

left a comment

Thanks for the updates! Looks fine to me now, and we can change the ID generation to something nicer in the future if we feel like it, as they are opaque. So, 👍 from me, but I won't merge yet as I want to give @stefwalter a chance to review this too.

@martinpitt martinpitt removed the bot label Mar 9, 2017


g_assert (condition == G_IO_IN);

read (fd, buffer, 100);

This comment has been minimized.

Copy link
@dperpeet

dperpeet Mar 10, 2017

Member

semaphore complains here while checking:

src/common/mock-service.c: In function ‘test_fd_pipe_readable’:
src/common/mock-service.c:469:8: error: ignoring return value of ‘read’, declared with attribute warn_unused_result [-Werror=unused-result]
   read (fd, buffer, 100);
        ^

Even in a mock function it's probably good practice to check this.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 10, 2017

Author Contributor

Thanks for the catch. I've added a similar check like I have for write. I think we can safely assume that we always get all data at once since this is a pipe and very little data.

@dperpeet

This comment has been minimized.

Copy link
Member

commented Mar 10, 2017

I didn't review anything besides the Semaphore error.

@larskarlitski larskarlitski force-pushed the larskarlitski:dbus-file-descriptor branch from a878cc1 to 089e9e4 Mar 10, 2017

semaphore error was fixed

@stefwalter
Copy link
Contributor

left a comment

The main change I would suggest is transferring ownership of the fd (or duping it, if ownership is shared).

@@ -384,6 +407,10 @@ cockpit_pipe_channel_prepare (CockpitChannel *channel)
else
self->pipe = cockpit_pipe_spawn ((const gchar **)argv, (const gchar **)env, dir, flags);
}
else if (internal && lookup_internal_fd (internal, &fd))

This comment has been minimized.

Copy link
@stefwalter

stefwalter Mar 11, 2017

Contributor

So the following line claims the fd, however lookup_internal_fd doesn't steal it from the hashtable or otherwise transfer ownership.

I imagine that such fd channels are open-once channels, after which the identifier becomes invalid? Or did you imagine them to be able to open many channels to the same fd? This is obviously a corner case, but either way needs to be made consistent.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 12, 2017

Author Contributor

I didn't bother because you wrote this

Yes, in theory multiple channels can be opened with the same unique string. This has similar properties to file descriptors used in process.

in bug #4380. Changed it to remove the channel and added a test.

This comment has been minimized.

Copy link
@stefwalter

stefwalter Mar 12, 2017

Contributor

Well it would have been fine if you had dup()'d it ... then the channel could have been opened more than once. But I think this makes sense, just have it be opened once.

GHashTable *internal_fds;

static gboolean
lookup_internal_fd (const gchar *name, gint *fdp)

This comment has been minimized.

Copy link
@stefwalter

stefwalter Mar 11, 2017

Contributor

Arguments should be on different lines.

if (!internal_fds)
internal_fds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, internal_fd_free);

id = g_strdup_printf ("file-descriptor-%" G_GUINT64_FORMAT, unique++);

This comment has been minimized.

Copy link
@stefwalter

stefwalter Mar 11, 2017

Contributor

This name is a bit misleading. For folks debugging this, they'll think that the file descriptor N is being opened. Worth choosing another name like 'internal-stream-XXX' or something like that? If you don't think so, I'm fine with keeping it as it is. Just consider it.

This comment has been minimized.

Copy link
@larskarlitski

larskarlitski Mar 12, 2017

Author Contributor

Changed it.

stefwalter and others added some commits Feb 28, 2017

bridge: Allow use of file descriptors passed over DBus
This works by replacing the file descriptor with a channel
identifier and then allowing that channel to be opened
as an internal stream channel.

There is a maximum number of file descriptors that will
be held in this state by a dbus client until those file
descriptors are used.

Fixes #4380
bridge: fix possible crash in CockpitDBusJson
Don't attempt to free 'peer' member if it is unset.

@larskarlitski larskarlitski force-pushed the larskarlitski:dbus-file-descriptor branch from 089e9e4 to c90f4d7 Mar 12, 2017

@larskarlitski

This comment has been minimized.

Copy link
Contributor Author

commented Mar 12, 2017

Update: address issues raised by @stefwalter. Thanks!

@larskarlitski larskarlitski deleted the larskarlitski:dbus-file-descriptor branch Mar 13, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.