resolv/resolv.h: allow alternative resolv.conf files

Message ID 20170817143231.6cafq26aqlhvcazi@cs.unibo.it
State New, archived
Headers

Commit Message

Renzo Davoli Aug. 17, 2017, 2:32 p.m. UTC
  > _PATH_RESCONF really has to be a string literal, and you can't use a GNU
> C extension in such a way in an installed header file.
> 
> The environment variable would have to be ignored in AT_SECURE mode, so
> you have to use __libc_secure_getenv or put it into unsecvars.h.

Here is a different implementation:
* it does not use any gcc extension
* use __libc_secure_getenv instead of getenv
* do not change installed header files.

Thank you.	
	renzo

2017-08-17 Renzo Davoli <renzo@cs.unibo.it>
  

Comments

Carlos O'Donell Aug. 17, 2017, 3:20 p.m. UTC | #1
On 08/17/2017 10:32 AM, Renzo Davoli wrote:
>> _PATH_RESCONF really has to be a string literal, and you can't use a GNU
>> C extension in such a way in an installed header file.
>>
>> The environment variable would have to be ignored in AT_SECURE mode, so
>> you have to use __libc_secure_getenv or put it into unsecvars.h.
> 
> Here is a different implementation:
> * it does not use any gcc extension
> * use __libc_secure_getenv instead of getenv
> * do not change installed header files.

You ask:

"Why to obtain a so simple feature should I have to overlay mount /etc?"

and the answer is:

* It complicates the glibc design when an overlay mount already exists.

* Environment variables are brittle and may be cleared in certain
  or unused in certain circumstances.

The most robust solution is an overlay mount in /etc.

Is there anything technically wrong with the overlay mount?

The overlay mount is semantically clearer than another env var that
changes existing behaviour.
  
Florian Weimer Aug. 17, 2017, 4:13 p.m. UTC | #2
On 08/17/2017 04:32 PM, Renzo Davoli wrote:
>> _PATH_RESCONF really has to be a string literal, and you can't use a GNU
>> C extension in such a way in an installed header file.
>>
>> The environment variable would have to be ignored in AT_SECURE mode, so
>> you have to use __libc_secure_getenv or put it into unsecvars.h.
> 
> Here is a different implementation:

> * it does not use any gcc extension
> * use __libc_secure_getenv instead of getenv
> * do not change installed header files.

That's certainly better.  Do you have an example of another system which
offers a similar environment variable?

Thanks,
Florian
  
Renzo Davoli Aug. 17, 2017, 4:18 p.m. UTC | #3
On Thu, Aug 17, 2017 at 11:20:53AM -0400, Carlos O'Donell wrote:
> Is there anything technically wrong with the overlay mount?

Yes. AFAIK it requires CAP_SYS_ADMIN to mount.  (see clone(2) or unshare(2)).

Please tell me how to mount an overlay in a user namespace, this would give
a solution to my problem (although a sub-optimal one, it is much more complex 
to use than an env var, IMHO).

thank you in advance.

	renzo
  
Florian Weimer Aug. 17, 2017, 4:24 p.m. UTC | #4
On 08/17/2017 06:18 PM, Renzo Davoli wrote:
> On Thu, Aug 17, 2017 at 11:20:53AM -0400, Carlos O'Donell wrote:
>> Is there anything technically wrong with the overlay mount?
> 
> Yes. AFAIK it requires CAP_SYS_ADMIN to mount.  (see clone(2) or unshare(2)).
> 
> Please tell me how to mount an overlay in a user namespace, this would give
> a solution to my problem (although a sub-optimal one, it is much more complex 
> to use than an env var, IMHO).

resolv/tst-resolv-res_init-skeleton.c uses the support/ framework to
replace /etc in the test chroot, precisely because the /etc/resolv.conf
path is currently hard-coded.  The test does not need root permissions
to run on current Fedora, for instance.

Is this what you had in mind?

Thanks,
Florian
  
Szabolcs Nagy Aug. 17, 2017, 4:48 p.m. UTC | #5
On 17/08/17 17:24, Florian Weimer wrote:
> resolv/tst-resolv-res_init-skeleton.c uses the support/ framework to
> replace /etc in the test chroot, precisely because the /etc/resolv.conf
> path is currently hard-coded.  The test does not need root permissions
> to run on current Fedora, for instance.

how does chroot work without root on fedora?
  
Florian Weimer Aug. 17, 2017, 4:54 p.m. UTC | #6
On 08/17/2017 06:48 PM, Szabolcs Nagy wrote:
> On 17/08/17 17:24, Florian Weimer wrote:
>> resolv/tst-resolv-res_init-skeleton.c uses the support/ framework to
>> replace /etc in the test chroot, precisely because the /etc/resolv.conf
>> path is currently hard-coded.  The test does not need root permissions
>> to run on current Fedora, for instance.
> 
> how does chroot work without root on fedora?

See support_become_root:

bool
support_become_root (void)
{
#ifdef CLONE_NEWUSER
  if (unshare (CLONE_NEWUSER | CLONE_NEWNS) == 0)
    /* Even if we do not have UID zero, we have extended privileges at
       this point.  */
    return true;
#endif
  if (setuid (0) != 0)
    {
      printf ("warning: could not become root outside namespace (%m)\n");
      return false;
    }
  return true;
}

Florian
  
Renzo Davoli Aug. 18, 2017, 3:07 p.m. UTC | #7
> That's certainly better.  Do you have an example of another system which
> offers a similar environment variable?
I don't know actually. I can search around.
Features similar to namespaces should exist in BSD and Solaris (zones).

I have found that someone else had a similar problem
https://github.com/hadess/resolvconf-override
This library rewrites _res.nsaddr_list[i]... which is more a dirty trick 
than a solution. It may break with future changes in glibc.

Queries for per-user configuration of resolv.conf can be found googling around:
e.g. using the key "per-user resolv.conf".

I have tested the proposals I got from this mailing list (thank you all!).

The code at the end of this message partially solves my problem.
It can be tested in this way:

$ gcc -o newresconf newresconf.c
$ echo nameserver 80.80.80.80 > /tmp/resolv.conf
$ cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 127.0.0.1
$ PATH_RESCONF=/tmp/resolv.conf ./newresconf bash
$ cat /etc/resolv.conf
nameserver 80.80.80.80
$ exit

There are still issues: e.g. setuid does not work in this namespace so e.g. I got:
$ ping 127.0.0.1
ping: socket: Operation not permitted

I had to define a USER+NS namespace, remap uids and gids (which needs a separate process),
and finally mount-bind the file.

It seems a bizantine procedure just to tell resolv to use a different resolv.conf.
There is some extra work for the kernel (keep track of the new namespaces, separate mounttables,
more mount items) for a problem that could be solved entirely at user level.

resolv.conf is just a configuration file of the library. A security model based
on the idea that something is safe because the library forbids the access to a
functionality is weak. System calls must enforce processes to behave correctly,
system calls can really forbid bad behaviors. When a syscall says no it is no.
Users can run different libraries if it is just the library to deny something.

So, why not providing a linear and straightforward simple way to solve a simple problem?

Providing complex methods only, does not make the system safer but prevents developers
from writing services or at least from writing good clean code to implement their services.
Isn't it?

(just my 2 euro cents).

thank you

	renzo

/* 
 * newresconf: experimental code. bind /etc/resolv.conf to a different file.
 * code inspired by vdens GPLv2+ by Renzo Davoli et al.
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sched.h>
#include <limits.h>
#include <errno.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/mount.h>

#define errExit(msg)    ({ perror(msg); exit(EXIT_FAILURE); })

static void uid_gid_map(pid_t pid) {
	char map_file[PATH_MAX];
	FILE *f;
	uid_t euid = geteuid();
	gid_t egid = getegid();
	snprintf(map_file, PATH_MAX, "/proc/%d/uid_map", pid);
	f = fopen(map_file, "w");
	if (f) {
		fprintf(f,"%d %d 1\n",euid,euid);
		fclose(f);
	}
	snprintf(map_file, PATH_MAX, "/proc/%d/setgroups", pid);
	f = fopen(map_file, "w");
	if (f) {
		fprintf(f,"deny\n");
		fclose(f);
	}
	snprintf(map_file, PATH_MAX, "/proc/%d/gid_map", pid);
	f = fopen(map_file, "w");
	if (f) {
		fprintf(f,"%d %d 1\n",egid,egid);
		fclose(f);
	}
}

static void unsharens(void) {
	int pipe_fd[2];
	pid_t child_pid;
	char buf[1];
	if (pipe2(pipe_fd, O_CLOEXEC) == -1)
		errExit("pipe");
	switch (child_pid = fork()) {
		case 0:
			close(pipe_fd[1]);
			read(pipe_fd[0], &buf, sizeof(buf));
			uid_gid_map(getppid());
			exit(0);
		default:
			close(pipe_fd[0]);
			if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == -1)
				errExit("unshare");
			close(pipe_fd[1]);
			if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */
				errExit("waitpid");
			break;
		case -1:
			errExit("unshare fork");
	}
}

void new_resolv_conf(const char *path_resconf) {

	if (path_resconf && *path_resconf) {
		unsharens();
		if (mount(path_resconf, "/etc/resolv.conf", "", MS_MGC_VAL|MS_BIND, NULL) < 0)
			errExit("mount");
	}
}

int main(int argc, char *argv[])
{
	char **cmdargv;
	char *argvsh[]={getenv("SHELL"),NULL};

	if (argc == 1)
		cmdargv = argvsh;
	else
		cmdargv = argv + 1;

	if (cmdargv[0] == NULL) {
		fprintf(stderr, "Error: $SHELL env variable not set\n");
		exit(EXIT_FAILURE); 
	}

	new_resolv_conf(getenv("PATH_RESCONF"));

	execvp(cmdargv[0], cmdargv);

	exit(EXIT_SUCCESS);
}
  
Carlos O'Donell Aug. 18, 2017, 4:41 p.m. UTC | #8
On 08/18/2017 11:07 AM, Renzo Davoli wrote:
> It seems a bizantine procedure just to tell resolv to use a different
> resolv.conf.

I agree, but that's not the extent of this...

It is not just about resolv.conf, the point is to avoid all the other
problems you may also run into. The point is to provide you with a robust
solution so you don't have to come back for all the other environment
variables you may need to tweak the runtime.

For each of these environment variables there is security risk, auditing
issues, maintenance costs etc.

> There is some extra work for the kernel (keep track of the new
> namespaces, separate mounttables, more mount items) for a problem
> that could be solved entirely at user level.

Yes, you are right.

You are paying the cost for a design abstraction.

Design abstractions make it easier for your users to know how your
systems work without needing to learn something new e.g. a new
environment variable.

> resolv.conf is just a configuration file of the library. A security
> model based on the idea that something is safe because the library
> forbids the access to a functionality is weak. System calls must
> enforce processes to behave correctly, system calls can really forbid
> bad behaviors. When a syscall says no it is no. Users can run
> different libraries if it is just the library to deny something.

Reductio ad absurdum is not valid here, there are layers of security
and core userspace libraries play a key role here in providing security.
 
> So, why not providing a linear and straightforward simple way to
> solve a simple problem?

It adds to the overall complexity of the library, and makes it harder
to know how to correctly configure your application and know exactly
where it will get resolver information from.

Users are already deploying containers to solve other problems and
if your solution dovetails nicely into this, then they will appreciate
that.

> Providing complex methods only, does not make the system safer but
> prevents developers from writing services or at least from writing
> good clean code to implement their services. Isn't it?

Making there only be 1 way to set the resolver configuration makes it
easier for application developers, and administrator to administer
their systems.

Using namespaces, including mount namespaces, could be made easier.
There is an argument for that, but this argument should not apply
as the reason for making there be 2 ways to set the resolver information.
This complicates configuration and debugging for users.

If anything I would focus on making namespace easier :-)

--
Cheers,
Carlos.
  
Renzo Davoli Aug. 22, 2017, 9:37 a.m. UTC | #9
I have released userbindconf, a library and a utility command to
bind-mount files and directores in user-namespaces.
https://github.com/rd235/userbindmount

Nevertheless, the whole stuff sounds as a workaround to me.

On Fri, Aug 18, 2017 at 12:41:50PM -0400, Carlos O'Donell wrote:
> On 08/18/2017 11:07 AM, Renzo Davoli wrote:
> > It seems a bizantine procedure just to tell resolv to use a different
> > resolv.conf.
> I agree, but that's not the extent of this...
...
> > There is some extra work for the kernel (keep track of the new
> > namespaces, separate mounttables, more mount items) for a problem
> > that could be solved entirely at user level.
> Yes, you are right.
> You are paying the cost for a design abstraction.
> 
> Design abstractions make it easier for your users to know how your
> systems work without needing to learn something new e.g. a new
> environment variable.

An abstraction should be a simple concept able to represent a wide
range of situations. Astractions should be simpler than the 
"abstracted" concepts.

Using complicated methods to override a lack of generalization of
the lower layer is more a "distraction" than an "abstraction".

There are several environment variables already in glibc to
configure the name resolving process:
RES_OPTIONS, RESOLV_ADD_TRIM_DOMAINS RESOLV_MULTI 
RESOLV_OVERRIDE_TRIM_DOMAINS RESOLV_REORDER 
RESOLV_SERV_ORDER RESOLV_SPOOF_CHECK 

What to say about:
RESOLV_HOST_CONF
?

It redefines the path to be used instead of /etc/host.conf.
Isn't this very close to what I proposed as PATH_RESCONF?
(which could be renamed as RESOLV_RESCONF to be consistent
 with the others).

Is there really the need for a user-namespace for a so simple
(and common) generalization?
Timezones and locales should be implemented as "abstractions"
requiring namespaces if this is the idea, isn't it?
Try to think on how complex would be the management of
timezones and locales it this was the case...

Thank you

	renzo
  
Carlos O'Donell Aug. 23, 2017, 4:07 a.m. UTC | #10
On 08/22/2017 05:37 AM, Renzo Davoli wrote:
> I have released userbindconf, a library and a utility command to
> bind-mount files and directores in user-namespaces.
> https://github.com/rd235/userbindmount
> 
> Nevertheless, the whole stuff sounds as a workaround to me.
> 
> On Fri, Aug 18, 2017 at 12:41:50PM -0400, Carlos O'Donell wrote:
>> On 08/18/2017 11:07 AM, Renzo Davoli wrote:
>>> It seems a bizantine procedure just to tell resolv to use a different
>>> resolv.conf.
>> I agree, but that's not the extent of this...
> ...
>>> There is some extra work for the kernel (keep track of the new
>>> namespaces, separate mounttables, more mount items) for a problem
>>> that could be solved entirely at user level.
>> Yes, you are right.
>> You are paying the cost for a design abstraction.
>>
>> Design abstractions make it easier for your users to know how your
>> systems work without needing to learn something new e.g. a new
>> environment variable.
> 
> An abstraction should be a simple concept able to represent a wide
> range of situations. Astractions should be simpler than the 
> "abstracted" concepts.

I agree.

> Using complicated methods to override a lack of generalization of
> the lower layer is more a "distraction" than an "abstraction".

There is a difference between the high-level abstraction and how it
is implemented. You may not like the complexity of the present
implementation, but the abstraction remains.

> There are several environment variables already in glibc to
> configure the name resolving process:
> RES_OPTIONS, RESOLV_ADD_TRIM_DOMAINS RESOLV_MULTI 
> RESOLV_OVERRIDE_TRIM_DOMAINS RESOLV_REORDER 
> RESOLV_SERV_ORDER RESOLV_SPOOF_CHECK 
> 
> What to say about:
> RESOLV_HOST_CONF
> ?

They exist because there was no other per-process solution, and
because glibc is backwards compatible we must keep them forever.

That doesn't mean they are a good design for today.

> It redefines the path to be used instead of /etc/host.conf.
> Isn't this very close to what I proposed as PATH_RESCONF?
> (which could be renamed as RESOLV_RESCONF to be consistent
>  with the others).

Yes it does.
 
> Is there really the need for a user-namespace for a so simple
> (and common) generalization?

Need is difficult to answer. There are performance, maintenance,
and security implications to take into account.

> Timezones and locales should be implemented as "abstractions"
> requiring namespaces if this is the idea, isn't it?
> Try to think on how complex would be the management of
> timezones and locales it this was the case...

Reductio ad absurdium should not be used to argue the technical
merits of a new interface, the old interface already exists and
may not be a good example of modern design.

Locales *are* being implemented in containers exactly how you
envision them. Thin containers provisioned with only C.UTF-8
(which several distros offer) are setup to provide microservices
which never need other locales (reduces QA and testing).

The locale, timezone, and resolver states can all be made
thread-local. This makes it harder to avoid providing all locales
for selection by threads which need specific languages to provide
those services in the language needed. Similarly some threads may
operate on different timezone data and so need all the timezone
data. Lastly, likewise different threads could use different
resolver state, you just need to configure it.

I see three solutions present today:

(a) Per-thread: Modify _res to get per-thread state. May go away
    some day, but backwards binary compat will always exist.

(b) Per-process / Per-thread:
    Alternate resolver library e.g. flexible DNS client library.
    The glibc resolver API is a standards compatible no-frills stub
    resolver.

(c) Per-process: mount namespace which overrides /etc/resolv.conf
    for your process.
  

Patch

diff --git a/resolv/res_init.c b/resolv/res_init.c
index fa46ce7813..bca60568ad 100644
--- a/resolv/res_init.c
+++ b/resolv/res_init.c
@@ -545,7 +545,7 @@  __resolv_conf_load (struct __res_state *preinit)
   /* Ensure that /etc/hosts.conf has been loaded (once).  */
   _res_hconf_init ();
 
-  FILE *fp = fopen (_PATH_RESCONF, "rce");
+  FILE *fp = fopen (__resolv_path_resconf(), "rce");
   if (fp == NULL)
     switch (errno)
       {
diff --git a/resolv/resolv-internal.h b/resolv/resolv-internal.h
index 32dc44777e..aa52497550 100644
--- a/resolv/resolv-internal.h
+++ b/resolv/resolv-internal.h
@@ -97,4 +97,8 @@  int __res_nopt (struct resolv_context *, int n0,
 int __inet_pton_length (int af, const char *src, size_t srclen, void *);
 libc_hidden_proto (__inet_pton_length)
 
+/* Return the current fileto use as resolv.conf 
+	 __PATH_RESCONF o the value of the env var PATH_RESCONF if it exists*/
+const char *__resolv_path_resconf(void);
+
 #endif  /* _RESOLV_INTERNAL_H */
diff --git a/resolv/resolv_conf.c b/resolv/resolv_conf.c
index f391d30c27..9896e64cdc 100644
--- a/resolv/resolv_conf.c
+++ b/resolv/resolv_conf.c
@@ -119,11 +119,19 @@  conf_decrement (struct resolv_conf *conf)
     free (conf);
 }
 
+const char *__resolv_path_resconf(void) {
+	char *path_resconf = __libc_secure_getenv("PATH_RESCONF");
+	if (path_resconf)
+		return path_resconf;
+	else
+		return _PATH_RESCONF;
+}
+
 struct resolv_conf *
 __resolv_conf_get_current (void)
 {
   struct stat64 st;
-  if (stat64 (_PATH_RESCONF, &st) != 0)
+  if (stat64 (__resolv_path_resconf(), &st) != 0)
     {
     switch (errno)
       {